From d1a528ca3126857ae2aad913e389923eab2da166 Mon Sep 17 00:00:00 2001 From: ZiuChen <457353192@qq.com> Date: Sat, 13 Jan 2024 04:07:58 +0000 Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20ZiuChen/?= =?UTF-8?q?ZiuChen.github.io@931788551a002171e109b7d7925bc78744a958e2=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 404.html | 2 +- .../【2023】青训营 - 前端练习题汇总解析.html | 2 +- article/【字节跳动】前端面试题总结.html | 2 +- article/【快手】深入理解前端面试题.html | 2 +- article/【用友金融】前端面试题总结.html | 2 +- article/一文读懂事件冒泡与事件捕获.html | 2 +- article/一文读懂伪类与伪元素.html | 2 +- article/一文读懂函数中this指向问题.html | 2 +- article/从0实现一个年度报告.html | 2 +- ...懂对象的数据属性描述符、存储属性描述符.html | 2 +- ...析defineProperty与Proxy实现的双向绑定.html | 2 +- article/深入JavaScript数据类型.html | 2 +- ...Vue3源码,看看Vue.use后究竟发生了什么?.html | 2 +- article/深入理解Proxy与Reflect.html | 2 +- article/深入理解浏览器缓存机制.html | 2 +- article/深入理解浏览器运行原理.html | 2 +- ...note_Front-end Engineering.md.75c88456.js} | 16 +++++++-------- ...Front-end Engineering.md.75c88456.lean.js} | 0 ...e15.js => note_React Hooks.md.7a05a70a.js} | 16 +++++++-------- ...s => note_React Hooks.md.7a05a70a.lean.js} | 0 ...96.js => note_React Router.md.8d4994d8.js} | 8 ++++---- ... => note_React Router.md.8d4994d8.lean.js} | 0 ....39156ee1.js => note_React.md.f9ad4dc2.js} | 2 +- ...lean.js => note_React.md.f9ad4dc2.lean.js} | 0 ....13a0409b.js => note_Redux.md.02de7bbe.js} | 14 ++++++------- ...lean.js => note_Redux.md.02de7bbe.lean.js} | 0 hashmap.json | 2 +- index.html | 2 +- note/CSS.html | 2 +- note/Front-end Engineering.html | 20 +++++++++---------- note/JavaScript.html | 2 +- note/JavaScriptEnhanced.html | 2 +- note/MySQL.html | 2 +- note/React Hooks.html | 20 +++++++++---------- note/React Router.html | 12 +++++------ note/React.html | 6 +++--- note/Redux.html | 18 ++++++++--------- note/SSR.html | 2 +- project/ClipboardManager/guide/index.html | 2 +- project/ClipboardManager/index.html | 2 +- project/ClipboardManager/log/index.html | 2 +- project/ClipboardManager/statement/index.html | 2 +- project/JSRunner/index.html | 2 +- project/JSRunner/log/index.html | 2 +- project/Markdown/index.html | 2 +- project/Markdown/log/index.html | 2 +- project/Markdown/shortcut/index.html | 2 +- project/SmartWordBreak/index.html | 2 +- project/SmartWordBreak/log/index.html | 2 +- project/SmartWordBreak/statement/index.html | 2 +- self/index.html | 2 +- works/contribution.html | 2 +- works/opensource.html | 2 +- 53 files changed, 104 insertions(+), 104 deletions(-) rename assets/{note_Front-end Engineering.md.3049f99d.js => note_Front-end Engineering.md.75c88456.js} (98%) rename assets/{note_Front-end Engineering.md.3049f99d.lean.js => note_Front-end Engineering.md.75c88456.lean.js} (100%) rename assets/{note_React Hooks.md.5193de15.js => note_React Hooks.md.7a05a70a.js} (99%) rename assets/{note_React Hooks.md.5193de15.lean.js => note_React Hooks.md.7a05a70a.lean.js} (100%) rename assets/{note_React Router.md.c89e8196.js => note_React Router.md.8d4994d8.js} (99%) rename assets/{note_React Router.md.c89e8196.lean.js => note_React Router.md.8d4994d8.lean.js} (100%) rename assets/{note_React.md.39156ee1.js => note_React.md.f9ad4dc2.js} (99%) rename assets/{note_React.md.39156ee1.lean.js => note_React.md.f9ad4dc2.lean.js} (100%) rename assets/{note_Redux.md.13a0409b.js => note_Redux.md.02de7bbe.js} (99%) rename assets/{note_Redux.md.13a0409b.lean.js => note_Redux.md.02de7bbe.lean.js} (100%) diff --git a/404.html b/404.html index cc6b6cd4..6b85e77b 100644 --- a/404.html +++ b/404.html @@ -15,7 +15,7 @@
Skip to content

404

PAGE NOT FOUND

But if you don't change your direction, and if you keep looking, you may end up where you are heading.

Released under the MIT License.

- + \ No newline at end of file diff --git a/article/【2023】青训营 - 前端练习题汇总解析.html b/article/【2023】青训营 - 前端练习题汇总解析.html index 4f1005bf..cfd899b4 100644 --- a/article/【2023】青训营 - 前端练习题汇总解析.html +++ b/article/【2023】青训营 - 前端练习题汇总解析.html @@ -540,7 +540,7 @@ return true } } - + \ No newline at end of file diff --git a/article/【字节跳动】前端面试题总结.html b/article/【字节跳动】前端面试题总结.html index 46d0ee2b..1a2c0e5c 100644 --- a/article/【字节跳动】前端面试题总结.html +++ b/article/【字节跳动】前端面试题总结.html @@ -72,7 +72,7 @@ false

问答题

HTTP缓存的请求头与响应头有哪些

宏任务与微任务有哪些区别?简述他们的应用场景

宏任务与微任务优先级不同

常见的宏任务与微任务

算法

中文数字转为阿拉伯数字

给定一个字符串,返回该字符串的所有组合

js
输入 abc 
 输出 ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
输入 abc 
 输出 ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']

回溯算法

- + \ No newline at end of file diff --git a/article/【快手】深入理解前端面试题.html b/article/【快手】深入理解前端面试题.html index fd3e8fb6..6efa8353 100644 --- a/article/【快手】深入理解前端面试题.html +++ b/article/【快手】深入理解前端面试题.html @@ -356,7 +356,7 @@ console.log(twoSum([1, 2, 3, 4, 4], 5)) // 2 console.log(twoSum([1, 1, 2, 3, 4, 4], 5)) // 3 - + \ No newline at end of file diff --git a/article/【用友金融】前端面试题总结.html b/article/【用友金融】前端面试题总结.html index 72abee68..054ade7b 100644 --- a/article/【用友金融】前端面试题总结.html +++ b/article/【用友金融】前端面试题总结.html @@ -120,7 +120,7 @@ console.log(typeof null) // object
console.log(typeof []) // object
 console.log(typeof {}) // object
 console.log(typeof null) // object

JS浮点数相加精度问题

简单讲讲Vue Router原理

ES6熟悉吗

ES6教程

CSS如何实现水平居中与垂直居中

参见CSS学习笔记

- + \ No newline at end of file diff --git a/article/一文读懂事件冒泡与事件捕获.html b/article/一文读懂事件冒泡与事件捕获.html index c7327c50..65df67c2 100644 --- a/article/一文读懂事件冒泡与事件捕获.html +++ b/article/一文读懂事件冒泡与事件捕获.html @@ -88,7 +88,7 @@ // some code ... e.preventDefault() });

相关链接

事件冒泡及捕获

- + \ No newline at end of file diff --git a/article/一文读懂伪类与伪元素.html b/article/一文读懂伪类与伪元素.html index cc8fe195..3a038a2c 100644 --- a/article/一文读懂伪类与伪元素.html +++ b/article/一文读懂伪类与伪元素.html @@ -132,7 +132,7 @@ .father { width: 100px; }

相关链接

代码片段

CSS选择器

- + \ No newline at end of file diff --git a/article/一文读懂函数中this指向问题.html b/article/一文读懂函数中this指向问题.html index ec349e03..df30f4a4 100644 --- a/article/一文读懂函数中this指向问题.html +++ b/article/一文读懂函数中this指向问题.html @@ -296,7 +296,7 @@ person1.foo4()() // 隐式绑定: person1 person1.foo4.call(person2)() // 显式绑定: person2 person1.foo4().call(person2) // 隐式绑定: person1 - + \ No newline at end of file diff --git a/article/从0实现一个年度报告.html b/article/从0实现一个年度报告.html index f4e51cb6..01bc91d9 100644 --- a/article/从0实现一个年度报告.html +++ b/article/从0实现一个年度报告.html @@ -194,7 +194,7 @@ nextPage() } })

代码打包

由于最后需要将项目放到码上掘金平台运行,所以需要考虑静态资源的加载问题

这里我使用到了Vite提供的类似file-loader的功能,可以将大小在指定阈值下的图片资源直接转为行内的DataURL,配置选项是config.build.assetsInlineLimit,这样所有的图片资源都不必考虑外部引入的问题,直接内嵌进代码。

技术介绍

主界面使用的是Vue3的SFC,主要逻辑都在单文件组件中完成。通过JSX语法编写不同页面的内容,这样更方便我们为每个节点添加不同的动画。

JSX编写的组件通过全局注册后,在SFC中通过<Component>动态加载。

图片资源方面,使用到了雪碧图,部署后可以降低客户端发起HTTP请求频次,提高性能

代码复用方面,样式代码都抽离为单个的xxxx.less文件,哪里用到了直接导入即可

使用到了Pinia状态管理库,将switching pageId audioStatus等全局状态放到其中管理非常方便,避免了provideinject的繁琐

功能介绍

Demo展示

Demo(Vercel)

jcode

- + \ No newline at end of file diff --git a/article/彻底搞懂对象的数据属性描述符、存储属性描述符.html b/article/彻底搞懂对象的数据属性描述符、存储属性描述符.html index e24f5567..e65ae3fb 100644 --- a/article/彻底搞懂对象的数据属性描述符、存储属性描述符.html +++ b/article/彻底搞懂对象的数据属性描述符、存储属性描述符.html @@ -152,7 +152,7 @@ function foo() { console.log("resetted address value once") } - + \ No newline at end of file diff --git a/article/浅析defineProperty与Proxy实现的双向绑定.html b/article/浅析defineProperty与Proxy实现的双向绑定.html index b6ad2475..aecf46e4 100644 --- a/article/浅析defineProperty与Proxy实现的双向绑定.html +++ b/article/浅析defineProperty与Proxy实现的双向绑定.html @@ -56,7 +56,7 @@ }) </script> </body>

通过 defineProperty 实现的响应式,不能检测数组和对象的变化:

对于对象:

Vue 无法检测 property 的添加或移除。

var vm = new Vue({ data:{ a:1 } }) // vm.a 是响应式的 vm.b = 2 // vm.b 是非响应式的

对于数组:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

🔰 Vue3的响应式原理

- + \ No newline at end of file diff --git a/article/深入JavaScript数据类型.html b/article/深入JavaScript数据类型.html index 484442b8..11092708 100644 --- a/article/深入JavaScript数据类型.html +++ b/article/深入JavaScript数据类型.html @@ -82,7 +82,7 @@ } console.log(classof({})) // Object

核心原理

深入Object.prototype.toString

参考ECMA6规范文档:Object.prototype.toString()

ES5标准下 Object.prototype.toString 执行原理

ES6标准下 Object.prototype.toString 执行原理

Object.prototype.toString()被调用时,会进行如下步骤:

Object.prototype.toString()

在ES6里,之前的内部属性 [[Class]] 不再使用,取而代之的是一系列的 internal slot

- + \ No newline at end of file diff --git a/article/深入Vue3源码,看看Vue.use后究竟发生了什么?.html b/article/深入Vue3源码,看看Vue.use后究竟发生了什么?.html index 3016db96..2b8141e9 100644 --- a/article/深入Vue3源码,看看Vue.use后究竟发生了什么?.html +++ b/article/深入Vue3源码,看看Vue.use后究竟发生了什么?.html @@ -216,7 +216,7 @@ const app = createApp(App).use(registerElement) app.mount('#app')

当有新的需要使用的组件时,只需要到register-element.ts文件中引入一次即可。

参考阅读

Vue文档: App.use

Vue文档: Plugins

- + \ No newline at end of file diff --git a/article/深入理解Proxy与Reflect.html b/article/深入理解Proxy与Reflect.html index 2aa85412..8516f563 100644 --- a/article/深入理解Proxy与Reflect.html +++ b/article/深入理解Proxy与Reflect.html @@ -306,7 +306,7 @@ const stu = new Reflect.construct(Person, ['ziu', 18], Student) console.log(stu) - + \ No newline at end of file diff --git a/article/深入理解浏览器缓存机制.html b/article/深入理解浏览器缓存机制.html index 55bf739e..6ae6d5bc 100644 --- a/article/深入理解浏览器缓存机制.html +++ b/article/深入理解浏览器缓存机制.html @@ -30,7 +30,7 @@ // 数字都用16进制表示 return `${fileLength.toString(16)}-${fileLastModifiedTime.toString(16)}` }

Last-Modified与If-Modified-Since

Last-Modified是该资源文件最后一次更改时间,服务器会在ResponseHeader里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到RequestHeader里的If-Modified-Since里,服务器在接收到后也会做比对,如果相同则命中协商缓存。

If-None-Match的优先级要高于If-Modified-Since,即:如果浏览器同时存在

两种协商缓存的区别

内存缓存与硬盘缓存

当我们打开一个新网页,服务器返回200,将资源发送给浏览器,浏览器做本地缓存

当我们刷新标签页,浏览器从内存缓存获得资源

当我们关闭标签页重新打开,浏览器从硬盘缓存获得资源

在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(MemoryCache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(DiskCache)。

用户对浏览器缓存的控制

参考资料

[稀土掘金] 彻底理解浏览器的缓存机制

[微信公众号] 浏览器的缓存机制小结

[微信公众号] 浏览器缓存机制剖析

[RFC-9111] Expires

[MDN] Expires

[MDN] Cache-Control

[MDN] ETag

- + \ No newline at end of file diff --git a/article/深入理解浏览器运行原理.html b/article/深入理解浏览器运行原理.html index 8be90162..7bfa4035 100644 --- a/article/深入理解浏览器运行原理.html +++ b/article/深入理解浏览器运行原理.html @@ -118,7 +118,7 @@ DOMContentLoaded enter
script enter
 defer script enter
 DOMContentLoaded enter

async属性

async属性也可以做到:让脚本异步加载而不阻塞DOM树的构建,它与defer的区别:

要使用async属性标记的script操作DOM,必须在其中使用DOMContentLoaded监听器的回调函数,在该事件触发(DOM树构建完毕)后,执行相应的回调函数

- + \ No newline at end of file diff --git a/assets/note_Front-end Engineering.md.3049f99d.js b/assets/note_Front-end Engineering.md.75c88456.js similarity index 98% rename from assets/note_Front-end Engineering.md.3049f99d.js rename to assets/note_Front-end Engineering.md.75c88456.js index ec950f94..aedbefc6 100644 --- a/assets/note_Front-end Engineering.md.3049f99d.js +++ b/assets/note_Front-end Engineering.md.75c88456.js @@ -96,7 +96,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o= })() // moduleB.js -console.log(moduleA.name) // 在其他模块中调用

CommonJS

CommonJS是一种规范,当初命名为ServerJS,旨在浏览器以外的地方使用,后为体现其广泛性,改名为CommonJS,简称CJS

规范 是用来指导 实现的

所以,Node.js对CommonJS进行了支持和实现,让JavaScript在Node上运行时可以实现模块化开发

js
// env.js
+console.log(moduleA.name) // 在其他模块中调用

CommonJS

CommonJS是一种规范,当初命名为ServerJS,旨在浏览器以外的地方使用,后为体现其广泛性,改名为CommonJS,简称CJS

规范 是用来指导 实现的

所以,Node.js对CommonJS进行了支持和实现,让JavaScript在Node上运行时可以实现模块化开发

js
// env.js
 exports.name = 'Ziu'
 exports.age = 18
// env.js
 exports.name = 'Ziu'
@@ -126,7 +126,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
 sum(1, 2) // 3
 
 const { name, age } = require('env.js')
-console.log(name, age) // Ziu 18

exports的本质

exportsrequire在Node中的本质

js
// utils.js
+console.log(name, age) // Ziu 18

exports的本质

exportsrequire在Node中的本质

js
// utils.js
 exports.a = 0
 
 // 1s后修改a值
@@ -176,7 +176,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
 module.exports = {
   name,
   run
-}

二者的区别

既然如此,为什么还要存在exports这个概念呢?

  • 在CommonJS中是没有module.exports的概念的
  • 为了实现模块的导出,Node.js使用的是Module类,每一个模块都是Module的实例,也就是module
  • 所以在Node.js中真正用于导出的并不是exports,而是module.exports
  • module对象中的exports属性是exports对象的一个引用
    • module.exports === exports === utils

如果module.exports不再引用exports对象了,修改exports对象也就没有意义了

js
// utils.js
+}

二者的区别

既然如此,为什么还要存在exports这个概念呢?

  • 在CommonJS中是没有module.exports的概念的
  • 为了实现模块的导出,Node.js使用的是Module类,每一个模块都是Module的实例,也就是module
  • 所以在Node.js中真正用于导出的并不是exports,而是module.exports
  • module对象中的exports属性是exports对象的一个引用
    • module.exports === exports === utils

如果module.exports不再引用exports对象了,修改exports对象也就没有意义了

js
// utils.js
 module.exports = {
   name: 'Ziu'
 }
@@ -190,7 +190,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
 console.log(utils.age) // undefined
// index.js
 const utils = require('utils.js')
 console.log(utils.name) // Ziu
-console.log(utils.age) // undefined

当使用module.exports = { ... }后,模块中原有的exports不再被导入识别,导入的内容将变为module.exports指定的对象内容

require的本质

require是一个函数,可以帮助我们导入一个文件(模块)中导出的对象

  • 为什么可以省略掉.js后缀,直接使用require('./utils')
  • 为什么可以省略掉index.js,直接使用require('./tools')导入tools/index.js

这涉及到require在匹配路径后的查找规则:

分为三种情况:内置模块、自定义路径、包名

  • 导入Node.js内置的模块,如const path = require('path')
    • 直接返回该内置模块 并停止后续的查找
  • 根据路径导入自定义的模块,如const utils = require('./{filename}')
    • 按照路径寻找该模块./ ../ /
    • 如果指定了后缀名,则按照后缀名查找
    • 如果未指定后缀名,则:
      1. 直接查找该文件
      2. 查找{filename}.js文件
      3. 查找{filename}.json文件
      4. 查找{filename}.node文件
    • 如果按照上述方式没找到文件,则{filename}作为路径继续查找
    • 查找目录下的index文件 {filename}/index
      1. 查找{filename}/index.js文件
      2. ··· ···
    • 没找到:报错Cannot find module 'xxx'
  • 包名,如const lodash = require('lodash')
    • 到项目根目录的node_modules中查找
    • node_modules/{package_name}/index.js
    • 当前项目目录的node_modules找不到则继续向上查找,直到查找到根目录的node_modules

模块的加载过程

  • 模块在被第一次引入时,模块中的JS代码会被运行一次
    • 代码执行顺序与require的位置相关
  • 模块如果被多次引入,会被缓存,最终只加载一次
    • 这是因为每个模块对象module上都有一个属性loaded
    • loaded === false表示该模块尚未被加载
    • 第二次被require引入时会检查该属性是否为true
  • 如果有循环引用,加载顺序如何?
    • 数据结构:图结构(graph)遍历时有深度优先搜索(DFS)、广度优先搜索(BFS)两种算法
    • Node采用的是深度优先算法

CommonJS的缺点

  • 加载模块是同步加载的
    • 只有等到对应的模块加载完毕,当前模块中的内容才能被执行
    • 当然,在服务器中加载JS文件都是本地文件,加载速度非常快,不会受影响
  • 但是在浏览器中使用CommonJS
    • 需要先从服务器下载JS文件,后加载运行
    • 阻塞JS执行 阻塞页面加载
  • 在WebPack中使用CommonJS
    • CommonJS会被WebPack解析
    • 将CommonJS代码转化为bundle 浏览器可以直接运行

ESModule

  • ES6 模块采用编译时加载,使得编译时就能确定模块的依赖关系,有助于静态优化
  • CommonJS模块在运行时加载,且必须借助对象加载模块内容

exportimport用法概览

ESModule借助exportimport导入导出内容,需要注意的是导入导出的并不是对象

export定义的是当前模块导出的接口import可以导入来自其他不同模块的接口

  • export default可以设置默认导出对象
  • export { ... }可以统一导出多个内容
  • exportimport都可以使用as关键字重命名导出/导入的接口
  • import * from 'xxx' export * from 'xxx'批量导入/导出
js
// utils.js
+console.log(utils.age) // undefined

当使用module.exports = { ... }后,模块中原有的exports不再被导入识别,导入的内容将变为module.exports指定的对象内容

require的本质

require是一个函数,可以帮助我们导入一个文件(模块)中导出的对象

  • 为什么可以省略掉.js后缀,直接使用require('./utils')
  • 为什么可以省略掉index.js,直接使用require('./tools')导入tools/index.js

这涉及到require在匹配路径后的查找规则:

分为三种情况:内置模块、自定义路径、包名

  • 导入Node.js内置的模块,如const path = require('path')
    • 直接返回该内置模块 并停止后续的查找
  • 根据路径导入自定义的模块,如const utils = require('./{filename}')
    • 按照路径寻找该模块./ ../ /
    • 如果指定了后缀名,则按照后缀名查找
    • 如果未指定后缀名,则:
      1. 直接查找该文件
      2. 查找{filename}.js文件
      3. 查找{filename}.json文件
      4. 查找{filename}.node文件
    • 如果按照上述方式没找到文件,则{filename}作为路径继续查找
    • 查找目录下的index文件 {filename}/index
      1. 查找{filename}/index.js文件
      2. ··· ···
    • 没找到:报错Cannot find module 'xxx'
  • 包名,如const lodash = require('lodash')
    • 到项目根目录的node_modules中查找
    • node_modules/{package_name}/index.js
    • 当前项目目录的node_modules找不到则继续向上查找,直到查找到根目录的node_modules

模块的加载过程

  • 模块在被第一次引入时,模块中的JS代码会被运行一次
    • 代码执行顺序与require的位置相关
  • 模块如果被多次引入,会被缓存,最终只加载一次
    • 这是因为每个模块对象module上都有一个属性loaded
    • loaded === false表示该模块尚未被加载
    • 第二次被require引入时会检查该属性是否为true
  • 如果有循环引用,加载顺序如何?
    • 数据结构:图结构(graph)遍历时有深度优先搜索(DFS)、广度优先搜索(BFS)两种算法
    • Node采用的是深度优先算法

CommonJS的缺点

  • 加载模块是同步加载的
    • 只有等到对应的模块加载完毕,当前模块中的内容才能被执行
    • 当然,在服务器中加载JS文件都是本地文件,加载速度非常快,不会受影响
  • 但是在浏览器中使用CommonJS
    • 需要先从服务器下载JS文件,后加载运行
    • 阻塞JS执行 阻塞页面加载
  • 在WebPack中使用CommonJS
    • CommonJS会被WebPack解析
    • 将CommonJS代码转化为bundle 浏览器可以直接运行

ESModule

  • ES6 模块采用编译时加载,使得编译时就能确定模块的依赖关系,有助于静态优化
  • CommonJS模块在运行时加载,且必须借助对象加载模块内容

exportimport用法概览

ESModule借助exportimport导入导出内容,需要注意的是导入导出的并不是对象

export定义的是当前模块导出的接口import可以导入来自其他不同模块的接口

  • export default可以设置默认导出对象
  • export { ... }可以统一导出多个内容
  • exportimport都可以使用as关键字重命名导出/导入的接口
  • import * from 'xxx' export * from 'xxx'批量导入/导出
js
// utils.js
 export function sum(a, b) {
   return a + b
 }
@@ -230,7 +230,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
 
 sum(1, 2) // 3
 sub(2, 3) // -1
-log(name, age, ENV_VARIABLE) // 'Ziu' 18 'Hello, World!'

需要注意的是,在浏览器中要使用ESModule,需要为<script>标签添加module标记:

<script src="index.js" type="module"></script>

  • 当浏览器解析到type="module"的JS代码后,会分析模块中导入的ESModule模块
  • 每导入一个ESModule模块,浏览器都会发起一个HTTP请求去加载它
  • 在本地运行时加载不同协议头的文件会遇到跨域问题,需要开启本地Web服务器

另外,exportimport必须位于模块的顶层,如果位于作用域内会报错,因为这就无法对代码进行静态分析优化了

export详解

export有两种导出方式:

  • 命名导出 export const name = 'Ziu' export { v1, v2 } export * from 'xxx'
    • 导出时需要指定名字
    • 导入时也需要知道对应的名字
  • 默认导出 export default AGE = 18
    • 在从其他位置导入时需要为此默认导出指定新的名字
    • 给用户方便:不必阅读文档就可以加载模块

值的动态绑定

  • ESModule模块通过export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
  • CommonJS模块输出的是值的缓存,不存在动态更新

我们援引之前介绍CJS时的案例,将后缀名改为mjs即可在Node中运行ESModule模块代码

初始获得的a值为0,经过1s后,在utils.mjs中修改了a的值,这时导入utils.mjs模块的其他模块可以获取到a最新的值

js
// utils.mjs
+log(name, age, ENV_VARIABLE) // 'Ziu' 18 'Hello, World!'

需要注意的是,在浏览器中要使用ESModule,需要为<script>标签添加module标记:

<script src="index.js" type="module"></script>

  • 当浏览器解析到type="module"的JS代码后,会分析模块中导入的ESModule模块
  • 每导入一个ESModule模块,浏览器都会发起一个HTTP请求去加载它
  • 在本地运行时加载不同协议头的文件会遇到跨域问题,需要开启本地Web服务器

另外,exportimport必须位于模块的顶层,如果位于作用域内会报错,因为这就无法对代码进行静态分析优化了

export详解

export有两种导出方式:

  • 命名导出 export const name = 'Ziu' export { v1, v2 } export * from 'xxx'
    • 导出时需要指定名字
    • 导入时也需要知道对应的名字
  • 默认导出 export default AGE = 18
    • 在从其他位置导入时需要为此默认导出指定新的名字
    • 给用户方便:不必阅读文档就可以加载模块

值的动态绑定

  • ESModule模块通过export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
  • CommonJS模块输出的是值的缓存,不存在动态更新

我们援引之前介绍CJS时的案例,将后缀名改为mjs即可在Node中运行ESModule模块代码

初始获得的a值为0,经过1s后,在utils.mjs中修改了a的值,这时导入utils.mjs模块的其他模块可以获取到a最新的值

js
// utils.mjs
 export let a = 0
 
 // 1s后修改a值
@@ -306,7 +306,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
   import('light.js').then(() => ...)
 }

传入动态值

js
let moduleName = () => ['Home', 'History', 'User'][0]
 import(\`./\${moduleName()}.js\`)
let moduleName = () => ['Home', 'History', 'User'][0]
-import(\`./\${moduleName()}.js\`)

import.meta

ES2020引入了import.meta,它仅能在模块内部使用,包含一些模块自身的信息,即模块元信息

  • import.meta.url 返回当前模块的URL路径
    • 浏览器加载ESModule都是通过HTTP发起请求
      • 例如当前模块为fetchData.js,要在模块内引入一个名为data.json的数据:
      • import( new URL('data.json', import.meta.url) )
    • Node.js环境下,该值都是file://协议的链接
  • import.meta.scriptElement
    • 浏览器特有的属性
    • 返回加载模块的<script>标签,相当于document.currentScript

规范中并未规定import.meta中包含哪些属性,一般包括上面两个属性

深入理解模块加载

ESModule的解析过程

ESModule的解析过程可以分为三个阶段:

  • 构建 Construction
    • 根据地址查找JS文件,并发起HTTP请求下载,将其解析为模块记录 Module Record
  • 实例化 Instatiation
    • 对模块记录进行实例化,并为其分配内存空间
    • 解析ESModule模块的导入和导出语句,将模块指向对应的内存地址
    • 例如export const name = 'Ziu',会将变量name添加到模块环境记录中 Module Enviroment Record
  • 运行 Evaluation
    • 运行代码,计算值,并且将值填充到内存地址中
    • 将导入导出的赋给对应的变量name = 'Ziu'

ESModule解析过程

文章推荐:ES modules: A cartoon deep-dive

MJS和CJS的区别

  • CommonJS模块输出的是值的拷贝,而ESModule模块输出的是值的引用
    • CJS导出的变量,其值如果在模块内发生变化,外部导入是不会同步更新的,除非导出的是一个取值函数
    • MJS导出变量,外部模块每次访问时都会得到该变量最新的值,即使变量在模块内被修改了
  • CommonJS模块是运行时加载,而ESModule是编译时输出接口
    • CJS是通过对象实现的导入导出,它在运行时才被确定依赖关系和其值
    • MJS则是通过静态定义,在代码运行之前的静态解析阶段即可确定模块的导入导出内容
  • CommonJS模块的require()是同步加载模块,而ESModule模块的import命令是异步加载模块
    • import命令拥有一个独立的模块依赖的解析阶段

CJS中的循环加载

设想有以下两文件 a.jsb.js

js
// a.js
+import(\`./\${moduleName()}.js\`)

import.meta

ES2020引入了import.meta,它仅能在模块内部使用,包含一些模块自身的信息,即模块元信息

  • import.meta.url 返回当前模块的URL路径
    • 浏览器加载ESModule都是通过HTTP发起请求
      • 例如当前模块为fetchData.js,要在模块内引入一个名为data.json的数据:
      • import( new URL('data.json', import.meta.url) )
    • Node.js环境下,该值都是file://协议的链接
  • import.meta.scriptElement
    • 浏览器特有的属性
    • 返回加载模块的<script>标签,相当于document.currentScript

规范中并未规定import.meta中包含哪些属性,一般包括上面两个属性

深入理解模块加载

ESModule的解析过程

ESModule的解析过程可以分为三个阶段:

  • 构建 Construction
    • 根据地址查找JS文件,并发起HTTP请求下载,将其解析为模块记录 Module Record
  • 实例化 Instatiation
    • 对模块记录进行实例化,并为其分配内存空间
    • 解析ESModule模块的导入和导出语句,将模块指向对应的内存地址
    • 例如export const name = 'Ziu',会将变量name添加到模块环境记录中 Module Enviroment Record
  • 运行 Evaluation
    • 运行代码,计算值,并且将值填充到内存地址中
    • 将导入导出的赋给对应的变量name = 'Ziu'

ESModule解析过程

文章推荐:ES modules: A cartoon deep-dive

MJS和CJS的区别

  • CommonJS模块输出的是值的拷贝,而ESModule模块输出的是值的引用
    • CJS导出的变量,其值如果在模块内发生变化,外部导入是不会同步更新的,除非导出的是一个取值函数
    • MJS导出变量,外部模块每次访问时都会得到该变量最新的值,即使变量在模块内被修改了
  • CommonJS模块是运行时加载,而ESModule是编译时输出接口
    • CJS是通过对象实现的导入导出,它在运行时才被确定依赖关系和其值
    • MJS则是通过静态定义,在代码运行之前的静态解析阶段即可确定模块的导入导出内容
  • CommonJS模块的require()是同步加载模块,而ESModule模块的import命令是异步加载模块
    • import命令拥有一个独立的模块依赖的解析阶段

CJS中的循环加载

设想有以下两文件 a.jsb.js

js
// a.js
 exports.done = false
 const b = require('./b.js')
 console.log('在 a.js 之中,b.done = %j', b.done)
@@ -340,7 +340,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
 b.js 执行完毕
  a.js 之中,b.done = true
 a.js 执行完毕
- main.js 之中, a.done=true, b.done=true

总结:

  • CJS的模块导出是输出值的拷贝,而不是引用,值的变化不是动态的,而是会被缓存的
  • 循环加载时,CJS模块导出的值是当前已经执行部分代码产生的结果的值,而不是模块代码完全执行完后的最终值

MJS中的循环加载

ESModule的导入和导出与CommonJS有本质不同:

js
// a.mjs
+ main.js 之中, a.done=true, b.done=true

总结:

  • CJS的模块导出是输出值的拷贝,而不是引用,值的变化不是动态的,而是会被缓存的
  • 循环加载时,CJS模块导出的值是当前已经执行部分代码产生的结果的值,而不是模块代码完全执行完后的最终值

MJS中的循环加载

ESModule的导入和导出与CommonJS有本质不同:

js
// a.mjs
 import { bar } from './b.mjs'
 console.log('a.mjs')
 console.log(bar)
@@ -356,7 +356,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const o=
 import { foo } from './a.mjs'
 console.log('b.mjs')
 console.log(foo)
-export let bar = 'bar'

执行a.mjs后发现报错了:ReferenceError: Cannot access 'foo' before initialization,变量foo未定义

  • MJS模块在代码执行前会进行静态分析
  • 分析a.mjs的依赖关系时,发现其依赖了b.mjs
  • 于是加载b.mjs并解析它的依赖关系
  • 解析b.mjs的过程中,发现它又依赖了a.mjs
  • 这时引擎不会再去加载a.mjs 而是认为a.mjs这个模块的Module Record已经存在了
  • 继续向下执行,执行到console.log(foo)时发现foo未定义 抛出错误

要实现预期效果,可以将foobar改写为取值函数,这时执行就不会报错了:

js
// a.mjs
+export let bar = 'bar'

执行a.mjs后发现报错了:ReferenceError: Cannot access 'foo' before initialization,变量foo未定义

  • MJS模块在代码执行前会进行静态分析
  • 分析a.mjs的依赖关系时,发现其依赖了b.mjs
  • 于是加载b.mjs并解析它的依赖关系
  • 解析b.mjs的过程中,发现它又依赖了a.mjs
  • 这时引擎不会再去加载a.mjs 而是认为a.mjs这个模块的Module Record已经存在了
  • 继续向下执行,执行到console.log(foo)时发现foo未定义 抛出错误

要实现预期效果,可以将foobar改写为取值函数,这时执行就不会报错了:

js
// a.mjs
 import { bar } from './b.mjs'
 console.log('a.mjs')
 console.log(bar())
diff --git a/assets/note_Front-end Engineering.md.3049f99d.lean.js b/assets/note_Front-end Engineering.md.75c88456.lean.js
similarity index 100%
rename from assets/note_Front-end Engineering.md.3049f99d.lean.js
rename to assets/note_Front-end Engineering.md.75c88456.lean.js
diff --git a/assets/note_React Hooks.md.5193de15.js b/assets/note_React Hooks.md.7a05a70a.js
similarity index 99%
rename from assets/note_React Hooks.md.5193de15.js
rename to assets/note_React Hooks.md.7a05a70a.js
index 08f0057b..774aa59f 100644
--- a/assets/note_React Hooks.md.5193de15.js	
+++ b/assets/note_React Hooks.md.7a05a70a.js	
@@ -1,4 +1,4 @@
-import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p="/assets/useLayoutEffect.a5021f93.svg",o="/assets/SSR.7c2917b7.svg",m=JSON.parse('{"title":"React Hooks","description":"","frontmatter":{},"headers":[],"relativePath":"note/React Hooks.md","filePath":"note/React Hooks.md","lastUpdated":1704524259000}'),e={name:"note/React Hooks.md"},t=l(`

React Hooks

  • 认识和体验Hooks
  • State/Effect
  • Context/Reducer
  • Callback/Memo
  • Ref/LayoutEffect
  • 自定义Hooks使用

认识React Hooks

Hooks 是 React16.8 推出的新特性

在没有Hooks时,类组件能够完成的大部分工作,函数式组件都无法胜任:

  • 类组件可以定义并保存组件内部状态,并在状态发生改变时触发视图重新渲染
    • 函数式组件不行,每次调用函数其中的变量都会被重新初始化,重新渲染时整个函数都重新执行
  • 类组件可以在其内部的生命周期回调中添加副作用
    • 例如componentDidMount在类组件生命周期只会执行一次
    • 函数式组件没有生命周期,如果在函数体内发起网络请求,那每次重新渲染都会发起请求

类组件存在的问题:

  • 复杂组件变得难以理解
    • 业务代码相互耦合,类组件变得复杂
    • 逻辑强耦合在一起难以拆分,强行拆分会导致过度设计,进一步增加代码复杂度
  • class关键字的理解
    • 初学React时class关键字理解存在困难
    • 处理this的指向问题需要花费额外的心智负担
  • 组件状态复用
    • 要复用组件需要借助高阶组件
    • redux 中的 connect 或者 react-router 中的 withRouter,高阶组件的目的就是为了状态复
    • 或通过Provider、Consumer来共享状态,但是Comsumer嵌套问题较严重

Hooks带来的优势:

  • 在不编写class的情况下使用state和其他React特性(如生命周期)
  • Hooks 允许我们在函数式组件中使用状态,并在状态发生改变时让视图重新渲染
  • 同时,我们还可以在函数式组件中使用生命周期回调
  • 更多的优点 ...

计数器案例对比

分别使用Hooks和类组件编写一个计数器:

tsx
// CounterClass.jsx
+import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p="/assets/useLayoutEffect.a5021f93.svg",o="/assets/SSR.7c2917b7.svg",m=JSON.parse('{"title":"React Hooks","description":"","frontmatter":{},"headers":[],"relativePath":"note/React Hooks.md","filePath":"note/React Hooks.md","lastUpdated":1704524259000}'),e={name:"note/React Hooks.md"},t=l(`

React Hooks

  • 认识和体验Hooks
  • State/Effect
  • Context/Reducer
  • Callback/Memo
  • Ref/LayoutEffect
  • 自定义Hooks使用

认识React Hooks

Hooks 是 React16.8 推出的新特性

在没有Hooks时,类组件能够完成的大部分工作,函数式组件都无法胜任:

  • 类组件可以定义并保存组件内部状态,并在状态发生改变时触发视图重新渲染
    • 函数式组件不行,每次调用函数其中的变量都会被重新初始化,重新渲染时整个函数都重新执行
  • 类组件可以在其内部的生命周期回调中添加副作用
    • 例如componentDidMount在类组件生命周期只会执行一次
    • 函数式组件没有生命周期,如果在函数体内发起网络请求,那每次重新渲染都会发起请求

类组件存在的问题:

  • 复杂组件变得难以理解
    • 业务代码相互耦合,类组件变得复杂
    • 逻辑强耦合在一起难以拆分,强行拆分会导致过度设计,进一步增加代码复杂度
  • class关键字的理解
    • 初学React时class关键字理解存在困难
    • 处理this的指向问题需要花费额外的心智负担
  • 组件状态复用
    • 要复用组件需要借助高阶组件
    • redux 中的 connect 或者 react-router 中的 withRouter,高阶组件的目的就是为了状态复
    • 或通过Provider、Consumer来共享状态,但是Comsumer嵌套问题较严重

Hooks带来的优势:

  • 在不编写class的情况下使用state和其他React特性(如生命周期)
  • Hooks 允许我们在函数式组件中使用状态,并在状态发生改变时让视图重新渲染
  • 同时,我们还可以在函数式组件中使用生命周期回调
  • 更多的优点 ...

计数器案例对比

分别使用Hooks和类组件编写一个计数器:

tsx
// CounterClass.jsx
 import React, { PureComponent } from 'react'
 
 export default class CounterClass extends PureComponent {
@@ -192,7 +192,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
       console.log('Effect Cleaned Up')
     }
   }, [])
-...

useContext

在之前的开发中,要在组件中使用共享的Context有两种方式

  • 类组件可以通过ClassName.contextType = SomeContext绑定上下文
  • 在类的函数中通过this.context.xxx获取上下文中共享的状态
  • 同时有多个Context时/函数式组件中,通过SomeContext.Consumer的方式共享上下文状态

其中最大的问题就是:多个Context在同时使用时会引入大量的嵌套,而useContext可以帮我们解决这个问题

通过useContext可以直接获取到某个上下文中共享的状态变量

tsx
// Profile.jsx
+...

useContext

在之前的开发中,要在组件中使用共享的Context有两种方式

  • 类组件可以通过ClassName.contextType = SomeContext绑定上下文
  • 在类的函数中通过this.context.xxx获取上下文中共享的状态
  • 同时有多个Context时/函数式组件中,通过SomeContext.Consumer的方式共享上下文状态

其中最大的问题就是:多个Context在同时使用时会引入大量的嵌套,而useContext可以帮我们解决这个问题

通过useContext可以直接获取到某个上下文中共享的状态变量

tsx
// Profile.jsx
 import React, { useContext } from 'react'
 import { UserContext, ThemeContext } from '../context'
 
@@ -272,7 +272,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
       </UserContext.Provider>
     </div>
   )
-}

当组件上层最近的SomeContext.Provider提供的值发生更新时,useContext会使用上下文中最新的数据触发组件的重新渲染

useReducer

useReducer并不是Redux的替代品

  • useReduceruseState在某些场景下的替代方案
  • 如果state需要处理的数据较为复杂,我们可以通过useReducer对其进行拆分
  • 或者需要修改的state需要依赖之前的state时,也可以使用useReducer

下面举一个例子:用户信息包含多个复杂的字段,当用户执行操作后需要同时对多个字段进行修改

我们分别用useStateuseReducer来实现:

tsx
// UserInfoWithReducer.jsx
+}

当组件上层最近的SomeContext.Provider提供的值发生更新时,useContext会使用上下文中最新的数据触发组件的重新渲染

useReducer

useReducer并不是Redux的替代品

  • useReduceruseState在某些场景下的替代方案
  • 如果state需要处理的数据较为复杂,我们可以通过useReducer对其进行拆分
  • 或者需要修改的state需要依赖之前的state时,也可以使用useReducer

下面举一个例子:用户信息包含多个复杂的字段,当用户执行操作后需要同时对多个字段进行修改

我们分别用useStateuseReducer来实现:

tsx
// UserInfoWithReducer.jsx
 import React, { useReducer } from 'react'
 
 function reducer(state, action) {
@@ -558,7 +558,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
 const bar1 = foo()
 
 bar1() // 1
-bar1() // 1

不论调用了多少次bar1,其内部取到的值都始终是最初的那个count,自然值也不会发生变化

所以,我们需要显式地为useCallback指定依赖state,这样才能准确地使用最新的状态定义新的函数

真实的useCallback使用场景

经过之前的说明,目前useCallback看起来并没有实际的用途,它没有减少函数的定义次数,甚至在不合理使用时还会出现闭包陷阱,而带来的唯一好处就是:当状态没有发生改变时,保证函数指向确定且唯一

下面我们举一个实际场景来说明useCallback的用途:

一个嵌套计数器的例子,外部计数器可以展示/改变计数器的值,子组件也可以通过调用props传递来的函数来改变计数器的值,同时外部计数器还包含了其他的状态在动态被修改

tsx
// InnerCounter.jsx
+bar1() // 1

不论调用了多少次bar1,其内部取到的值都始终是最初的那个count,自然值也不会发生变化

所以,我们需要显式地为useCallback指定依赖state,这样才能准确地使用最新的状态定义新的函数

真实的useCallback使用场景

经过之前的说明,目前useCallback看起来并没有实际的用途,它没有减少函数的定义次数,甚至在不合理使用时还会出现闭包陷阱,而带来的唯一好处就是:当状态没有发生改变时,保证函数指向确定且唯一

下面我们举一个实际场景来说明useCallback的用途:

一个嵌套计数器的例子,外部计数器可以展示/改变计数器的值,子组件也可以通过调用props传递来的函数来改变计数器的值,同时外部计数器还包含了其他的状态在动态被修改

tsx
// InnerCounter.jsx
 import React, { memo } from 'react'
 
 export default memo(function InnerCounter(props) {
@@ -1054,7 +1054,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
 const theme = useContext(ThemeContext)
 
 console.log(user.name, theme.primaryColor) // ...
-...

我们可以使用自定义Hook来简化这一操作,将所有的Context统一导入并转化为对象,直接在组件中使用

对之前的Profile组件使用Hook进行增强:

ts
// useSharedContext.js
+...

我们可以使用自定义Hook来简化这一操作,将所有的Context统一导入并转化为对象,直接在组件中使用

对之前的Profile组件使用Hook进行增强:

ts
// useSharedContext.js
 import { useContext } from 'react'
 import { UserContext, ThemeContext } from '../context'
 
@@ -1102,7 +1102,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
       <div>theme: {context.theme.theme}</div>
     </div>
   )
-}

案例二:获取滚动位置

tsx
// useScrollPosition.js
+}

案例二:获取滚动位置

tsx
// useScrollPosition.js
 import { useState, useEffect } from 'react'
 
 export function useScrollPosition(options = {}) {
@@ -1176,7 +1176,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
   )
 })
 
-export default GiantList

案例三:封装localStorage

在使用状态变量的时候,为状态变量值的更新添加副作用,将变量名作为key,值更新到localStorage中

tsx
// useLocalStorage.js
+export default GiantList

案例三:封装localStorage

在使用状态变量的时候,为状态变量值的更新添加副作用,将变量名作为key,值更新到localStorage中

tsx
// useLocalStorage.js
 import { useState, useEffect } from 'react'
 
 export function useLocalStorage(key) {
@@ -1246,7 +1246,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
   )
 })
 
-export default UserInfoStorage

这里的useState还展示了一个额外的用法,向useState传递一个函数,函数的返回值会作为状态变量的初始值

Redux Hooks

之前的Redux开发中,为了让组件和Redux建立联系,我们使用了react-redux中的connect

  • 必须与高阶函数结合,必须使用返回的高阶组件
  • 必须编写mapStateToProps mapDispatchToProps,将上下文状态映射到props中

从Redux7.1开始,支持Hook写法,不再需要编写connect以及映射函数了

useSelector

将state映射到组件中

  • 参数一:将state映射到需要的数据中
  • 参数二:可以进行比较,来决定组件是否重新渲染

默认情况下useSelector监听整个state的变化,只要state中有状态变量发生变化,无论当前组件是否使用到了这个状态变量,都会触发组件的重新渲染。这就需要我们显式地为其指定重新渲染的判断条件

useSelector会比较我们返回的两个对象是否相等:

ts
const refEquality = (a, b) => (a === b);
const refEquality = (a, b) => (a === b);

只有两个对象全等时,才可以不触发重新渲染

useDispatch

直接获取dispatch函数,之后在组件中直接调用即可

另外,我们还可以通过useStore来获取当前的store对象

拿之前Redux的计数器举例,使用useSelectoruseDispatch进行重构:

tsx
// [Now] Counter.jsx
+export default UserInfoStorage

这里的useState还展示了一个额外的用法,向useState传递一个函数,函数的返回值会作为状态变量的初始值

Redux Hooks

之前的Redux开发中,为了让组件和Redux建立联系,我们使用了react-redux中的connect

  • 必须与高阶函数结合,必须使用返回的高阶组件
  • 必须编写mapStateToProps mapDispatchToProps,将上下文状态映射到props中

从Redux7.1开始,支持Hook写法,不再需要编写connect以及映射函数了

useSelector

将state映射到组件中

  • 参数一:将state映射到需要的数据中
  • 参数二:可以进行比较,来决定组件是否重新渲染

默认情况下useSelector监听整个state的变化,只要state中有状态变量发生变化,无论当前组件是否使用到了这个状态变量,都会触发组件的重新渲染。这就需要我们显式地为其指定重新渲染的判断条件

useSelector会比较我们返回的两个对象是否相等:

ts
const refEquality = (a, b) => (a === b);
const refEquality = (a, b) => (a === b);

只有两个对象全等时,才可以不触发重新渲染

useDispatch

直接获取dispatch函数,之后在组件中直接调用即可

另外,我们还可以通过useStore来获取当前的store对象

拿之前Redux的计数器举例,使用useSelectoruseDispatch进行重构:

tsx
// [Now] Counter.jsx
 import { memo } from 'react'
 import { useSelector, useDispatch, shallowEqual } from 'react-redux'
 import { addCount, subCount } from '../store/features/counter'
diff --git a/assets/note_React Hooks.md.5193de15.lean.js b/assets/note_React Hooks.md.7a05a70a.lean.js
similarity index 100%
rename from assets/note_React Hooks.md.5193de15.lean.js
rename to assets/note_React Hooks.md.7a05a70a.lean.js
diff --git a/assets/note_React Router.md.c89e8196.js b/assets/note_React Router.md.8d4994d8.js
similarity index 99%
rename from assets/note_React Router.md.c89e8196.js
rename to assets/note_React Router.md.8d4994d8.js
index a23b9210..76f68433 100644
--- a/assets/note_React Router.md.c89e8196.js	
+++ b/assets/note_React Router.md.8d4994d8.js	
@@ -78,7 +78,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const F=
       </div>
     )
   }
-}

另外,这里还有一个小技巧,在最末一个路由指定一个path为*的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面

嵌套路由

嵌套路由可以通过在Route组件内部嵌套新的Route组件来实现

再通过Outlet组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view)

我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand

同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件

tsx
// App.jsx
+}

另外,这里还有一个小技巧,在最末一个路由指定一个path为*的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面

嵌套路由

嵌套路由可以通过在Route组件内部嵌套新的Route组件来实现

再通过Outlet组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view)

我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand

同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件

tsx
// App.jsx
 import React, { PureComponent } from 'react'
 import { Routes, Route, Navigate, NavLink } from 'react-router-dom'
 import Home from './views/Home'
@@ -148,7 +148,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const F=
       </div>
     )
   }
-}

编程式导航(高阶组件)

之前使用的ReactRouter提供的路由跳转的组件,无论是Link还是NavLink可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转

ReactRouter提供了编程式导航的API:useNavigate

自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。

我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件

封装一个高阶组件withRouter,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法:

tsx
// withRouter.js
+}

编程式导航(高阶组件)

之前使用的ReactRouter提供的路由跳转的组件,无论是Link还是NavLink可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转

ReactRouter提供了编程式导航的API:useNavigate

自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。

我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件

封装一个高阶组件withRouter,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法:

tsx
// withRouter.js
 import { useNavigate } from 'react-router-dom'
 
 export function withRouter(WrapperComponent) {
@@ -202,7 +202,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const F=
       )
     }
   }
-)

我们使用withRouter高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转

这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目

动态路由(路由传参)

传递参数由两种方式:

  • 动态路由的方式
  • 查询字符串传递参数

动态路由是指:路由中的路径信息并不会固定

  • 比如匹配规则为/detail/:id时,/detail/123 detail/888都会被匹配上,并将123/888作为id参数传递
  • 其中/detail/:id这个匹配规则被称为动态路由

动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页

我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数

同样地,react-router-dom为我们提供了从路由获取参数的API:useParams,它是一个Hooks,我们将它应用到之前编写的高级组件withRouter

  • 在使用了withRouter的组件中,就可以通过this.props.router.params.xxx获取到当前路由中传递的参数
  • 使用动态匹配路由时,传递给Route组件的path属性为:xxx,这里是/detail/:id
tsx
// withRouter.js
+)

我们使用withRouter高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转

这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目

动态路由(路由传参)

传递参数由两种方式:

  • 动态路由的方式
  • 查询字符串传递参数

动态路由是指:路由中的路径信息并不会固定

  • 比如匹配规则为/detail/:id时,/detail/123 detail/888都会被匹配上,并将123/888作为id参数传递
  • 其中/detail/:id这个匹配规则被称为动态路由

动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页

我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数

同样地,react-router-dom为我们提供了从路由获取参数的API:useParams,它是一个Hooks,我们将它应用到之前编写的高级组件withRouter

  • 在使用了withRouter的组件中,就可以通过this.props.router.params.xxx获取到当前路由中传递的参数
  • 使用动态匹配路由时,传递给Route组件的path属性为:xxx,这里是/detail/:id
tsx
// withRouter.js
 import { useNavigate, useParams } from 'react-router-dom'
 
 export function withRouter(WrapperComponent) {
@@ -332,7 +332,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const F=
     const router = { navigate, params, query }
     return <WrapperComponent {...props} router={router} />
   }
-}

TIP

需要注意的是,这里的useSearchParams是一个Hooks的常见形态

它返回一个数组,数组的首位为值,数组的次位为改变值的方法

与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意

而对象解构恰恰相反,不必保证位置,而需要保证命名一致

路由的配置方式

至此为止,路由的配置是耦合在App.jsx中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写

  • 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库react-router-config
  • 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容

6版本为我们提供了一个API:useRoutes,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖

需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了

tsx
// router/index.js
+}

TIP

需要注意的是,这里的useSearchParams是一个Hooks的常见形态

它返回一个数组,数组的首位为值,数组的次位为改变值的方法

与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意

而对象解构恰恰相反,不必保证位置,而需要保证命名一致

路由的配置方式

至此为止,路由的配置是耦合在App.jsx中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写

  • 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库react-router-config
  • 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容

6版本为我们提供了一个API:useRoutes,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖

需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了

tsx
// router/index.js
 import { Navigate } from 'react-router-dom'
 import Home from '../views/Home'
 import HomeRanking from '../views/HomeRanking'
diff --git a/assets/note_React Router.md.c89e8196.lean.js b/assets/note_React Router.md.8d4994d8.lean.js
similarity index 100%
rename from assets/note_React Router.md.c89e8196.lean.js
rename to assets/note_React Router.md.8d4994d8.lean.js
diff --git a/assets/note_React.md.39156ee1.js b/assets/note_React.md.f9ad4dc2.js
similarity index 99%
rename from assets/note_React.md.39156ee1.js
rename to assets/note_React.md.f9ad4dc2.js
index 42a52534..c459ef5f 100644
--- a/assets/note_React.md.39156ee1.js
+++ b/assets/note_React.md.f9ad4dc2.js
@@ -2050,7 +2050,7 @@ import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.9b71bc06.js";const p=
       </div>
     )
   }
-}

Context跨组件传参

非父子组件之间的数据共享

  • props层层传递 跨组件会很不方便 对于中间那些本不需要这些props数据的组件是冗余的
  • 第三方状态库 外置于React 如Redux (实际开发中较为常用)
  • 事件总线 ...

针对跨组件传参的场景,React提供了一个API名为Context

  • Context 提供了一个在组件之间共享此类值的方式,而不是显式地通过组件树逐层传递props
  • 使用 Context 共享那些全局的数据,如主题色、用户登录状态、locales等

用Context实现跨组件传参

假设有App Profile UserCard三个嵌套组件,我们希望App中的 isDarkMode 状态能够透传到UserCard组件中

  • 全局通过 createContext 创建一个上下文
  • 根组件通过 DarkModeContext.Provider 标签与 value 传递值到上下文中
  • 需要使用到该值的子组件通过 UserCard.contextType = DarkModeContext 绑定到上下文
  • 随后即可在子组件中通过 this.context 获取到此上下文当前绑定的状态值
tsx
// context.js
+}

Context跨组件传参

非父子组件之间的数据共享

  • props层层传递 跨组件会很不方便 对于中间那些本不需要这些props数据的组件是冗余的
  • 第三方状态库 外置于React 如Redux (实际开发中较为常用)
  • 事件总线 ...

针对跨组件传参的场景,React提供了一个API名为Context

  • Context 提供了一个在组件之间共享此类值的方式,而不是显式地通过组件树逐层传递props
  • 使用 Context 共享那些全局的数据,如主题色、用户登录状态、locales等

用Context实现跨组件传参

假设有App Profile UserCard三个嵌套组件,我们希望App中的 isDarkMode 状态能够透传到UserCard组件中

  • 全局通过 createContext 创建一个上下文
  • 根组件通过 DarkModeContext.Provider 标签与 value 传递值到上下文中
  • 需要使用到该值的子组件通过 UserCard.contextType = DarkModeContext 绑定到上下文
  • 随后即可在子组件中通过 this.context 获取到此上下文当前绑定的状态值
tsx
// context.js
 import { createContext } from 'react'
 
 export const DarkModeContext = createContext()
// context.js
diff --git a/assets/note_React.md.39156ee1.lean.js b/assets/note_React.md.f9ad4dc2.lean.js
similarity index 100%
rename from assets/note_React.md.39156ee1.lean.js
rename to assets/note_React.md.f9ad4dc2.lean.js
diff --git a/assets/note_Redux.md.13a0409b.js b/assets/note_Redux.md.02de7bbe.js
similarity index 99%
rename from assets/note_Redux.md.13a0409b.js
rename to assets/note_Redux.md.02de7bbe.js
index e4c21b09..9a74ffaf 100644
--- a/assets/note_Redux.md.13a0409b.js
+++ b/assets/note_Redux.md.02de7bbe.js
@@ -1,4 +1,4 @@
-import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l="/assets/redux-usage.7df84df8.svg",o="/assets/redux-async-action.0dc40e11.svg",e="/assets/immutable.dc41f87b.gif",h=JSON.parse('{"title":"Redux","description":"","frontmatter":{},"headers":[],"relativePath":"note/Redux.md","filePath":"note/Redux.md","lastUpdated":1704524259000}'),t={name:"note/Redux.md"},c=p(`

Redux

理解JavaScript的纯函数

  • 函数式编程中有一个非常重要的概念 纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
    • 在React开发中,纯函数被多次提及:
    • React组件被要求像一个纯函数(为什么是像,因为还有类组件)
    • Redux中有一个reducer的概念,同样是要求必须是一个纯函数
  • 掌握纯函数对于理解很多框架的设计都是有帮助的

一个纯函数必然具备以下特征:

  • 确定的输入一定产生确定的输出
  • 函数的执行过程中,不能产生副作用

为什么需要Redux

  • JS需要管理的状态越来越多,越来越复杂
  • 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
  • React提供了自动更新视图的方法,但状态仍需要手动管理
  • Redux可以帮我们管理状态,提供了可预测的状态管理
  • 框架无关,体积只有2KB大小

Redux的核心理念

Redux的核心理念 Store

  • 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
  • list.push() list[0].age = 18

Redux的核心理念 Action

  • Redux要求:要修改数据,必须通过Action来修改
  • 所有数据的变化,必须通过派发(Patch)Action来更新
  • Action是一个普通的JS对象,用来描述此次更新的type与content
  • const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }

Redux的核心理念 Reducer

  • 如何将Store和Action联系在一起?
  • reducer是一个纯函数
  • 完成的工作就是:将传入的state和action结合起来,生成一个新的state
  • patch => reducer => newState => Store

Redux Demo

下例中,通过createStore创建了一个Store(已经不推荐了)

  • initialState用于在调用createStore时作为默认值传入reducer
  • 后续每次store.dispatch都会调用reducer
  • 通过reducer更新state中的数据

在React中,可以通过store.subscribe注册State变化的监听回调

  • 当state发生变化时,通过调用this.forceUpdate触发组件的更新
  • 一般情况下,我们在componentDidMount注册监听回调,在componentWillUnmount解除监听
tsx
// App.jsx
+import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l="/assets/redux-usage.7df84df8.svg",o="/assets/redux-async-action.0dc40e11.svg",e="/assets/immutable.dc41f87b.gif",h=JSON.parse('{"title":"Redux","description":"","frontmatter":{},"headers":[],"relativePath":"note/Redux.md","filePath":"note/Redux.md","lastUpdated":1704524259000}'),t={name:"note/Redux.md"},c=p(`

Redux

理解JavaScript的纯函数

  • 函数式编程中有一个非常重要的概念 纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
    • 在React开发中,纯函数被多次提及:
    • React组件被要求像一个纯函数(为什么是像,因为还有类组件)
    • Redux中有一个reducer的概念,同样是要求必须是一个纯函数
  • 掌握纯函数对于理解很多框架的设计都是有帮助的

一个纯函数必然具备以下特征:

  • 确定的输入一定产生确定的输出
  • 函数的执行过程中,不能产生副作用

为什么需要Redux

  • JS需要管理的状态越来越多,越来越复杂
  • 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
  • React提供了自动更新视图的方法,但状态仍需要手动管理
  • Redux可以帮我们管理状态,提供了可预测的状态管理
  • 框架无关,体积只有2KB大小

Redux的核心理念

Redux的核心理念 Store

  • 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
  • list.push() list[0].age = 18

Redux的核心理念 Action

  • Redux要求:要修改数据,必须通过Action来修改
  • 所有数据的变化,必须通过派发(Patch)Action来更新
  • Action是一个普通的JS对象,用来描述此次更新的type与content
  • const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }

Redux的核心理念 Reducer

  • 如何将Store和Action联系在一起?
  • reducer是一个纯函数
  • 完成的工作就是:将传入的state和action结合起来,生成一个新的state
  • patch => reducer => newState => Store

Redux Demo

下例中,通过createStore创建了一个Store(已经不推荐了)

  • initialState用于在调用createStore时作为默认值传入reducer
  • 后续每次store.dispatch都会调用reducer
  • 通过reducer更新state中的数据

在React中,可以通过store.subscribe注册State变化的监听回调

  • 当state发生变化时,通过调用this.forceUpdate触发组件的更新
  • 一般情况下,我们在componentDidMount注册监听回调,在componentWillUnmount解除监听
tsx
// App.jsx
 import React, { PureComponent } from 'react'
 import store from './store'
 
@@ -144,7 +144,7 @@ import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l=
 
 const store = createStore(reducer)
 
-export default store

redux-usage

进一步封装

可以将耦合在一起的代码拆分到不同文件中

  • reducer抽取出来reducer.js,简化store/index.js内容
  • action.type抽取为常量constants.js,使用时做导入,以保证一致性
  • action抽取出来actionFactory.js,用于外部dispatch时规范类型
tsx
// store/index.js
+export default store

redux-usage

进一步封装

可以将耦合在一起的代码拆分到不同文件中

  • reducer抽取出来reducer.js,简化store/index.js内容
  • action.type抽取为常量constants.js,使用时做导入,以保证一致性
  • action抽取出来actionFactory.js,用于外部dispatch时规范类型
tsx
// store/index.js
 import { createStore } from 'redux'
 import reducer from './reducer'
 
@@ -294,7 +294,7 @@ import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l=
       </div>
     )
   }
-}

Redux的三大原则

单一数据源

  • 整个应用程序的状态都被存储在一棵Object Tree上
  • 且这个Object Tree只存储在一个Store中
  • 但Redux并不强制限制创建多Store,不利于数据维护
  • 单一数据源有利于整个应用程序的维护、追踪、修改

State属性是只读的

  • 允许修改State的方法只有patch action,不要直接修改State
  • 确保了View或网络请求都不能修改State
  • 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题

使用纯函数来执行修改

  • 通过reducer将旧State与新State联系在一起,并且返回一个新的State
  • 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
  • 所有的reducer都应该是纯函数,不能产生任何的副作用

优化重复代码

当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码

在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态

针对重复代码的问题,可以使用之前学到的高阶组件来做优化

Redux官方提供的库react-redux,可以让我们更方便的在React中使用Redux

bash
npm i react-redux
npm i react-redux

在Profile组件中,通过高阶函数connect实现的

将store中需要的状态通过mapStoreToProps转为props,并将需要使用store中状态的组件传入调用connect返回的函数中

Profile组件中就可以从props中获取到store中的状态

tsx
// App.jsx
+}

Redux的三大原则

单一数据源

  • 整个应用程序的状态都被存储在一棵Object Tree上
  • 且这个Object Tree只存储在一个Store中
  • 但Redux并不强制限制创建多Store,不利于数据维护
  • 单一数据源有利于整个应用程序的维护、追踪、修改

State属性是只读的

  • 允许修改State的方法只有patch action,不要直接修改State
  • 确保了View或网络请求都不能修改State
  • 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题

使用纯函数来执行修改

  • 通过reducer将旧State与新State联系在一起,并且返回一个新的State
  • 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
  • 所有的reducer都应该是纯函数,不能产生任何的副作用

优化重复代码

当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码

在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态

针对重复代码的问题,可以使用之前学到的高阶组件来做优化

Redux官方提供的库react-redux,可以让我们更方便的在React中使用Redux

bash
npm i react-redux
npm i react-redux

在Profile组件中,通过高阶函数connect实现的

将store中需要的状态通过mapStoreToProps转为props,并将需要使用store中状态的组件传入调用connect返回的函数中

Profile组件中就可以从props中获取到store中的状态

tsx
// App.jsx
 import React, { PureComponent } from 'react'
 import { Provider } from 'react-redux'
 import store from './store'
@@ -434,7 +434,7 @@ import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l=
       )
     }
   }
-)

本质上是connect内部对操作进行了封装,把逻辑隐藏起来了:

  • 调用connect这个高阶函数,返回一个高阶组件
  • 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
  • 新组件的props包含了来自Store中状态/dispatch的映射

异步Action

有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据

但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数

可以通过中间件redux-thunk来对Redux做增强,让dispatch能够对函数进行派发

bash
npm i redux-thunk
npm i redux-thunk

通过applyMiddleware引入redux-thunk这个中间件:

tsx
// store/index.js
+)

本质上是connect内部对操作进行了封装,把逻辑隐藏起来了:

  • 调用connect这个高阶函数,返回一个高阶组件
  • 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
  • 新组件的props包含了来自Store中状态/dispatch的映射

异步Action

有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据

但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数

可以通过中间件redux-thunk来对Redux做增强,让dispatch能够对函数进行派发

bash
npm i redux-thunk
npm i redux-thunk

通过applyMiddleware引入redux-thunk这个中间件:

tsx
// store/index.js
 import { createStore, applyMiddleware } from 'redux'
 import thunk from 'redux-thunk'
 import reducer from './reducer'
@@ -634,7 +634,7 @@ import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l=
     counter: counterReducer(state.counter, action),
     postList: postListReducer(state.postList, action)
   }
-}

ReduxToolkit

  • ReduxToolkit重构
  • ReduxToolkit异步
  • connect高阶组件
  • 中间件的实现原理
  • React状态管理选择

认识ReduxToolkit

之前在使用createStore创建Store时会出现deprecated标识,推荐我们使用@reduxjs/toolkit包中的configureStore函数

Redux Toolkit是官方推荐编写Redux逻辑的方法

  • 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
  • 代码分拆在不同模块中,存在大量重复代码
  • Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
  • 这个包常被称为:RTK

使用ReduxToolkit重写Store

Redux Toolkit依赖于react-redux包,所以需要同时安装这二者

bash
npm i @reduxjs/toolkit react-redux
npm i @reduxjs/toolkit react-redux

Redux Toolkit的核心API主要是下述几个:

  • configureStore 包装createStore以提供简化的配置选项和良好的默认值
    • 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
    • 默认包含redux-thunk,并启用Redux DevTools Extension
  • createSlice 创建切片 片段
    • 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
  • createAsyncThunk
    • 接受一个动作类型字符串和一个返回Promise的函数
    • 并生成一个pending / fullfilled / rejected基于该承诺分派动作类型的thunk

写一个Demo:

tsx
// store/index.js
+}

ReduxToolkit

  • ReduxToolkit重构
  • ReduxToolkit异步
  • connect高阶组件
  • 中间件的实现原理
  • React状态管理选择

认识ReduxToolkit

之前在使用createStore创建Store时会出现deprecated标识,推荐我们使用@reduxjs/toolkit包中的configureStore函数

Redux Toolkit是官方推荐编写Redux逻辑的方法

  • 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
  • 代码分拆在不同模块中,存在大量重复代码
  • Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
  • 这个包常被称为:RTK

使用ReduxToolkit重写Store

Redux Toolkit依赖于react-redux包,所以需要同时安装这二者

bash
npm i @reduxjs/toolkit react-redux
npm i @reduxjs/toolkit react-redux

Redux Toolkit的核心API主要是下述几个:

  • configureStore 包装createStore以提供简化的配置选项和良好的默认值
    • 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
    • 默认包含redux-thunk,并启用Redux DevTools Extension
  • createSlice 创建切片 片段
    • 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
  • createAsyncThunk
    • 接受一个动作类型字符串和一个返回Promise的函数
    • 并生成一个pending / fullfilled / rejected基于该承诺分派动作类型的thunk

写一个Demo:

tsx
// store/index.js
 import { configureStore } from '@reduxjs/toolkit'
 import counterSlice from './features/counter'
 
@@ -778,7 +778,7 @@ import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l=
       )
     }
   }
-)

createSlice 函数参数解读

  • name 标记Slice 展示在dev-tool中
  • initialState 初始化状态
  • reducers 对象 对应之前的reducer函数
  • 返回值: 一个对象 包含所有actions

configureStore 解读

  • reducer 将slice中的reducer组成一个对象,传入此参数
  • middleware 额外的中间件
    • RTK已经为我们集成了redux-thunkredux-devtool两个中间件
  • devTools 布尔值 是否启用开发者工具

使用RTK执行异步dispatch

实际场景中都是在组件中发起网络请求,并且将状态更新到Store中

之前的开发中,我们通过redux-thunk这个中间件,让dispatch中可以进行异步操作

ReduxToolkit默认已经给我们集成了Thunk相关的功能:createAsyncThunk

下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来

tsx
// store/features/postList.js
+)

createSlice 函数参数解读

  • name 标记Slice 展示在dev-tool中
  • initialState 初始化状态
  • reducers 对象 对应之前的reducer函数
  • 返回值: 一个对象 包含所有actions

configureStore 解读

  • reducer 将slice中的reducer组成一个对象,传入此参数
  • middleware 额外的中间件
    • RTK已经为我们集成了redux-thunkredux-devtool两个中间件
  • devTools 布尔值 是否启用开发者工具

使用RTK执行异步dispatch

实际场景中都是在组件中发起网络请求,并且将状态更新到Store中

之前的开发中,我们通过redux-thunk这个中间件,让dispatch中可以进行异步操作

ReduxToolkit默认已经给我们集成了Thunk相关的功能:createAsyncThunk

下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来

tsx
// store/features/postList.js
 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
 
 export const fetchPostList = createAsyncThunk('fetch/postList', async () => {
@@ -1114,7 +1114,7 @@ import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.9b71bc06.js";const l=
         return <WrapperComponent {...this.props} {...state} {...dispatch} />
       }
     }
-}

经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染

如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render

进一步地,我们可以补充更多细节:

  • 当组件卸载时解除监听
    • store.subscribe会返回一个unsubscribe函数 用于解除监听
  • 解除与业务代码store的耦合
    • 目前的store来自业务代码 更优的做法是从context中动态获取到store
    • 应当提供一个context Provider供用户使用
    • 就像react-redux一样,使用connect前需要将App用Provider包裹并传入store

至此就基本完成了一个connect函数

tsx
// connect.js
+}

经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染

如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render

进一步地,我们可以补充更多细节:

  • 当组件卸载时解除监听
    • store.subscribe会返回一个unsubscribe函数 用于解除监听
  • 解除与业务代码store的耦合
    • 目前的store来自业务代码 更优的做法是从context中动态获取到store
    • 应当提供一个context Provider供用户使用
    • 就像react-redux一样,使用connect前需要将App用Provider包裹并传入store

至此就基本完成了一个connect函数

tsx
// connect.js
 import { PureComponent } from 'react'
 import { StoreContext } from './storeContext'
 
diff --git a/assets/note_Redux.md.13a0409b.lean.js b/assets/note_Redux.md.02de7bbe.lean.js
similarity index 100%
rename from assets/note_Redux.md.13a0409b.lean.js
rename to assets/note_Redux.md.02de7bbe.lean.js
diff --git a/hashmap.json b/hashmap.json
index f6db3f8d..48e711bb 100644
--- a/hashmap.json
+++ b/hashmap.json
@@ -1 +1 @@
-{"article_【字节跳动】前端面试题总结.md":"30e03a5f","article_一文读懂函数中this指向问题.md":"28d5e9b4","article_一文读懂伪类与伪元素.md":"68cce63a","article_【用友金融】前端面试题总结.md":"6d315a97","article_彻底搞懂对象的数据属性描述符、存储属性描述符.md":"dd182e72","index.md":"48a36636","note_ssr.md":"ee0ee312","note_react router.md":"c89e8196","project_clipboardmanager_guide_index.md":"3c3d400d","project_clipboardmanager_statement_index.md":"158b6468","article_深入理解浏览器缓存机制.md":"69e6a2c6","project_clipboardmanager_index.md":"a4052946","project_clipboardmanager_log_index.md":"30d348e4","project_jsrunner_index.md":"d62a7429","article_深入javascript数据类型.md":"477ee358","note_javascript.md":"45a113ba","note_redux.md":"13a0409b","article_深入理解浏览器运行原理.md":"a9684ca4","works_opensource.md":"db24f357","project_markdown_shortcut_index.md":"90359831","article_【快手】深入理解前端面试题.md":"d44b9955","project_markdown_index.md":"5a93c50a","article_深入vue3源码,看看vue.use后究竟发生了什么?.md":"70bae50e","article_一文读懂事件冒泡与事件捕获.md":"d1929e50","note_css.md":"fbe56ac3","article_浅析defineproperty与proxy实现的双向绑定.md":"13ed773f","article_从0实现一个年度报告.md":"175c992f","project_markdown_log_index.md":"bb95b786","note_mysql.md":"45f0f4c7","works_contribution.md":"997cb34f","article_深入理解proxy与reflect.md":"fdad28fc","project_smartwordbreak_index.md":"66a30ca5","project_smartwordbreak_statement_index.md":"33bd8d5d","article_【2023】青训营 - 前端练习题汇总解析.md":"dd1797c0","note_front-end engineering.md":"3049f99d","note_react hooks.md":"5193de15","project_jsrunner_log_index.md":"5b2edea9","self_index.md":"82f653ee","project_smartwordbreak_log_index.md":"6c6fe4e0","note_react.md":"39156ee1","note_javascriptenhanced.md":"f1655783"}
+{"index.md":"48a36636","article_浅析defineproperty与proxy实现的双向绑定.md":"13ed773f","article_一文读懂事件冒泡与事件捕获.md":"d1929e50","article_深入理解浏览器缓存机制.md":"69e6a2c6","article_一文读懂函数中this指向问题.md":"28d5e9b4","article_深入javascript数据类型.md":"477ee358","note_css.md":"fbe56ac3","article_深入理解浏览器运行原理.md":"a9684ca4","note_redux.md":"02de7bbe","project_smartwordbreak_index.md":"66a30ca5","note_javascript.md":"45a113ba","article_【快手】深入理解前端面试题.md":"d44b9955","article_【字节跳动】前端面试题总结.md":"30e03a5f","article_【用友金融】前端面试题总结.md":"6d315a97","article_深入vue3源码,看看vue.use后究竟发生了什么?.md":"70bae50e","article_一文读懂伪类与伪元素.md":"68cce63a","article_从0实现一个年度报告.md":"175c992f","article_深入理解proxy与reflect.md":"fdad28fc","project_jsrunner_log_index.md":"5b2edea9","note_react router.md":"8d4994d8","project_clipboardmanager_statement_index.md":"158b6468","project_jsrunner_index.md":"d62a7429","project_markdown_index.md":"5a93c50a","article_【2023】青训营 - 前端练习题汇总解析.md":"dd1797c0","project_clipboardmanager_log_index.md":"30d348e4","project_clipboardmanager_index.md":"a4052946","article_彻底搞懂对象的数据属性描述符、存储属性描述符.md":"dd182e72","project_smartwordbreak_log_index.md":"6c6fe4e0","project_smartwordbreak_statement_index.md":"33bd8d5d","note_mysql.md":"45f0f4c7","works_contribution.md":"997cb34f","works_opensource.md":"db24f357","project_markdown_shortcut_index.md":"90359831","project_clipboardmanager_guide_index.md":"3c3d400d","project_markdown_log_index.md":"bb95b786","note_front-end engineering.md":"75c88456","note_ssr.md":"ee0ee312","self_index.md":"82f653ee","note_react hooks.md":"7a05a70a","note_react.md":"f9ad4dc2","note_javascriptenhanced.md":"f1655783"}
diff --git a/index.html b/index.html
index ee96a55f..6b48bc20 100644
--- a/index.html
+++ b/index.html
@@ -18,7 +18,7 @@
   
   
     
Skip to content

ZiuChen

无限进步.

Infinite Progress...

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/CSS.html b/note/CSS.html index 55940e38..c0a4ad57 100644 --- a/note/CSS.html +++ b/note/CSS.html @@ -820,7 +820,7 @@ console.log(el.dataset.name) // 'ziu'
const el = document.querySelector('.item')
 console.log(el.dataset.age) // 18
 console.log(el.dataset.name) // 'ziu'

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/Front-end Engineering.html b/note/Front-end Engineering.html index c8d6b808..29f8c14c 100644 --- a/note/Front-end Engineering.html +++ b/note/Front-end Engineering.html @@ -11,7 +11,7 @@ - + @@ -115,7 +115,7 @@ })() // moduleB.js -console.log(moduleA.name) // 在其他模块中调用

CommonJS

CommonJS是一种规范,当初命名为ServerJS,旨在浏览器以外的地方使用,后为体现其广泛性,改名为CommonJS,简称CJS

规范 是用来指导 实现的

  • Node 是CommonJS在服务端的代表实现
  • Browserify 是CommonJS在浏览器中的一种实现 (正在被淘汰)
  • WebPack 打包工具具备支持CommonJS的支持和转换

所以,Node.js对CommonJS进行了支持和实现,让JavaScript在Node上运行时可以实现模块化开发

  • 每个.js文件都是一个单独的模块
  • 每个模块中都包含变量exports module.exports require
js
// env.js
+console.log(moduleA.name) // 在其他模块中调用

CommonJS

CommonJS是一种规范,当初命名为ServerJS,旨在浏览器以外的地方使用,后为体现其广泛性,改名为CommonJS,简称CJS

规范 是用来指导 实现的

  • Node 是CommonJS在服务端的代表实现
  • Browserify 是CommonJS在浏览器中的一种实现 (正在被淘汰)
  • WebPack 打包工具具备支持CommonJS的支持和转换

所以,Node.js对CommonJS进行了支持和实现,让JavaScript在Node上运行时可以实现模块化开发

  • 每个.js文件都是一个单独的模块
  • 每个模块中都包含变量exports module.exports require
js
// env.js
 exports.name = 'Ziu'
 exports.age = 18
// env.js
 exports.name = 'Ziu'
@@ -145,7 +145,7 @@
 sum(1, 2) // 3
 
 const { name, age } = require('env.js')
-console.log(name, age) // Ziu 18

exports的本质

exportsrequire在Node中的本质

  • exports是一个对象,我们可以在这个对象中添加很多属性,添加的属性则会被导出
    • 在没有向该对象添加任何属性之前,它是一个空对象
  • 当通过require导入时:const env = require('env.js')
    • env这个变量等于env.js中的exports对象
    • 本质上是envexports对象的引用赋值
    • { id: '...', exports: { ... }, loaded: true, ... }
  • 后续即使再次执行require导入模块,模块中的代码也不会重新执行(module.loaded属性)
    • 当从模块中取值时,会从已经加载的exports对象缓存上取值
js
// utils.js
+console.log(name, age) // Ziu 18

exports的本质

exportsrequire在Node中的本质

  • exports是一个对象,我们可以在这个对象中添加很多属性,添加的属性则会被导出
    • 在没有向该对象添加任何属性之前,它是一个空对象
  • 当通过require导入时:const env = require('env.js')
    • env这个变量等于env.js中的exports对象
    • 本质上是envexports对象的引用赋值
    • { id: '...', exports: { ... }, loaded: true, ... }
  • 后续即使再次执行require导入模块,模块中的代码也不会重新执行(module.loaded属性)
    • 当从模块中取值时,会从已经加载的exports对象缓存上取值
js
// utils.js
 exports.a = 0
 
 // 1s后修改a值
@@ -195,7 +195,7 @@
 module.exports = {
   name,
   run
-}

二者的区别

既然如此,为什么还要存在exports这个概念呢?

  • 在CommonJS中是没有module.exports的概念的
  • 为了实现模块的导出,Node.js使用的是Module类,每一个模块都是Module的实例,也就是module
  • 所以在Node.js中真正用于导出的并不是exports,而是module.exports
  • module对象中的exports属性是exports对象的一个引用
    • module.exports === exports === utils

如果module.exports不再引用exports对象了,修改exports对象也就没有意义了

js
// utils.js
+}

二者的区别

既然如此,为什么还要存在exports这个概念呢?

  • 在CommonJS中是没有module.exports的概念的
  • 为了实现模块的导出,Node.js使用的是Module类,每一个模块都是Module的实例,也就是module
  • 所以在Node.js中真正用于导出的并不是exports,而是module.exports
  • module对象中的exports属性是exports对象的一个引用
    • module.exports === exports === utils

如果module.exports不再引用exports对象了,修改exports对象也就没有意义了

js
// utils.js
 module.exports = {
   name: 'Ziu'
 }
@@ -209,7 +209,7 @@
 console.log(utils.age) // undefined
// index.js
 const utils = require('utils.js')
 console.log(utils.name) // Ziu
-console.log(utils.age) // undefined

当使用module.exports = { ... }后,模块中原有的exports不再被导入识别,导入的内容将变为module.exports指定的对象内容

require的本质

require是一个函数,可以帮助我们导入一个文件(模块)中导出的对象

  • 为什么可以省略掉.js后缀,直接使用require('./utils')
  • 为什么可以省略掉index.js,直接使用require('./tools')导入tools/index.js

这涉及到require在匹配路径后的查找规则:

分为三种情况:内置模块、自定义路径、包名

  • 导入Node.js内置的模块,如const path = require('path')
    • 直接返回该内置模块 并停止后续的查找
  • 根据路径导入自定义的模块,如const utils = require('./{filename}')
    • 按照路径寻找该模块./ ../ /
    • 如果指定了后缀名,则按照后缀名查找
    • 如果未指定后缀名,则:
      1. 直接查找该文件
      2. 查找{filename}.js文件
      3. 查找{filename}.json文件
      4. 查找{filename}.node文件
    • 如果按照上述方式没找到文件,则{filename}作为路径继续查找
    • 查找目录下的index文件 {filename}/index
      1. 查找{filename}/index.js文件
      2. ··· ···
    • 没找到:报错Cannot find module 'xxx'
  • 包名,如const lodash = require('lodash')
    • 到项目根目录的node_modules中查找
    • node_modules/{package_name}/index.js
    • 当前项目目录的node_modules找不到则继续向上查找,直到查找到根目录的node_modules

模块的加载过程

  • 模块在被第一次引入时,模块中的JS代码会被运行一次
    • 代码执行顺序与require的位置相关
  • 模块如果被多次引入,会被缓存,最终只加载一次
    • 这是因为每个模块对象module上都有一个属性loaded
    • loaded === false表示该模块尚未被加载
    • 第二次被require引入时会检查该属性是否为true
  • 如果有循环引用,加载顺序如何?
    • 数据结构:图结构(graph)遍历时有深度优先搜索(DFS)、广度优先搜索(BFS)两种算法
    • Node采用的是深度优先算法

CommonJS的缺点

  • 加载模块是同步加载的
    • 只有等到对应的模块加载完毕,当前模块中的内容才能被执行
    • 当然,在服务器中加载JS文件都是本地文件,加载速度非常快,不会受影响
  • 但是在浏览器中使用CommonJS
    • 需要先从服务器下载JS文件,后加载运行
    • 阻塞JS执行 阻塞页面加载
  • 在WebPack中使用CommonJS
    • CommonJS会被WebPack解析
    • 将CommonJS代码转化为bundle 浏览器可以直接运行

ESModule

  • ES6 模块采用编译时加载,使得编译时就能确定模块的依赖关系,有助于静态优化
  • CommonJS模块在运行时加载,且必须借助对象加载模块内容

exportimport用法概览

ESModule借助exportimport导入导出内容,需要注意的是导入导出的并不是对象

export定义的是当前模块导出的接口import可以导入来自其他不同模块的接口

  • export default可以设置默认导出对象
  • export { ... }可以统一导出多个内容
  • exportimport都可以使用as关键字重命名导出/导入的接口
  • import * from 'xxx' export * from 'xxx'批量导入/导出
js
// utils.js
+console.log(utils.age) // undefined

当使用module.exports = { ... }后,模块中原有的exports不再被导入识别,导入的内容将变为module.exports指定的对象内容

require的本质

require是一个函数,可以帮助我们导入一个文件(模块)中导出的对象

  • 为什么可以省略掉.js后缀,直接使用require('./utils')
  • 为什么可以省略掉index.js,直接使用require('./tools')导入tools/index.js

这涉及到require在匹配路径后的查找规则:

分为三种情况:内置模块、自定义路径、包名

  • 导入Node.js内置的模块,如const path = require('path')
    • 直接返回该内置模块 并停止后续的查找
  • 根据路径导入自定义的模块,如const utils = require('./{filename}')
    • 按照路径寻找该模块./ ../ /
    • 如果指定了后缀名,则按照后缀名查找
    • 如果未指定后缀名,则:
      1. 直接查找该文件
      2. 查找{filename}.js文件
      3. 查找{filename}.json文件
      4. 查找{filename}.node文件
    • 如果按照上述方式没找到文件,则{filename}作为路径继续查找
    • 查找目录下的index文件 {filename}/index
      1. 查找{filename}/index.js文件
      2. ··· ···
    • 没找到:报错Cannot find module 'xxx'
  • 包名,如const lodash = require('lodash')
    • 到项目根目录的node_modules中查找
    • node_modules/{package_name}/index.js
    • 当前项目目录的node_modules找不到则继续向上查找,直到查找到根目录的node_modules

模块的加载过程

  • 模块在被第一次引入时,模块中的JS代码会被运行一次
    • 代码执行顺序与require的位置相关
  • 模块如果被多次引入,会被缓存,最终只加载一次
    • 这是因为每个模块对象module上都有一个属性loaded
    • loaded === false表示该模块尚未被加载
    • 第二次被require引入时会检查该属性是否为true
  • 如果有循环引用,加载顺序如何?
    • 数据结构:图结构(graph)遍历时有深度优先搜索(DFS)、广度优先搜索(BFS)两种算法
    • Node采用的是深度优先算法

CommonJS的缺点

  • 加载模块是同步加载的
    • 只有等到对应的模块加载完毕,当前模块中的内容才能被执行
    • 当然,在服务器中加载JS文件都是本地文件,加载速度非常快,不会受影响
  • 但是在浏览器中使用CommonJS
    • 需要先从服务器下载JS文件,后加载运行
    • 阻塞JS执行 阻塞页面加载
  • 在WebPack中使用CommonJS
    • CommonJS会被WebPack解析
    • 将CommonJS代码转化为bundle 浏览器可以直接运行

ESModule

  • ES6 模块采用编译时加载,使得编译时就能确定模块的依赖关系,有助于静态优化
  • CommonJS模块在运行时加载,且必须借助对象加载模块内容

exportimport用法概览

ESModule借助exportimport导入导出内容,需要注意的是导入导出的并不是对象

export定义的是当前模块导出的接口import可以导入来自其他不同模块的接口

  • export default可以设置默认导出对象
  • export { ... }可以统一导出多个内容
  • exportimport都可以使用as关键字重命名导出/导入的接口
  • import * from 'xxx' export * from 'xxx'批量导入/导出
js
// utils.js
 export function sum(a, b) {
   return a + b
 }
@@ -249,7 +249,7 @@
 
 sum(1, 2) // 3
 sub(2, 3) // -1
-log(name, age, ENV_VARIABLE) // 'Ziu' 18 'Hello, World!'

需要注意的是,在浏览器中要使用ESModule,需要为<script>标签添加module标记:

<script src="index.js" type="module"></script>

  • 当浏览器解析到type="module"的JS代码后,会分析模块中导入的ESModule模块
  • 每导入一个ESModule模块,浏览器都会发起一个HTTP请求去加载它
  • 在本地运行时加载不同协议头的文件会遇到跨域问题,需要开启本地Web服务器

另外,exportimport必须位于模块的顶层,如果位于作用域内会报错,因为这就无法对代码进行静态分析优化了

export详解

export有两种导出方式:

  • 命名导出 export const name = 'Ziu' export { v1, v2 } export * from 'xxx'
    • 导出时需要指定名字
    • 导入时也需要知道对应的名字
  • 默认导出 export default AGE = 18
    • 在从其他位置导入时需要为此默认导出指定新的名字
    • 给用户方便:不必阅读文档就可以加载模块

值的动态绑定

  • ESModule模块通过export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
  • CommonJS模块输出的是值的缓存,不存在动态更新

我们援引之前介绍CJS时的案例,将后缀名改为mjs即可在Node中运行ESModule模块代码

初始获得的a值为0,经过1s后,在utils.mjs中修改了a的值,这时导入utils.mjs模块的其他模块可以获取到a最新的值

js
// utils.mjs
+log(name, age, ENV_VARIABLE) // 'Ziu' 18 'Hello, World!'

需要注意的是,在浏览器中要使用ESModule,需要为<script>标签添加module标记:

<script src="index.js" type="module"></script>

  • 当浏览器解析到type="module"的JS代码后,会分析模块中导入的ESModule模块
  • 每导入一个ESModule模块,浏览器都会发起一个HTTP请求去加载它
  • 在本地运行时加载不同协议头的文件会遇到跨域问题,需要开启本地Web服务器

另外,exportimport必须位于模块的顶层,如果位于作用域内会报错,因为这就无法对代码进行静态分析优化了

export详解

export有两种导出方式:

  • 命名导出 export const name = 'Ziu' export { v1, v2 } export * from 'xxx'
    • 导出时需要指定名字
    • 导入时也需要知道对应的名字
  • 默认导出 export default AGE = 18
    • 在从其他位置导入时需要为此默认导出指定新的名字
    • 给用户方便:不必阅读文档就可以加载模块

值的动态绑定

  • ESModule模块通过export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
  • CommonJS模块输出的是值的缓存,不存在动态更新

我们援引之前介绍CJS时的案例,将后缀名改为mjs即可在Node中运行ESModule模块代码

初始获得的a值为0,经过1s后,在utils.mjs中修改了a的值,这时导入utils.mjs模块的其他模块可以获取到a最新的值

js
// utils.mjs
 export let a = 0
 
 // 1s后修改a值
@@ -325,7 +325,7 @@
   import('light.js').then(() => ...)
 }

传入动态值

js
let moduleName = () => ['Home', 'History', 'User'][0]
 import(`./${moduleName()}.js`)
let moduleName = () => ['Home', 'History', 'User'][0]
-import(`./${moduleName()}.js`)

import.meta

ES2020引入了import.meta,它仅能在模块内部使用,包含一些模块自身的信息,即模块元信息

  • import.meta.url 返回当前模块的URL路径
    • 浏览器加载ESModule都是通过HTTP发起请求
      • 例如当前模块为fetchData.js,要在模块内引入一个名为data.json的数据:
      • import( new URL('data.json', import.meta.url) )
    • Node.js环境下,该值都是file://协议的链接
  • import.meta.scriptElement
    • 浏览器特有的属性
    • 返回加载模块的<script>标签,相当于document.currentScript

规范中并未规定import.meta中包含哪些属性,一般包括上面两个属性

深入理解模块加载

ESModule的解析过程

ESModule的解析过程可以分为三个阶段:

  • 构建 Construction
    • 根据地址查找JS文件,并发起HTTP请求下载,将其解析为模块记录 Module Record
  • 实例化 Instatiation
    • 对模块记录进行实例化,并为其分配内存空间
    • 解析ESModule模块的导入和导出语句,将模块指向对应的内存地址
    • 例如export const name = 'Ziu',会将变量name添加到模块环境记录中 Module Enviroment Record
  • 运行 Evaluation
    • 运行代码,计算值,并且将值填充到内存地址中
    • 将导入导出的赋给对应的变量name = 'Ziu'

ESModule解析过程

文章推荐:ES modules: A cartoon deep-dive

MJS和CJS的区别

  • CommonJS模块输出的是值的拷贝,而ESModule模块输出的是值的引用
    • CJS导出的变量,其值如果在模块内发生变化,外部导入是不会同步更新的,除非导出的是一个取值函数
    • MJS导出变量,外部模块每次访问时都会得到该变量最新的值,即使变量在模块内被修改了
  • CommonJS模块是运行时加载,而ESModule是编译时输出接口
    • CJS是通过对象实现的导入导出,它在运行时才被确定依赖关系和其值
    • MJS则是通过静态定义,在代码运行之前的静态解析阶段即可确定模块的导入导出内容
  • CommonJS模块的require()是同步加载模块,而ESModule模块的import命令是异步加载模块
    • import命令拥有一个独立的模块依赖的解析阶段

CJS中的循环加载

设想有以下两文件 a.jsb.js

js
// a.js
+import(`./${moduleName()}.js`)

import.meta

ES2020引入了import.meta,它仅能在模块内部使用,包含一些模块自身的信息,即模块元信息

  • import.meta.url 返回当前模块的URL路径
    • 浏览器加载ESModule都是通过HTTP发起请求
      • 例如当前模块为fetchData.js,要在模块内引入一个名为data.json的数据:
      • import( new URL('data.json', import.meta.url) )
    • Node.js环境下,该值都是file://协议的链接
  • import.meta.scriptElement
    • 浏览器特有的属性
    • 返回加载模块的<script>标签,相当于document.currentScript

规范中并未规定import.meta中包含哪些属性,一般包括上面两个属性

深入理解模块加载

ESModule的解析过程

ESModule的解析过程可以分为三个阶段:

  • 构建 Construction
    • 根据地址查找JS文件,并发起HTTP请求下载,将其解析为模块记录 Module Record
  • 实例化 Instatiation
    • 对模块记录进行实例化,并为其分配内存空间
    • 解析ESModule模块的导入和导出语句,将模块指向对应的内存地址
    • 例如export const name = 'Ziu',会将变量name添加到模块环境记录中 Module Enviroment Record
  • 运行 Evaluation
    • 运行代码,计算值,并且将值填充到内存地址中
    • 将导入导出的赋给对应的变量name = 'Ziu'

ESModule解析过程

文章推荐:ES modules: A cartoon deep-dive

MJS和CJS的区别

  • CommonJS模块输出的是值的拷贝,而ESModule模块输出的是值的引用
    • CJS导出的变量,其值如果在模块内发生变化,外部导入是不会同步更新的,除非导出的是一个取值函数
    • MJS导出变量,外部模块每次访问时都会得到该变量最新的值,即使变量在模块内被修改了
  • CommonJS模块是运行时加载,而ESModule是编译时输出接口
    • CJS是通过对象实现的导入导出,它在运行时才被确定依赖关系和其值
    • MJS则是通过静态定义,在代码运行之前的静态解析阶段即可确定模块的导入导出内容
  • CommonJS模块的require()是同步加载模块,而ESModule模块的import命令是异步加载模块
    • import命令拥有一个独立的模块依赖的解析阶段

CJS中的循环加载

设想有以下两文件 a.jsb.js

js
// a.js
 exports.done = false
 const b = require('./b.js')
 console.log('在 a.js 之中,b.done = %j', b.done)
@@ -359,7 +359,7 @@
 b.js 执行完毕
  a.js 之中,b.done = true
 a.js 执行完毕
- main.js 之中, a.done=true, b.done=true

总结:

  • CJS的模块导出是输出值的拷贝,而不是引用,值的变化不是动态的,而是会被缓存的
  • 循环加载时,CJS模块导出的值是当前已经执行部分代码产生的结果的值,而不是模块代码完全执行完后的最终值

MJS中的循环加载

ESModule的导入和导出与CommonJS有本质不同:

js
// a.mjs
+ main.js 之中, a.done=true, b.done=true

总结:

  • CJS的模块导出是输出值的拷贝,而不是引用,值的变化不是动态的,而是会被缓存的
  • 循环加载时,CJS模块导出的值是当前已经执行部分代码产生的结果的值,而不是模块代码完全执行完后的最终值

MJS中的循环加载

ESModule的导入和导出与CommonJS有本质不同:

js
// a.mjs
 import { bar } from './b.mjs'
 console.log('a.mjs')
 console.log(bar)
@@ -375,7 +375,7 @@
 import { foo } from './a.mjs'
 console.log('b.mjs')
 console.log(foo)
-export let bar = 'bar'

执行a.mjs后发现报错了:ReferenceError: Cannot access 'foo' before initialization,变量foo未定义

  • MJS模块在代码执行前会进行静态分析
  • 分析a.mjs的依赖关系时,发现其依赖了b.mjs
  • 于是加载b.mjs并解析它的依赖关系
  • 解析b.mjs的过程中,发现它又依赖了a.mjs
  • 这时引擎不会再去加载a.mjs 而是认为a.mjs这个模块的Module Record已经存在了
  • 继续向下执行,执行到console.log(foo)时发现foo未定义 抛出错误

要实现预期效果,可以将foobar改写为取值函数,这时执行就不会报错了:

js
// a.mjs
+export let bar = 'bar'

执行a.mjs后发现报错了:ReferenceError: Cannot access 'foo' before initialization,变量foo未定义

  • MJS模块在代码执行前会进行静态分析
  • 分析a.mjs的依赖关系时,发现其依赖了b.mjs
  • 于是加载b.mjs并解析它的依赖关系
  • 解析b.mjs的过程中,发现它又依赖了a.mjs
  • 这时引擎不会再去加载a.mjs 而是认为a.mjs这个模块的Module Record已经存在了
  • 继续向下执行,执行到console.log(foo)时发现foo未定义 抛出错误

要实现预期效果,可以将foobar改写为取值函数,这时执行就不会报错了:

js
// a.mjs
 import { bar } from './b.mjs'
 console.log('a.mjs')
 console.log(bar())
@@ -440,7 +440,7 @@
 const p1 = 'abc/cba'
 const p2 = '../why/kobe/james.txt'
 console.log(path.join(p1, p2)) // \\abc\\why\\kobe\\james.txt
  • path.resolve 将多个路径片段解析、拼接为一个绝对路径
    • 从右往左处理传入的路径序列,每个路径依次被解析,直到构造完成一个绝对路径
    • 如果在处理完所有给定path的段之后,还没有生成绝对路径,则使用当前工作目录
    • 生成的路径被规范化并删除尾部斜杠,零长度path段被忽略
    • 如果没有传递path段,则path.resolve()将返回当前工作目录的绝对路径

初识Webpack

path模块在webpack的使用中有较大作用,所以预先介绍了一下,下面正式进入Webpack的学习

  • Webpack能够帮助我们进行模块化开发
  • 高级特性提升开发效率和代码安全性、可维护性,如 ES6+ TypeScript SASS Less 等都需要构建工具
  • 实时文件监听(开发热更新)、代码压缩合并等

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/JavaScript.html b/note/JavaScript.html index 8bd650fa..a65c4031 100644 --- a/note/JavaScript.html +++ b/note/JavaScript.html @@ -18,7 +18,7 @@
Skip to content

JavaScript 基础

JavaScript 组成

  • ECMAScript 定义语言规范
  • DOM 用于操作文档的API
  • BOM 用于操作浏览器的API

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/JavaScriptEnhanced.html b/note/JavaScriptEnhanced.html index c8c5f5dc..8e4cbed7 100644 --- a/note/JavaScriptEnhanced.html +++ b/note/JavaScriptEnhanced.html @@ -3726,7 +3726,7 @@ setTimeout(() => { eventBus.off('navClick', callBack) // 移除某个事件的某次回调 }, 5000)

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/MySQL.html b/note/MySQL.html index 8b900ed4..24283fb4 100644 --- a/note/MySQL.html +++ b/note/MySQL.html @@ -130,7 +130,7 @@ alter table emp modify nickname username varchar(30) alter table emp drop username alter table emp rename to employee

MySQL数据类型

数值类型

在定义字段时,通过关键字UNSIGNED确定其无符号 / 有符号

类型大小范围(有符号)范围(无符号)用途
TINYINT1 Bytes(-128,127)(0,255)小整数值
SMALLINT2 Bytes(-32 768,32 767)(0,65 535)大整数值
MEDIUMINT3 Bytes(-8 388 608,8 388 607)(0,16 777 215)大整数值
INT或INTEGER4 Bytes(-2 147 483 648,2 147 483 647)(0,4 294 967 295)大整数值
BIGINT8 Bytes(-9,223,372,036,854,775,808,9 223 372 036 854 775 807)(0,18 446 744 073 709 551 615)极大整数值
FLOAT4 Bytes(-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38)0,(1.175 494 351 E-38,3.402 823 466 E+38)单精度 浮点数值
DOUBLE8 Bytes(-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)双精度 浮点数值
DECIMAL对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2依赖于M和D的值依赖于M和D的值小数值

字符串类型

类型大小用途
CHAR0-255 bytes定长字符串
VARCHAR0-65535 bytes变长字符串
TINYBLOB0-255 bytes不超过 255 个字符的二进制字符串
TINYTEXT0-255 bytes短文本字符串
BLOB0-65 535 bytes二进制形式的长文本数据
TEXT0-65 535 bytes长文本数据
MEDIUMBLOB0-16 777 215 bytes二进制形式的中等长度文本数据
MEDIUMTEXT0-16 777 215 bytes中等长度文本数据
LONGBLOB0-4 294 967 295 bytes二进制形式的极大文本数据
LONGTEXT0-4 294 967 295 bytes极大文本数据

注意:char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。

CHAR 和 VARCHAR 类型类似,但它们保存和检索的方式不同。它们的最大长度和是否尾部空格被保留等方面也不同。在存储或检索过程中不进行大小写转换。CHAR性能更优

BINARY 和 VARBINARY 类似于 CHAR 和 VARCHAR,不同的是它们包含二进制字符串而不要非二进制字符串。也就是说,它们包含字节字符串而不是字符字符串。这说明它们没有字符集,并且排序和比较基于列值字节的数值值。

BLOB 是一个二进制大对象,可以容纳可变数量的数据。有 4 种 BLOB 类型:TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB。它们区别在于可容纳存储范围不同。

有 4 种 TEXT 类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。对应的这 4 种 BLOB 类型,可存储的最大长度不同,可根据实际情况选择。

日期时间类型

类型大小 ( bytes)范围格式用途
DATE31000-01-01/9999-12-31YYYY-MM-DD日期值
TIME3'-838:59:59'/'838:59:59'HH:MM:SS时间值或持续时间
YEAR11901/2155YYYY年份值
DATETIME81000-01-01 00:00:00/9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS混合日期和时间值
TIMESTAMP41970-01-01 00:00:00/2038结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07YYYYMMDD HHMMSS混合日期和时间值,时间戳

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/React Hooks.html b/note/React Hooks.html index e5222162..e94cc942 100644 --- a/note/React Hooks.html +++ b/note/React Hooks.html @@ -11,13 +11,13 @@ - + -
Skip to content

React Hooks

  • 认识和体验Hooks
  • State/Effect
  • Context/Reducer
  • Callback/Memo
  • Ref/LayoutEffect
  • 自定义Hooks使用

认识React Hooks

Hooks 是 React16.8 推出的新特性

在没有Hooks时,类组件能够完成的大部分工作,函数式组件都无法胜任:

  • 类组件可以定义并保存组件内部状态,并在状态发生改变时触发视图重新渲染
    • 函数式组件不行,每次调用函数其中的变量都会被重新初始化,重新渲染时整个函数都重新执行
  • 类组件可以在其内部的生命周期回调中添加副作用
    • 例如componentDidMount在类组件生命周期只会执行一次
    • 函数式组件没有生命周期,如果在函数体内发起网络请求,那每次重新渲染都会发起请求

类组件存在的问题:

  • 复杂组件变得难以理解
    • 业务代码相互耦合,类组件变得复杂
    • 逻辑强耦合在一起难以拆分,强行拆分会导致过度设计,进一步增加代码复杂度
  • class关键字的理解
    • 初学React时class关键字理解存在困难
    • 处理this的指向问题需要花费额外的心智负担
  • 组件状态复用
    • 要复用组件需要借助高阶组件
    • redux 中的 connect 或者 react-router 中的 withRouter,高阶组件的目的就是为了状态复
    • 或通过Provider、Consumer来共享状态,但是Comsumer嵌套问题较严重

Hooks带来的优势:

  • 在不编写class的情况下使用state和其他React特性(如生命周期)
  • Hooks 允许我们在函数式组件中使用状态,并在状态发生改变时让视图重新渲染
  • 同时,我们还可以在函数式组件中使用生命周期回调
  • 更多的优点 ...

计数器案例对比

分别使用Hooks和类组件编写一个计数器:

tsx
// CounterClass.jsx
+    
Skip to content

React Hooks

  • 认识和体验Hooks
  • State/Effect
  • Context/Reducer
  • Callback/Memo
  • Ref/LayoutEffect
  • 自定义Hooks使用

认识React Hooks

Hooks 是 React16.8 推出的新特性

在没有Hooks时,类组件能够完成的大部分工作,函数式组件都无法胜任:

  • 类组件可以定义并保存组件内部状态,并在状态发生改变时触发视图重新渲染
    • 函数式组件不行,每次调用函数其中的变量都会被重新初始化,重新渲染时整个函数都重新执行
  • 类组件可以在其内部的生命周期回调中添加副作用
    • 例如componentDidMount在类组件生命周期只会执行一次
    • 函数式组件没有生命周期,如果在函数体内发起网络请求,那每次重新渲染都会发起请求

类组件存在的问题:

  • 复杂组件变得难以理解
    • 业务代码相互耦合,类组件变得复杂
    • 逻辑强耦合在一起难以拆分,强行拆分会导致过度设计,进一步增加代码复杂度
  • class关键字的理解
    • 初学React时class关键字理解存在困难
    • 处理this的指向问题需要花费额外的心智负担
  • 组件状态复用
    • 要复用组件需要借助高阶组件
    • redux 中的 connect 或者 react-router 中的 withRouter,高阶组件的目的就是为了状态复
    • 或通过Provider、Consumer来共享状态,但是Comsumer嵌套问题较严重

Hooks带来的优势:

  • 在不编写class的情况下使用state和其他React特性(如生命周期)
  • Hooks 允许我们在函数式组件中使用状态,并在状态发生改变时让视图重新渲染
  • 同时,我们还可以在函数式组件中使用生命周期回调
  • 更多的优点 ...

计数器案例对比

分别使用Hooks和类组件编写一个计数器:

tsx
// CounterClass.jsx
 import React, { PureComponent } from 'react'
 
 export default class CounterClass extends PureComponent {
@@ -211,7 +211,7 @@
       console.log('Effect Cleaned Up')
     }
   }, [])
-...

useContext

在之前的开发中,要在组件中使用共享的Context有两种方式

  • 类组件可以通过ClassName.contextType = SomeContext绑定上下文
  • 在类的函数中通过this.context.xxx获取上下文中共享的状态
  • 同时有多个Context时/函数式组件中,通过SomeContext.Consumer的方式共享上下文状态

其中最大的问题就是:多个Context在同时使用时会引入大量的嵌套,而useContext可以帮我们解决这个问题

通过useContext可以直接获取到某个上下文中共享的状态变量

tsx
// Profile.jsx
+...

useContext

在之前的开发中,要在组件中使用共享的Context有两种方式

  • 类组件可以通过ClassName.contextType = SomeContext绑定上下文
  • 在类的函数中通过this.context.xxx获取上下文中共享的状态
  • 同时有多个Context时/函数式组件中,通过SomeContext.Consumer的方式共享上下文状态

其中最大的问题就是:多个Context在同时使用时会引入大量的嵌套,而useContext可以帮我们解决这个问题

通过useContext可以直接获取到某个上下文中共享的状态变量

tsx
// Profile.jsx
 import React, { useContext } from 'react'
 import { UserContext, ThemeContext } from '../context'
 
@@ -291,7 +291,7 @@
       </UserContext.Provider>
     </div>
   )
-}

当组件上层最近的SomeContext.Provider提供的值发生更新时,useContext会使用上下文中最新的数据触发组件的重新渲染

useReducer

useReducer并不是Redux的替代品

  • useReduceruseState在某些场景下的替代方案
  • 如果state需要处理的数据较为复杂,我们可以通过useReducer对其进行拆分
  • 或者需要修改的state需要依赖之前的state时,也可以使用useReducer

下面举一个例子:用户信息包含多个复杂的字段,当用户执行操作后需要同时对多个字段进行修改

我们分别用useStateuseReducer来实现:

tsx
// UserInfoWithReducer.jsx
+}

当组件上层最近的SomeContext.Provider提供的值发生更新时,useContext会使用上下文中最新的数据触发组件的重新渲染

useReducer

useReducer并不是Redux的替代品

  • useReduceruseState在某些场景下的替代方案
  • 如果state需要处理的数据较为复杂,我们可以通过useReducer对其进行拆分
  • 或者需要修改的state需要依赖之前的state时,也可以使用useReducer

下面举一个例子:用户信息包含多个复杂的字段,当用户执行操作后需要同时对多个字段进行修改

我们分别用useStateuseReducer来实现:

tsx
// UserInfoWithReducer.jsx
 import React, { useReducer } from 'react'
 
 function reducer(state, action) {
@@ -577,7 +577,7 @@
 const bar1 = foo()
 
 bar1() // 1
-bar1() // 1

不论调用了多少次bar1,其内部取到的值都始终是最初的那个count,自然值也不会发生变化

所以,我们需要显式地为useCallback指定依赖state,这样才能准确地使用最新的状态定义新的函数

真实的useCallback使用场景

经过之前的说明,目前useCallback看起来并没有实际的用途,它没有减少函数的定义次数,甚至在不合理使用时还会出现闭包陷阱,而带来的唯一好处就是:当状态没有发生改变时,保证函数指向确定且唯一

下面我们举一个实际场景来说明useCallback的用途:

一个嵌套计数器的例子,外部计数器可以展示/改变计数器的值,子组件也可以通过调用props传递来的函数来改变计数器的值,同时外部计数器还包含了其他的状态在动态被修改

tsx
// InnerCounter.jsx
+bar1() // 1

不论调用了多少次bar1,其内部取到的值都始终是最初的那个count,自然值也不会发生变化

所以,我们需要显式地为useCallback指定依赖state,这样才能准确地使用最新的状态定义新的函数

真实的useCallback使用场景

经过之前的说明,目前useCallback看起来并没有实际的用途,它没有减少函数的定义次数,甚至在不合理使用时还会出现闭包陷阱,而带来的唯一好处就是:当状态没有发生改变时,保证函数指向确定且唯一

下面我们举一个实际场景来说明useCallback的用途:

一个嵌套计数器的例子,外部计数器可以展示/改变计数器的值,子组件也可以通过调用props传递来的函数来改变计数器的值,同时外部计数器还包含了其他的状态在动态被修改

tsx
// InnerCounter.jsx
 import React, { memo } from 'react'
 
 export default memo(function InnerCounter(props) {
@@ -1073,7 +1073,7 @@
 const theme = useContext(ThemeContext)
 
 console.log(user.name, theme.primaryColor) // ...
-...

我们可以使用自定义Hook来简化这一操作,将所有的Context统一导入并转化为对象,直接在组件中使用

对之前的Profile组件使用Hook进行增强:

ts
// useSharedContext.js
+...

我们可以使用自定义Hook来简化这一操作,将所有的Context统一导入并转化为对象,直接在组件中使用

对之前的Profile组件使用Hook进行增强:

ts
// useSharedContext.js
 import { useContext } from 'react'
 import { UserContext, ThemeContext } from '../context'
 
@@ -1121,7 +1121,7 @@
       <div>theme: {context.theme.theme}</div>
     </div>
   )
-}

案例二:获取滚动位置

tsx
// useScrollPosition.js
+}

案例二:获取滚动位置

tsx
// useScrollPosition.js
 import { useState, useEffect } from 'react'
 
 export function useScrollPosition(options = {}) {
@@ -1195,7 +1195,7 @@
   )
 })
 
-export default GiantList

案例三:封装localStorage

在使用状态变量的时候,为状态变量值的更新添加副作用,将变量名作为key,值更新到localStorage中

tsx
// useLocalStorage.js
+export default GiantList

案例三:封装localStorage

在使用状态变量的时候,为状态变量值的更新添加副作用,将变量名作为key,值更新到localStorage中

tsx
// useLocalStorage.js
 import { useState, useEffect } from 'react'
 
 export function useLocalStorage(key) {
@@ -1265,7 +1265,7 @@
   )
 })
 
-export default UserInfoStorage

这里的useState还展示了一个额外的用法,向useState传递一个函数,函数的返回值会作为状态变量的初始值

Redux Hooks

之前的Redux开发中,为了让组件和Redux建立联系,我们使用了react-redux中的connect

  • 必须与高阶函数结合,必须使用返回的高阶组件
  • 必须编写mapStateToProps mapDispatchToProps,将上下文状态映射到props中

从Redux7.1开始,支持Hook写法,不再需要编写connect以及映射函数了

useSelector

将state映射到组件中

  • 参数一:将state映射到需要的数据中
  • 参数二:可以进行比较,来决定组件是否重新渲染

默认情况下useSelector监听整个state的变化,只要state中有状态变量发生变化,无论当前组件是否使用到了这个状态变量,都会触发组件的重新渲染。这就需要我们显式地为其指定重新渲染的判断条件

useSelector会比较我们返回的两个对象是否相等:

ts
const refEquality = (a, b) => (a === b);
const refEquality = (a, b) => (a === b);

只有两个对象全等时,才可以不触发重新渲染

useDispatch

直接获取dispatch函数,之后在组件中直接调用即可

另外,我们还可以通过useStore来获取当前的store对象

拿之前Redux的计数器举例,使用useSelectoruseDispatch进行重构:

tsx
// [Now] Counter.jsx
+export default UserInfoStorage

这里的useState还展示了一个额外的用法,向useState传递一个函数,函数的返回值会作为状态变量的初始值

Redux Hooks

之前的Redux开发中,为了让组件和Redux建立联系,我们使用了react-redux中的connect

  • 必须与高阶函数结合,必须使用返回的高阶组件
  • 必须编写mapStateToProps mapDispatchToProps,将上下文状态映射到props中

从Redux7.1开始,支持Hook写法,不再需要编写connect以及映射函数了

useSelector

将state映射到组件中

  • 参数一:将state映射到需要的数据中
  • 参数二:可以进行比较,来决定组件是否重新渲染

默认情况下useSelector监听整个state的变化,只要state中有状态变量发生变化,无论当前组件是否使用到了这个状态变量,都会触发组件的重新渲染。这就需要我们显式地为其指定重新渲染的判断条件

useSelector会比较我们返回的两个对象是否相等:

ts
const refEquality = (a, b) => (a === b);
const refEquality = (a, b) => (a === b);

只有两个对象全等时,才可以不触发重新渲染

useDispatch

直接获取dispatch函数,之后在组件中直接调用即可

另外,我们还可以通过useStore来获取当前的store对象

拿之前Redux的计数器举例,使用useSelectoruseDispatch进行重构:

tsx
// [Now] Counter.jsx
 import { memo } from 'react'
 import { useSelector, useDispatch, shallowEqual } from 'react-redux'
 import { addCount, subCount } from '../store/features/counter'
@@ -1540,7 +1540,7 @@
 })
 
 export default GiantList

不需要使用额外的API来显式指定哪些操作作为过渡任务延迟执行更新,只需要将原有的状态变量使用useDeferredValue包裹后,使用返回的值进行展示。

后续,对原来的状态变量进行的任何操作,当更新反映到真实DOM时都会被延迟执行

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/React Router.html b/note/React Router.html index 9b8d7f68..ff4f2cae 100644 --- a/note/React Router.html +++ b/note/React Router.html @@ -11,7 +11,7 @@ - + @@ -97,7 +97,7 @@ </div> ) } -}

另外,这里还有一个小技巧,在最末一个路由指定一个path为*的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面

嵌套路由

嵌套路由可以通过在Route组件内部嵌套新的Route组件来实现

再通过Outlet组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view)

我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand

同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件

tsx
// App.jsx
+}

另外,这里还有一个小技巧,在最末一个路由指定一个path为*的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面

嵌套路由

嵌套路由可以通过在Route组件内部嵌套新的Route组件来实现

再通过Outlet组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view)

我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand

同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件

tsx
// App.jsx
 import React, { PureComponent } from 'react'
 import { Routes, Route, Navigate, NavLink } from 'react-router-dom'
 import Home from './views/Home'
@@ -167,7 +167,7 @@
       </div>
     )
   }
-}

编程式导航(高阶组件)

之前使用的ReactRouter提供的路由跳转的组件,无论是Link还是NavLink可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转

ReactRouter提供了编程式导航的API:useNavigate

自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。

我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件

封装一个高阶组件withRouter,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法:

tsx
// withRouter.js
+}

编程式导航(高阶组件)

之前使用的ReactRouter提供的路由跳转的组件,无论是Link还是NavLink可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转

ReactRouter提供了编程式导航的API:useNavigate

自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。

我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件

封装一个高阶组件withRouter,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法:

tsx
// withRouter.js
 import { useNavigate } from 'react-router-dom'
 
 export function withRouter(WrapperComponent) {
@@ -221,7 +221,7 @@
       )
     }
   }
-)

我们使用withRouter高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转

这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目

动态路由(路由传参)

传递参数由两种方式:

  • 动态路由的方式
  • 查询字符串传递参数

动态路由是指:路由中的路径信息并不会固定

  • 比如匹配规则为/detail/:id时,/detail/123 detail/888都会被匹配上,并将123/888作为id参数传递
  • 其中/detail/:id这个匹配规则被称为动态路由

动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页

我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数

同样地,react-router-dom为我们提供了从路由获取参数的API:useParams,它是一个Hooks,我们将它应用到之前编写的高级组件withRouter

  • 在使用了withRouter的组件中,就可以通过this.props.router.params.xxx获取到当前路由中传递的参数
  • 使用动态匹配路由时,传递给Route组件的path属性为:xxx,这里是/detail/:id
tsx
// withRouter.js
+)

我们使用withRouter高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转

这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目

动态路由(路由传参)

传递参数由两种方式:

  • 动态路由的方式
  • 查询字符串传递参数

动态路由是指:路由中的路径信息并不会固定

  • 比如匹配规则为/detail/:id时,/detail/123 detail/888都会被匹配上,并将123/888作为id参数传递
  • 其中/detail/:id这个匹配规则被称为动态路由

动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页

我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数

同样地,react-router-dom为我们提供了从路由获取参数的API:useParams,它是一个Hooks,我们将它应用到之前编写的高级组件withRouter

  • 在使用了withRouter的组件中,就可以通过this.props.router.params.xxx获取到当前路由中传递的参数
  • 使用动态匹配路由时,传递给Route组件的path属性为:xxx,这里是/detail/:id
tsx
// withRouter.js
 import { useNavigate, useParams } from 'react-router-dom'
 
 export function withRouter(WrapperComponent) {
@@ -351,7 +351,7 @@
     const router = { navigate, params, query }
     return <WrapperComponent {...props} router={router} />
   }
-}

TIP

需要注意的是,这里的useSearchParams是一个Hooks的常见形态

它返回一个数组,数组的首位为值,数组的次位为改变值的方法

与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意

而对象解构恰恰相反,不必保证位置,而需要保证命名一致

路由的配置方式

至此为止,路由的配置是耦合在App.jsx中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写

  • 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库react-router-config
  • 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容

6版本为我们提供了一个API:useRoutes,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖

需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了

tsx
// router/index.js
+}

TIP

需要注意的是,这里的useSearchParams是一个Hooks的常见形态

它返回一个数组,数组的首位为值,数组的次位为改变值的方法

与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意

而对象解构恰恰相反,不必保证位置,而需要保证命名一致

路由的配置方式

至此为止,路由的配置是耦合在App.jsx中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写

  • 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库react-router-config
  • 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容

6版本为我们提供了一个API:useRoutes,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖

需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了

tsx
// router/index.js
 import { Navigate } from 'react-router-dom'
 import Home from '../views/Home'
 import HomeRanking from '../views/HomeRanking'
@@ -560,7 +560,7 @@
     </HashRouter>
   </StrictMode>
 )

当根组件内部有组件处于异步加载状态时,都会在页面上展示Loading...而不是崩溃掉

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/React.html b/note/React.html index ca385a2e..99082527 100644 --- a/note/React.html +++ b/note/React.html @@ -11,7 +11,7 @@ - + @@ -2069,7 +2069,7 @@ </div> ) } -}

Context跨组件传参

非父子组件之间的数据共享

  • props层层传递 跨组件会很不方便 对于中间那些本不需要这些props数据的组件是冗余的
  • 第三方状态库 外置于React 如Redux (实际开发中较为常用)
  • 事件总线 ...

针对跨组件传参的场景,React提供了一个API名为Context

  • Context 提供了一个在组件之间共享此类值的方式,而不是显式地通过组件树逐层传递props
  • 使用 Context 共享那些全局的数据,如主题色、用户登录状态、locales等

用Context实现跨组件传参

假设有App Profile UserCard三个嵌套组件,我们希望App中的 isDarkMode 状态能够透传到UserCard组件中

  • 全局通过 createContext 创建一个上下文
  • 根组件通过 DarkModeContext.Provider 标签与 value 传递值到上下文中
  • 需要使用到该值的子组件通过 UserCard.contextType = DarkModeContext 绑定到上下文
  • 随后即可在子组件中通过 this.context 获取到此上下文当前绑定的状态值
tsx
// context.js
+}

Context跨组件传参

非父子组件之间的数据共享

  • props层层传递 跨组件会很不方便 对于中间那些本不需要这些props数据的组件是冗余的
  • 第三方状态库 外置于React 如Redux (实际开发中较为常用)
  • 事件总线 ...

针对跨组件传参的场景,React提供了一个API名为Context

  • Context 提供了一个在组件之间共享此类值的方式,而不是显式地通过组件树逐层传递props
  • 使用 Context 共享那些全局的数据,如主题色、用户登录状态、locales等

用Context实现跨组件传参

假设有App Profile UserCard三个嵌套组件,我们希望App中的 isDarkMode 状态能够透传到UserCard组件中

  • 全局通过 createContext 创建一个上下文
  • 根组件通过 DarkModeContext.Provider 标签与 value 传递值到上下文中
  • 需要使用到该值的子组件通过 UserCard.contextType = DarkModeContext 绑定到上下文
  • 随后即可在子组件中通过 this.context 获取到此上下文当前绑定的状态值
tsx
// context.js
 import { createContext } from 'react'
 
 export const DarkModeContext = createContext()
// context.js
@@ -4340,7 +4340,7 @@
 classNames(...['foo', 'bar'])
classNames('foo', 'bar')
 classNames('foo', { bar: true })
 classNames(...['foo', 'bar'])

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/Redux.html b/note/Redux.html index 539077e0..edc1ac3d 100644 --- a/note/Redux.html +++ b/note/Redux.html @@ -11,13 +11,13 @@ - + -
Skip to content

Redux

理解JavaScript的纯函数

  • 函数式编程中有一个非常重要的概念 纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
    • 在React开发中,纯函数被多次提及:
    • React组件被要求像一个纯函数(为什么是像,因为还有类组件)
    • Redux中有一个reducer的概念,同样是要求必须是一个纯函数
  • 掌握纯函数对于理解很多框架的设计都是有帮助的

一个纯函数必然具备以下特征:

  • 确定的输入一定产生确定的输出
  • 函数的执行过程中,不能产生副作用

为什么需要Redux

  • JS需要管理的状态越来越多,越来越复杂
  • 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
  • React提供了自动更新视图的方法,但状态仍需要手动管理
  • Redux可以帮我们管理状态,提供了可预测的状态管理
  • 框架无关,体积只有2KB大小

Redux的核心理念

Redux的核心理念 Store

  • 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
  • list.push() list[0].age = 18

Redux的核心理念 Action

  • Redux要求:要修改数据,必须通过Action来修改
  • 所有数据的变化,必须通过派发(Patch)Action来更新
  • Action是一个普通的JS对象,用来描述此次更新的type与content
  • const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }

Redux的核心理念 Reducer

  • 如何将Store和Action联系在一起?
  • reducer是一个纯函数
  • 完成的工作就是:将传入的state和action结合起来,生成一个新的state
  • patch => reducer => newState => Store

Redux Demo

下例中,通过createStore创建了一个Store(已经不推荐了)

  • initialState用于在调用createStore时作为默认值传入reducer
  • 后续每次store.dispatch都会调用reducer
  • 通过reducer更新state中的数据

在React中,可以通过store.subscribe注册State变化的监听回调

  • 当state发生变化时,通过调用this.forceUpdate触发组件的更新
  • 一般情况下,我们在componentDidMount注册监听回调,在componentWillUnmount解除监听
tsx
// App.jsx
+    
Skip to content

Redux

理解JavaScript的纯函数

  • 函数式编程中有一个非常重要的概念 纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
    • 在React开发中,纯函数被多次提及:
    • React组件被要求像一个纯函数(为什么是像,因为还有类组件)
    • Redux中有一个reducer的概念,同样是要求必须是一个纯函数
  • 掌握纯函数对于理解很多框架的设计都是有帮助的

一个纯函数必然具备以下特征:

  • 确定的输入一定产生确定的输出
  • 函数的执行过程中,不能产生副作用

为什么需要Redux

  • JS需要管理的状态越来越多,越来越复杂
  • 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
  • React提供了自动更新视图的方法,但状态仍需要手动管理
  • Redux可以帮我们管理状态,提供了可预测的状态管理
  • 框架无关,体积只有2KB大小

Redux的核心理念

Redux的核心理念 Store

  • 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
  • list.push() list[0].age = 18

Redux的核心理念 Action

  • Redux要求:要修改数据,必须通过Action来修改
  • 所有数据的变化,必须通过派发(Patch)Action来更新
  • Action是一个普通的JS对象,用来描述此次更新的type与content
  • const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }

Redux的核心理念 Reducer

  • 如何将Store和Action联系在一起?
  • reducer是一个纯函数
  • 完成的工作就是:将传入的state和action结合起来,生成一个新的state
  • patch => reducer => newState => Store

Redux Demo

下例中,通过createStore创建了一个Store(已经不推荐了)

  • initialState用于在调用createStore时作为默认值传入reducer
  • 后续每次store.dispatch都会调用reducer
  • 通过reducer更新state中的数据

在React中,可以通过store.subscribe注册State变化的监听回调

  • 当state发生变化时,通过调用this.forceUpdate触发组件的更新
  • 一般情况下,我们在componentDidMount注册监听回调,在componentWillUnmount解除监听
tsx
// App.jsx
 import React, { PureComponent } from 'react'
 import store from './store'
 
@@ -163,7 +163,7 @@
 
 const store = createStore(reducer)
 
-export default store

redux-usage

进一步封装

可以将耦合在一起的代码拆分到不同文件中

  • reducer抽取出来reducer.js,简化store/index.js内容
  • action.type抽取为常量constants.js,使用时做导入,以保证一致性
  • action抽取出来actionFactory.js,用于外部dispatch时规范类型
tsx
// store/index.js
+export default store

redux-usage

进一步封装

可以将耦合在一起的代码拆分到不同文件中

  • reducer抽取出来reducer.js,简化store/index.js内容
  • action.type抽取为常量constants.js,使用时做导入,以保证一致性
  • action抽取出来actionFactory.js,用于外部dispatch时规范类型
tsx
// store/index.js
 import { createStore } from 'redux'
 import reducer from './reducer'
 
@@ -313,7 +313,7 @@
       </div>
     )
   }
-}

Redux的三大原则

单一数据源

  • 整个应用程序的状态都被存储在一棵Object Tree上
  • 且这个Object Tree只存储在一个Store中
  • 但Redux并不强制限制创建多Store,不利于数据维护
  • 单一数据源有利于整个应用程序的维护、追踪、修改

State属性是只读的

  • 允许修改State的方法只有patch action,不要直接修改State
  • 确保了View或网络请求都不能修改State
  • 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题

使用纯函数来执行修改

  • 通过reducer将旧State与新State联系在一起,并且返回一个新的State
  • 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
  • 所有的reducer都应该是纯函数,不能产生任何的副作用

优化重复代码

当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码

在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态

针对重复代码的问题,可以使用之前学到的高阶组件来做优化

Redux官方提供的库react-redux,可以让我们更方便的在React中使用Redux

bash
npm i react-redux
npm i react-redux

在Profile组件中,通过高阶函数connect实现的

将store中需要的状态通过mapStoreToProps转为props,并将需要使用store中状态的组件传入调用connect返回的函数中

Profile组件中就可以从props中获取到store中的状态

tsx
// App.jsx
+}

Redux的三大原则

单一数据源

  • 整个应用程序的状态都被存储在一棵Object Tree上
  • 且这个Object Tree只存储在一个Store中
  • 但Redux并不强制限制创建多Store,不利于数据维护
  • 单一数据源有利于整个应用程序的维护、追踪、修改

State属性是只读的

  • 允许修改State的方法只有patch action,不要直接修改State
  • 确保了View或网络请求都不能修改State
  • 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题

使用纯函数来执行修改

  • 通过reducer将旧State与新State联系在一起,并且返回一个新的State
  • 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
  • 所有的reducer都应该是纯函数,不能产生任何的副作用

优化重复代码

当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码

在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态

针对重复代码的问题,可以使用之前学到的高阶组件来做优化

Redux官方提供的库react-redux,可以让我们更方便的在React中使用Redux

bash
npm i react-redux
npm i react-redux

在Profile组件中,通过高阶函数connect实现的

将store中需要的状态通过mapStoreToProps转为props,并将需要使用store中状态的组件传入调用connect返回的函数中

Profile组件中就可以从props中获取到store中的状态

tsx
// App.jsx
 import React, { PureComponent } from 'react'
 import { Provider } from 'react-redux'
 import store from './store'
@@ -453,7 +453,7 @@
       )
     }
   }
-)

本质上是connect内部对操作进行了封装,把逻辑隐藏起来了:

  • 调用connect这个高阶函数,返回一个高阶组件
  • 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
  • 新组件的props包含了来自Store中状态/dispatch的映射

异步Action

有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据

但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数

可以通过中间件redux-thunk来对Redux做增强,让dispatch能够对函数进行派发

bash
npm i redux-thunk
npm i redux-thunk

通过applyMiddleware引入redux-thunk这个中间件:

tsx
// store/index.js
+)

本质上是connect内部对操作进行了封装,把逻辑隐藏起来了:

  • 调用connect这个高阶函数,返回一个高阶组件
  • 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
  • 新组件的props包含了来自Store中状态/dispatch的映射

异步Action

有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据

但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数

可以通过中间件redux-thunk来对Redux做增强,让dispatch能够对函数进行派发

bash
npm i redux-thunk
npm i redux-thunk

通过applyMiddleware引入redux-thunk这个中间件:

tsx
// store/index.js
 import { createStore, applyMiddleware } from 'redux'
 import thunk from 'redux-thunk'
 import reducer from './reducer'
@@ -653,7 +653,7 @@
     counter: counterReducer(state.counter, action),
     postList: postListReducer(state.postList, action)
   }
-}

ReduxToolkit

  • ReduxToolkit重构
  • ReduxToolkit异步
  • connect高阶组件
  • 中间件的实现原理
  • React状态管理选择

认识ReduxToolkit

之前在使用createStore创建Store时会出现deprecated标识,推荐我们使用@reduxjs/toolkit包中的configureStore函数

Redux Toolkit是官方推荐编写Redux逻辑的方法

  • 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
  • 代码分拆在不同模块中,存在大量重复代码
  • Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
  • 这个包常被称为:RTK

使用ReduxToolkit重写Store

Redux Toolkit依赖于react-redux包,所以需要同时安装这二者

bash
npm i @reduxjs/toolkit react-redux
npm i @reduxjs/toolkit react-redux

Redux Toolkit的核心API主要是下述几个:

  • configureStore 包装createStore以提供简化的配置选项和良好的默认值
    • 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
    • 默认包含redux-thunk,并启用Redux DevTools Extension
  • createSlice 创建切片 片段
    • 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
  • createAsyncThunk
    • 接受一个动作类型字符串和一个返回Promise的函数
    • 并生成一个pending / fullfilled / rejected基于该承诺分派动作类型的thunk

写一个Demo:

tsx
// store/index.js
+}

ReduxToolkit

  • ReduxToolkit重构
  • ReduxToolkit异步
  • connect高阶组件
  • 中间件的实现原理
  • React状态管理选择

认识ReduxToolkit

之前在使用createStore创建Store时会出现deprecated标识,推荐我们使用@reduxjs/toolkit包中的configureStore函数

Redux Toolkit是官方推荐编写Redux逻辑的方法

  • 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
  • 代码分拆在不同模块中,存在大量重复代码
  • Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
  • 这个包常被称为:RTK

使用ReduxToolkit重写Store

Redux Toolkit依赖于react-redux包,所以需要同时安装这二者

bash
npm i @reduxjs/toolkit react-redux
npm i @reduxjs/toolkit react-redux

Redux Toolkit的核心API主要是下述几个:

  • configureStore 包装createStore以提供简化的配置选项和良好的默认值
    • 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
    • 默认包含redux-thunk,并启用Redux DevTools Extension
  • createSlice 创建切片 片段
    • 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
  • createAsyncThunk
    • 接受一个动作类型字符串和一个返回Promise的函数
    • 并生成一个pending / fullfilled / rejected基于该承诺分派动作类型的thunk

写一个Demo:

tsx
// store/index.js
 import { configureStore } from '@reduxjs/toolkit'
 import counterSlice from './features/counter'
 
@@ -797,7 +797,7 @@
       )
     }
   }
-)

createSlice 函数参数解读

  • name 标记Slice 展示在dev-tool中
  • initialState 初始化状态
  • reducers 对象 对应之前的reducer函数
  • 返回值: 一个对象 包含所有actions

configureStore 解读

  • reducer 将slice中的reducer组成一个对象,传入此参数
  • middleware 额外的中间件
    • RTK已经为我们集成了redux-thunkredux-devtool两个中间件
  • devTools 布尔值 是否启用开发者工具

使用RTK执行异步dispatch

实际场景中都是在组件中发起网络请求,并且将状态更新到Store中

之前的开发中,我们通过redux-thunk这个中间件,让dispatch中可以进行异步操作

ReduxToolkit默认已经给我们集成了Thunk相关的功能:createAsyncThunk

下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来

tsx
// store/features/postList.js
+)

createSlice 函数参数解读

  • name 标记Slice 展示在dev-tool中
  • initialState 初始化状态
  • reducers 对象 对应之前的reducer函数
  • 返回值: 一个对象 包含所有actions

configureStore 解读

  • reducer 将slice中的reducer组成一个对象,传入此参数
  • middleware 额外的中间件
    • RTK已经为我们集成了redux-thunkredux-devtool两个中间件
  • devTools 布尔值 是否启用开发者工具

使用RTK执行异步dispatch

实际场景中都是在组件中发起网络请求,并且将状态更新到Store中

之前的开发中,我们通过redux-thunk这个中间件,让dispatch中可以进行异步操作

ReduxToolkit默认已经给我们集成了Thunk相关的功能:createAsyncThunk

下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来

tsx
// store/features/postList.js
 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
 
 export const fetchPostList = createAsyncThunk('fetch/postList', async () => {
@@ -1133,7 +1133,7 @@
         return <WrapperComponent {...this.props} {...state} {...dispatch} />
       }
     }
-}

经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染

如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render

进一步地,我们可以补充更多细节:

  • 当组件卸载时解除监听
    • store.subscribe会返回一个unsubscribe函数 用于解除监听
  • 解除与业务代码store的耦合
    • 目前的store来自业务代码 更优的做法是从context中动态获取到store
    • 应当提供一个context Provider供用户使用
    • 就像react-redux一样,使用connect前需要将App用Provider包裹并传入store

至此就基本完成了一个connect函数

tsx
// connect.js
+}

经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染

如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render

进一步地,我们可以补充更多细节:

  • 当组件卸载时解除监听
    • store.subscribe会返回一个unsubscribe函数 用于解除监听
  • 解除与业务代码store的耦合
    • 目前的store来自业务代码 更优的做法是从context中动态获取到store
    • 应当提供一个context Provider供用户使用
    • 就像react-redux一样,使用connect前需要将App用Provider包裹并传入store

至此就基本完成了一个connect函数

tsx
// connect.js
 import { PureComponent } from 'react'
 import { StoreContext } from './storeContext'
 
@@ -1310,7 +1310,7 @@
 }
 
 applyMiddleware(store, logger, thunk) // 使用applyMiddleware

Released under the MIT License.

- + \ No newline at end of file diff --git a/note/SSR.html b/note/SSR.html index 2df611c4..62cda3d0 100644 --- a/note/SSR.html +++ b/note/SSR.html @@ -42,7 +42,7 @@ "nuxt": "^3.2.0" } }
  • build 构建正式版本,将被输出到.output文件夹
  • dev 开发环境
  • generate 打包正式版本项目,但是会预渲染每个路由(nuxt build --prerender
  • preview 对打包项目启动本地预览服务器
  • postinstall 该脚本为NPM的生命周期函数,将在npm install后执行

针对postinstall中执行的nuxi prepare脚本,官方文档有如下解释:

The prepare command creates a .nuxt directory in your application and generates types. This can be useful in a CI environment or as a postinstall command in your package.json.

执行该脚本后,将在项目根目录创建.nuxt文件夹

  • 这个文件夹将作为默认构建输出的文件夹
  • 其中的文件都以.d.ts结尾,是Nuxt的TS类型声明

配置 Configuration

  • 通过 nuxt.config.ts 文件,对Nuxt进行自定义配置
  • runtimeConfig 运行时配置 即定义环境变量
    • 直接定义在 runtimeConfig 中的值,仅在服务端可以访问到
      • 定义在runtimeConfig.public中的变量,在客户端和服务端中都能读取到
      • 也可以将环境变量定义在.env文件中,优先级.env > runtimeConfig
        • NUXT_开头的会作为私有环境变量读取到运行时
        • NUXT_PUBLIC_开头的会作为公共变量读取到运行时
    • appConfig 应用配置,定义在构建时确定的公共变量,如 theme
      • 配置会和app.config.ts合并,优先级app.config.ts > appConfig
    • app app的配置
    • head 给每个页面设置head信息,也支持useHead配置和内置组件
      • 在这个配置中定义的标签,会注入到所有页面的head标签中
      • 也可以在某些页面动态插入head标签内容 使用useHead函数
      • 或者在template中使用Nuxt的内置组件Head
    • ssr 指定应用渲染模式
      • 默认值为true 即采用SSR方式渲染应用
      • 如果指定了ssr: false 则会采用SPA的方式渲染应用,即客户端渲染
    • router 配置路由相关的信息,比如在客户端渲染可以配置hash路由
      • 需要注意的是:SSR并不支持哈希路由
      • router: { options: { hashMode: false } }
    • alias 路径别名
      • 默认已经为我们配置好了一些别名,详情可以参阅文档
    • modules 配置Nuxt扩展的模块,比如@pinia/nuxt @nuxt/image
    • routeRules 定义路由规则,可以更改路由的渲染模式或分配基于路由缓存策略
    • builder 指定使用Vite还是Webpack来构建应用,默认是Vite,如切换为Webpack还需要安装额外依赖

runtimeConfig 与 appConfig

  • runtimeConfig: 定义环境变量,比如:运行时需要指定的私有/公共的token等
  • appConfig: 定义公共变量,比如:构建时确定的公共token、网站配置等

针对他们的比较,官方文档提供了一个表格可以参阅:

FeatureruntimeConfigapp.config
Client SideHydratedBundled
Environment Variables✅ Yes❌ No
Reactive✅ Yes✅ Yes
Types support✅ Partial✅ Yes
Configuration per Request❌ No✅ Yes
Hot Module Replacement❌ No✅ Yes
Non primitive JS types❌ No✅ Yes

runtimeconfig-vs-appconfig

区分Client和Server环境

Nuxt为我们扩展了Node的process对象,并为我们在浏览器环境提供了process对象:

  • Nuxt会在服务端的process对象中注入属性dev server client以供使用
  • 也会在浏览器网页中注入process对象,包含上述的三个属性
  • 也可以手动判断typeof window === 'object'检查是服务器环境/浏览器环境

页面与组件 View and Component

Nuxt会自动为我们:注册组件、注册页面路由,约定>配置

  • 位于pages/下的页面都会被注册路由
    • 路由使用内置组件NuxtPage占位,相当于router-view
    • 相对应的,可以使用NuxtLink执行跳转,相当于router-link
    • 因为底层是vue-router,所以动态路由、嵌套路由都是支持的
  • 位于components/下的组件都会被自动全局注册

Nuxt3 内置组件

Nuxt3 框架提供了一些内置的组件,常用的如下:

  • SEO组件:Html Body Head Title Meta Style Link NoScript Base
    • 这些组件的作用是,向页面中不同部分插入标签,在SSR的过程中渲染出来并返回给客户端
    • 这样爬虫就会在同步获取页面数据时获取到这些标签
  • NuxtPage:是Nuxt自带的页面占位组件
    • 需要显示位于目录中的顶级或嵌套页面pages/
    • 是对 router-view 的封装
  • ClientOnly:该组件包裹的内容只会在客户端渲染
    • 其中内容不会出现在服务端返回的.html文件中
    • 会在客户端通过JS脚本动态渲染出来
      • 类似于Vue3新增的内置组件Suspence
      • 可以为其传入具名插槽#fallback展示组件被渲染前的加载中状态
  • ServerOnly:该组件包裹的内容只会在服务端渲染

创建页面

文件目录即路由,,可以手动创建 也可以通过命令行快速创建页面

  • npx nuxi pages category/index: 创建pages/category/index.vue

  • npx nuxi pages home/index: 创建pages/home/index.vue

  • npx nuxi pages detail/[id]: 创建pages/detail/[id].vue 动态路由

  • 页面路由

    • 页面水合之后,页面导航会通过前端路由来实现,可以防止整页刷新
    • 当然,手动输入URL之后,点击刷新浏览器也可以导航,但这会导致整页刷新

路由中间件

Nuxt提供了一个可定制的路由中间件,用来监听路由的导航,包括:局部和全局监听

  • 匿名中间件
    • 在页面中,通过definePageMeta定义
    • 可以监听局部路由,当注册多个中间件时,会按照注册顺序执行
    • 首次访问会在双端执行,后续都只会在客户端执行
  • 命名路由中间件
    • middleware目录下定义,会自动加载中间件
    • 首次访问会在双端执行,后续都只会在客户端执行
  • 全局路由中间件
    • middleware目录中,需要带.global后缀的文件,每次路由更改会自动运行
    • 与前两者不同,每次页面切换,双端都会执行全局中间件

路由验证 Validate

Nuxt支持对每个页面路由进行验证,可以通过definePageMeta中的validate数学对路由进行验证

  • validate属性接收一个回调函数,回调函数以route作为参数
    • 此回调返回一个布尔值,来决定是否放行路由
      • false 拦截路由 默认重定向到404页面
      • true 放行路由 正常跳转
    • 返回一个对象
      • { statusCode: 401 } 返回自定义的 401 页面 验证失败
  • 可以自定义错误页面
    • 在项目根目录 新建error.vue

页面布局 Layout

Layout布局是页面的包装器,可以将多个页面共性的东西抽取到Layout布局中

例如:每个页面的页眉和页脚,这些具有共性的组件,我们可以写到一个Layout布局中

本质上是Vue3的<slot>组件

  • 默认布局,创建layouts/default.vue
    • 然后在app.vue中通过内置组件<NuxtLayout>使用
  • 自定义布局
    • 创建layouts/custom-layout.vue
    • 然后在app.vue中,为<NuxtLayout>传入name属性(具名插槽)

渲染模式

浏览器和服务器都可以解释JavaScript代码,都可以将Vue.js组件呈现为HTML元素,此过程称为渲染

  • 在客户端渲染组件为HTML元素的过程,称为客户端渲染
  • 在服务端完成这个此操作的过程,称为服务端渲染

而Nuxt3支持多种渲染模式

  • 之前在配置文件中提到的ssr选项,可以选择以SSR模式渲染,还是CSR方式渲染
  • 混合渲染模式(SSR | CSR | SSG | SWR):需要在 routeRules 根据每个路由动态配置渲染模式

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/ClipboardManager/guide/index.html b/project/ClipboardManager/guide/index.html index 61754ad8..71e88db8 100644 --- a/project/ClipboardManager/guide/index.html +++ b/project/ClipboardManager/guide/index.html @@ -42,7 +42,7 @@ "match": ["image", { "type": "file", "regex": ".(?:jpg|jpeg|png)$" }], "command": "redirect:上传到图床" }

这个功能除了可以匹配图片,还可以将符合正则的图片文件匹配上,在匹配上的历史记录上展示上传到图床按钮,携带数据跳转到图床插件,一键上传。

需要注意的是,因为自定义功能按钮实现的是携带数据跳转不同插件,所以redirect后的内容并不应该是普通关键字(普通关键字仅能作为插件入口,而不能携带数据),而应该是文本/图片/文件或文件夹

  • id: String 全局唯一 必须以custom开头 建议以时间戳为后缀
  • title: String 鼠标悬停时展示的文本
  • icon: String 展示在插件内的图标
  • match: <String | Object>[] 匹配模式
  • command: String 执行跳转的关键字 前缀redirect:是必须的

在未来的版本更新中,超级剪贴板将开放更多自定义功能给高级用户,帮助你更高效率的管理、使用剪贴板。

- + \ No newline at end of file diff --git a/project/ClipboardManager/index.html b/project/ClipboardManager/index.html index ef4b4b61..6a2172a3 100644 --- a/project/ClipboardManager/index.html +++ b/project/ClipboardManager/index.html @@ -18,7 +18,7 @@
Skip to content

🔰 开始使用

首次安装需要设置“跟随主程序同时启动”

  • ✅ 监听剪贴板并持续将新内容更新到本地磁盘 数据加密保存在本地
  • ✅ 支持置顶、收藏、编辑、转存、多选、WebDAV同步、局域网共享等强大功能
  • ✅ 强大的工具栏,支持自定义快捷动作,定制自己的工作流
  • ✅ 优雅的界面动效与交互 高度自定义的配置选项 深色模式模式适配

📚 安装方式

Github

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/ClipboardManager/log/index.html b/project/ClipboardManager/log/index.html index 2c09e5b9..9f633fbc 100644 --- a/project/ClipboardManager/log/index.html +++ b/project/ClipboardManager/log/index.html @@ -18,7 +18,7 @@
Skip to content

更新日志

2.3.4

2024-01-06

  • feat: 支持 WebDAV 数据主动上传/下载
  • fix: 优化收藏内容展示逻辑
  • fix: 优化插件通知提示逻辑

2.3.3

2023-11-26

  • feat: 支持清空剪贴板图片
  • feat: 支持修改数据库路径
  • feat: 代码高亮自动换行
  • fix: 索引展示数量增加至99
  • fix: 优化大文本复制体验
  • fix: 置顶与收藏互斥
  • fix: 清理不必要的系统通知

2.3.2

2023-11-22

  • feat: 支持历史记录数字展示
  • fix: 修复WebDAV同步视图未更新的问题
  • fix: 修正数据导入收藏未保存的问题
  • fix: 优化收藏体验

2.3.1

2023-11-21

  • feat: 优化大文本展示
  • feat: 调整收藏项的样式
  • fix: 优化导入旧数据操作路径
  • fix: 修正多选状态下展示的工具栏

2.3.0

2023-11-21

  • refactor: 插件重构

v2.2.2

2023-09-25

  • feat: 新增快捷键 Ctrl+上下左右 快速切换分类与跳转首个/最末条目
  • feat: 分类支持左右快速跳转首个/最末分类
  • feat: 支持设置不记录文件内容
  • fix: 修正底部提示展示规则
  • fix: 执行完自定义操作按钮后自动退出预览抽屉

v2.2.1

2023-06-26

  • fix: 修复轮询监听器失效不监听的问题

v2.2.0

2023-06-25

  • feat: 会员新增Vim键位 支持h/j/k/l
  • feat: 会员新增快速预览快捷键 `
  • fix: 预览页内无法收藏的问题

v2.1.4

2023-05-08

  • fix: 多选模式下不再隐藏导航栏
  • fix: 移除Shift进入多选模式的快捷键
  • chore: 更新依赖版本

v2.1.3

2023-04-12

  • fix: 修正合并粘贴功能
  • fix: 合并图文导出时过滤掉文件内容
  • fix: 调整使用记录的存储组织方式
  • fix: 调整合并图文导出的插件关键字

v2.1.2

2023-04-09

  • feat: 支持多选后批量删除
  • feat: 支持合并图文保存到超级Markdown
  • fix: 优化深色模式下多选条目背景色
  • fix: 修复图片过宽时溢出容器的问题

v2.1.1

2023-04-05

  • feat: 支持在设置页一键清理所有图片
  • fix: 修正本地数据版本较新时被云端覆盖的问题

v2.1.0

2023-03-29

  • feat: 收藏功能增强 支持为收藏内容添加备注
  • feat: 优化界面动效

v2.0.11

2023-03-01

  • fix: 优化导航栏展示/隐藏逻辑
  • fix: 修复某些场景下ESC无法退出插件的问题

v2.0.10

2023-02-27

  • fix: 修复轮询监听器执行错误导致插件白屏的问题

v2.0.9

2023-02-27

  • feat: 添加键盘操作模式 屏蔽鼠标滑动 高效快捷
  • feat: 添加极简模式 隐藏页面不必要组件
  • feat: 顶部导航栏添加展示/隐藏动效
  • feat: 移除部分动画效果 让动作简洁有力
  • feat: 添加底部横幅提醒监听器状态
  • fix: 优化轮询监听器性能
  • fix: 修复某些情况下导致的白屏问题
  • fix: 优化代码执行细节 提升插件稳定性

v2.0.8

2023-02-24

  • feat: 添加插件会员用户彩蛋
  • fix: 修复某些情况导致的插件白屏问题

v2.0.7

2023-02-24

  • fix: 修复某些情况下插件主页渲染被阻塞导致的白屏问题
  • fix: 修复初始化时重复执行Webdav上传的问题

v2.0.6

2023-02-22

  • fix: 优化列表展示性能
  • fix: 优化本地读写稳定性 修复数据损坏导致的白屏问题
  • fix: 优化代码高亮依赖缓存 避免多次请求下载

v2.0.5

2023-02-19

  • fix: 修复插件启动时WebDav上传失败报错的问题
  • fix: 修复插件启动时会员用户登录联网失败的问题

v2.0.4

2023-02-13

  • feat: 支持修改代码高亮主题、样式与是否启用
  • feat: 右下角常驻当前选中条目的更新日期
  • feat: 页面滚动条样式优化
  • fix: 修复错误识别文本内容为网址的问题
  • fix: 修复删除条目后插件卡死的问题
  • fix: 修复某些情况下数据不再向下展示的问题

v2.0.3

2023-02-10

  • feat: 按住Ctrl/Command点击文本/链接 可以直接预览/跳转
  • feat: 按住Ctrl/Command点击图片 可以直接全局预览
  • feat: 按住Ctrl/Command点击文件 可以打开其所在文件夹
  • feat: 调整预览页展开样式
  • fix: 多选状态无法通过点击鼠标添加选项
  • fix: 某些情况下预览页无法执行Ctrl+C部分复制

v2.0.2

2023-02-10

  • fix: 更新到新版后首次进入白屏问题
  • fix: 新设备首次进入插件数据库路径读取问题
  • fix: 优化联网失败的弹窗逻辑
  • fix: 普通用户图标颜色异常

v2.0.1

2023-02-10

  • feat: 工具栏按钮顺序调整 会员用户添加金色标识
  • fix: 切换分类时出现明显卡顿
  • fix: 未登录用户进入用户中心白屏
  • fix: 添加网络问题导致登录验证失败的弹窗提醒

v2.0.0

2023-02-07

  • feat: 界面焕新 简洁高效
  • feat: 支持WebDav自动同步功能
  • feat: 支持预览页代码高亮
  • feat: 支持自定义分类卡片的顺序
  • feat: 支持识别文本内容为色值、链接等
  • feat: 支持左右键切换导航
  • feat: 支持关闭剪贴板图片记录
  • feat: 支持手动设置偏好主题
  • fix: 清空数据库保留收藏内容
  • fix: 滚动到底部触发懒加载失败
  • refactor: 组件重构 优化大列表运行时性能
  • refactor: 本地读写性能优化

v1.4.7

2022-11-03

  • feat: 设置页支持使用ESC返回上一级
  • fix: 移除了启动监听程序时对MacOS的特殊判断
  • fix: 移除了缺少监听程序时的通知报错

v1.4.6

2022-11-01

  • feat: 调整设置页图标与界面按钮细节
  • fix: 会员用户由于数据同步导致的插件白屏问题
  • fix: 多选复制时条目排序被颠倒

v1.4.5

2022-10-10

  • feat: 调整过期天数列表 最长支持31天
  • feat: 设置页添加handler安装引导
  • fix: 清空数据时内存数据未清空
  • fix: 多端同步时内存数据未更新
  • fix: 开发者模式下listener错误挂载

v1.4.4

2022-09-26

  • fix: 修复重复粘贴的问题

v1.4.3

2022-09-25

  • feat: 增加空格快捷键 按下连续多选的功能
  • feat: 多选状态下移动鼠标不再激活列表条目
  • feat: 支持展示剪贴板监听程序状态
  • fix: 删除某条历史记录时不再返回顶部
  • fix: 多选图片无法合并复制
  • fix: 内置按钮颜色管理关键字错误
  • fix: 设置页下拉框过长时被卡片遮挡
  • fix: 设置页滚动到底部触发懒加载

v1.4.2

2022-09-21

  • chore: 移除了插件内的二进制文件 恢复旧的监听机制 性能问题有待解决

v1.4.1

2022-09-21

  • fix: 修复了新的监听机制在 Mac 下无法正确获取剪贴板更新事件的问题

v1.4.0

2022-09-20

插件能够正确在Linux上运行 离不开 小千 不厌其烦的测试与调试 💖

感谢每一位为超级剪贴板提出过建议、参与测试与调试的用户

  • feat: 支持自定义数据库路径/存储条数/过期时间/展示在主界面的按钮
  • feat: 功能按钮支持自定义 创造属于你自己的功能按钮
  • feat: 添加了9个内置的自定义功能按钮
  • feat: 增加了更优雅的弹窗与提示界面
  • feat: 预览页更宽了 支持预览图片
  • feat: 导航栏图标样式修改
  • fix: 解决了CPU占用高、浏览大图卡顿的性能问题
  • fix: 从后台进入插件自动清空搜索框
  • refactor: 改变了监听剪贴板的方式 性能更优
  • refactor: 引入了ElementPlus组件库与图标库

v1.3.4

2022-09-19

  • feat: 移除插件内收藏Tab
  • feat: 点击收藏按钮后将携带数据跳转至备忘快贴
  • refactor: 优化代码执行逻辑 移除冗余代码

v1.3.3

2022-09-16

  • feat: 提高主色在深色模式下的对比度 改善长文本表现
  • feat: 添加快存功能按钮 配合超级粘贴插件实现快速转存
  • feat: 支持在预览页展示全部功能按钮
  • feat: 重要版本更新自动展示通知卡片
  • fix: 修正多选快捷键Shift影响检索输入的问题
  • fix: 检索词更新后 未自动激活首条记录

v1.3.2

2022-09-15

  • feat: 鼠标悬停展示具体日期、完整数据
  • feat: 调整历史记录上限为800条
  • fix: 修复鼠标置于功能按钮时上下键失效的问题
  • fix: 调整大图片判定尺寸

v1.3.1

2022-09-12

  • feat: 支持通过历史记录列表进入分词
  • feat: 减小插件体积 对智慧分词的支持改为插件跳转

v1.3.0

2022-09-09

  • feat: 添加多选功能 支持跨标签合并文本/图片/文件
  • feat: 支持通过按下Shift进入多选功能 支持按住Shift快速选择
  • feat: 支持使用Ctrl+CEnter快捷合并复制/粘贴
  • feat: 添加智慧分词功能 可对文本进行分割提取
  • feat: 长文本以蓝色高亮显示 不再提供查看全部按钮
  • feat: 优化界面动效

v1.2.3

2022-09-08

  • fix: 修复了检索内容时搜索记录不准确的问题

v1.2.2

2022-09-07

  • feat: 搜索框支持使用空格分词以同时检索多个关键词
  • fix: 修复了剪贴板数据记录遗漏的问题
  • fix: 收藏内容不再计入到条数限制中
  • fix: 修复了非预期的搜索框聚焦行为
  • fix: 预览页的按钮改为固定在顶部

v1.2.1

2022-09-05

  • feat: 支持通过功能按钮进入任意数据的预览页面
  • feat: 支持通过功能按钮打开文件所在目录
  • feat: 添加取消收藏功能按钮
  • feat: 搜索框支持展开/收起 输入任意内容展开并聚焦搜索
  • feat: 侧栏预览页添加复制全部智慧分词按钮
  • feat: 功能按钮改为使用图标展示
  • feat: 优化了界面样式
  • refactor: 清理定时器 优化插件性能

v1.2.0

2022-09-04

  • feat: 添加右侧操作栏 支持复制/收藏/删除操作
  • feat: 添加标签页收藏 支持在此页面管理所有收藏
  • feat: 调整侧栏宽度 调整界面样式细节

v1.1.7

2022-08-30

  • fix: 定时器检查剪贴板,修复了不记录剪贴板的问题

v1.1.6

2022-08-27

  • feat: 添加Alt+数字键Ctrl+数字键快速选择功能
  • feat: 调整界面样式 调整查看全部位置 移除图片背景色 增加动效
  • feat: 搜索框增加检索条数展示
  • fix: 改善插件读取图片的性能

v1.1.5

2022-08-25

  • fix: 提高剪贴板读取频率,避免高频复制时丢数据的情况

v1.1.4

2022-08-20

  • feat: 增加清空搜索框的按钮
  • feat: 优化插件内按下ESC的功能: 退出完整预览/清空搜索框
  • feat: 进入插件自动选中框内全部文本
  • fix: Mac在分离窗口状态左键会粘贴到搜索框 #13

v1.1.3

2022-08-19

  • feat: 调整界面在深色模式下的表现样式
  • fix: 在侧栏全部数据中使用Ctrl+C复制部分文本失效
  • fix: Mac下使用Ctrl+C复制单条记录失效
  • fix: 通过换行符个数区分超长文本
  • fix: 窗口分离下鼠标单击导致粘贴到搜索框
  • chore: 原插件名剪贴板改为超级剪贴板

v1.1.2

2022-08-18

  • feat: 执行复制后只隐藏主界面而不退出插件到后台
  • feat: 搜索时不再区分大小写
  • fix: 文本内容会出现异常首行缩进
  • fix: 图片展示在列表中的时候 右侧多出一个图块
  • fix: 移除鼠标hover时数据底部的色块

v1.1.1

2022-08-17

  • fix: 复制超大图片进入插件时崩溃

v1.1.0

2022-08-16

  • feat: 支持使用 键切换选中记录
  • feat: 支持使用 Ctrl+C 复制选中记录
  • feat: 支持使用 Enter 复制选中记录并粘贴

v1.0.4

2022-08-16

  • feat: 进入插件自动回到顶部、切换至全部分类
  • feat: 监听到键盘事件自动聚焦到搜索框
  • feat: 调整鼠标hover动画与历史记录的active样式
  • fix: 使用Tab切换导航失效
  • fix: 超长图片显示越界

v1.0.3

2022-08-16

  • fix: 路径分隔符导致写入错误的数据文件

v1.0.2

2022-08-15

  • fix: Mac下由权限导致的无法写入数据

v1.0.1

2022-08-15

  • feat: 区分鼠标点击行为:左键复制并粘贴,右键仅复制
  • feat: 适配深色模式
  • feat: 移除了右侧的查看更多按钮
  • feat: 单次展示的条数增加到了15条
  • feat: 增加条数限制500条 增加存储日期限制14天
  • feat: 增加了多平台支持
  • fix: 执行粘贴后主输入框未隐藏
  • refactor: Vue3重构

v1.0.0

2022-08-14

  • release: v1.0.0发布

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/ClipboardManager/statement/index.html b/project/ClipboardManager/statement/index.html index d687a00e..8492b3e6 100644 --- a/project/ClipboardManager/statement/index.html +++ b/project/ClipboardManager/statement/index.html @@ -18,7 +18,7 @@
Skip to content

Q&A

如何设置快捷键快速打开插件

uTools 个人中心 / 全局快捷键 / 将 Clipboard 设置为你想要的快捷键

即可通过快捷键快速唤出超级剪贴板插件

剪贴板记录丢失

一般是由于插件退出后台运行导致的, 插件需要保持后台运行才能记录剪贴板, 请检查:

  • 是否正确设置了插件跟随主程序启动
  • 是否手动清理或关闭了后台插件
    • 在独立窗口模式下关闭插件(uTools 机制:关闭独立窗口则退出插件)
    • 使用了 clear 命令
    • 在插件列表主动退出了插件
  • 插件在高频复制的场景下,可能会漏掉某些记录

使用中遇到任何问题,请通过论坛反馈

插件启动时闪退

使用二进制监听器时,插件首次启动会自动隐藏到后台,重新唤起插件即可

插件启动时报错/白屏

请尝试以下方法:

  1. 完全退出插件后重新启动插件
  2. 重新启动 uTools
  3. 格式化 uTools 内的插件数据 (个人中心/数据存储) 这不会清空你的本地数据
  4. 删除本地数据库目录(全局搜索 super-clipboard-data 文件夹)

如果上述方法都无法解决你的问题,欢迎论坛回帖交流具体情况

自动粘贴(自动上屏)功能失灵了

目前已知:自动粘贴功能在 Windows微信 的聊天输入框内是不可用的。在其他场景下,此功能都能够正常使用

此外,建议你保持 uTools 与插件版本更新,避免使用旧版 uTools 导致兼容性问题

我对这个插件的安全性有担忧, 插件偷窃我的隐私怎么办?

首先, 我写这个插件不是为了获取你的隐私的, 我对你的隐私没有兴趣, 这一点你完全可以放心;

其次 uTools 官方在插件上架前会对代码进行审查, 如果插件有高危行为, 那也不会过审;

再其次, uTools 大部分用户是程序员, 如果我真的在代码里藏了"毒", 那他们也有办法发现, 如果你真的对你的隐私十分关心, 可以选择从开源仓库下载代码自行构建

开源版本和插件市场版本的区别?

开源版本后续将只提供必要的BUG修复, 不再添加新功能

  • 开源版本: 包含完整的基本功能, 可以自行构建开源版本, 通过安装离线插件方式使用
  • 市场版本: 包含后续更新的新功能插件会员功能, 可以直接从插件应用市场安装

为什么不开源了, 为什么要开始收费?

目前由我个人维护的开源版本已经趋于稳定, 可以满足绝大部分场景的需求

  • 代码开源的出发点不是为了让不愿付费的人白嫖开发者的劳动, 而是为开发提供更多的经验和思路, 开源的代码已经启发了一些开发者上架了自己的剪贴板插件应用
  • 开发和维护插件需要时间和精力, 插件付费可以鼓励我更积极的更新
  • 目前插件内已有的基本功能都不会转为收费, 可以放心使用

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/JSRunner/index.html b/project/JSRunner/index.html index e2b8dba0..77dd17d7 100644 --- a/project/JSRunner/index.html +++ b/project/JSRunner/index.html @@ -18,7 +18,7 @@
Skip to content
logo

✨ JavaScript运行器 支持多种运行环境 快速验证代码逻辑

🕶️ 在线体验👨🏻‍💻 开源地址🚚 更新日志

🔰 开始使用

  • ✅ 运行JavaScript代码 快速验证代码逻辑
  • ✅ 支持切换NodeJS/浏览器操作环境
  • Ctrl/Command+R 快速运行代码
  • Ctrl/Command+Q 清除控制台
  • Ctrl/Command+N 创建新的代码片段
  • Ctrl/Command+E 切换锁定状态
  • Ctrl/Command+Shift+P 调用命令面板
  • Ctrl/Command+Shift+L 列出所有历史记录
  • ✅ 支持回溯代码历史 支持保存/编辑代码运行历史
  • ✅ 支持手动触发代码执行/实时运行代码
  • ✅ 支持顶层await 适配深色模式

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/JSRunner/log/index.html b/project/JSRunner/log/index.html index 7c4672d4..736e4fbf 100644 --- a/project/JSRunner/log/index.html +++ b/project/JSRunner/log/index.html @@ -18,7 +18,7 @@
Skip to content

更新日志

v1.2.0

2024-01-06

  • feat: 支持编辑器锁定
  • feat: 支持快捷键切换编辑器锁定状态
  • feat: 支持自定义缩进、代码片段
  • feat: 添加更多快捷键
  • fix: 页面UX优化

v1.1.2

2023-07-09

  • feat: 支持删除选中的代码片段
  • feat: 控制台输出JSON时 支持点击跳转
  • feat: 切换页面时缓存编辑器实例
  • fix: 历史记录支持滚动 调整顺序

v1.1.1

2023-07-06

  • feat: 调整脚本执行时的按钮动效
  • feat: 进入插件自动聚焦编辑器
  • fix: 修正历史记录展示数量问题

v1.1.0

2023-06-26

  • feat: 新增内置Snippet用于快速输出日志
  • feat: 新增快捷键用于清空控制台、创建代码片段
  • feat: 支持解析首行代码注释作为代码名称并展示
  • feat: 支持删除其他代码历史片段
  • feat: 调整代码历史回溯数量至99
  • feat: 代码运行动效优化
  • fix: 修正切换代码片段历史后编辑器未更新的问题

v1.0.6

2023-05-20

  • fix: 修正浏览器环境错误未捕获的问题

v1.0.5

2023-05-10

  • feat: 函数调用增加节流处理
  • fix: 修正错误处理导致的插件闪退问题

v1.0.4

2023-05-08

  • feat: 支持顶层await调用
  • fix: 修正控制台日志循环引用输出
  • refactor: 调整NodeJS下代码执行实现
  • chore: 更新依赖版本

v1.0.3

2023-04-28

  • feat: 开源仓库链接增加小红点
  • fix: 控制台支持展示undefined/null/Symbol
  • fix: 修复无法设置手动触发模式的bug

v1.0.2

2023-04-24

  • fix: 修复NodeJS环境下无法导入第三方模块的问题

v1.0.1

2023-04-23

  • feat: 关于页添加项目开源地址
  • fix: 即时执行模式下代码为空时忽略警告
  • fix: 调整历史回溯的数量限制

v1.0.0

2023-04-19

  • 插件发布

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/Markdown/index.html b/project/Markdown/index.html index 9197b2bb..7b5dfbf8 100644 --- a/project/Markdown/index.html +++ b/project/Markdown/index.html @@ -18,7 +18,7 @@
Skip to content

🔰 开始使用

  • 由ByteMD强力驱动,功能丰富、性能强劲
  • 支持GFM扩展语法、脚注、Gemoji、KaTeX数学公式、Mermaid图表
  • 支持通过Frontmatter设置多种主题、代码高亮样式
  • 支持多级目录,目录支持无限嵌套
  • 支持通过粘贴/拖拽的方式批量上传图片、支持截取屏幕截图
  • 支持Markdown文件的批量导入、批量导出
  • 支持插件多开,同时编辑/参考多个文章
  • 支持实时同步预览、自动保存

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/Markdown/log/index.html b/project/Markdown/log/index.html index 86cbb683..e975afbf 100644 --- a/project/Markdown/log/index.html +++ b/project/Markdown/log/index.html @@ -18,7 +18,7 @@
Skip to content

更新日志

v1.2.1

2023-10-12

  • feat: 适配深色模式
  • feat: 折叠侧栏按钮移动至顶部

v1.2.0

2023-06-24

  • feat: 更新依赖 支持Mindmap与Timeline
  • feat: 导出MD文档功能增强 支持导出为本地图片
  • feat: 支持展示Base64编码图片

v1.1.4

2023-04-23

  • feat: 关于页增加插件开源地址

v1.1.3

2023-04-17

  • feat: 优化侧栏操作逻辑
  • fix: 修正XML标签代码高亮异常的问题
  • fix: 修正侧栏滚动条样式

v1.1.2

2023-04-12

  • feat: 调整合并图文导出关键字定义
  • feat: 新建文章自动在标题跟随日期
  • fix: 优化不同场景下编辑器聚焦行为

v1.1.1

2023-04-10

  • fix: 调整插件描述
  • fix: 修复关键字跳转问题
  • fix: 移除页面切换动效

v1.1.0

2023-04-09

  • feat: 支持通过关键字创建文章
  • feat: 支持从超级剪贴板多选合并创建文章
  • feat: 支持从文本/图片快速创建文章
  • feat: 支持批量导入Markdown文档
  • feat: 支持点击设置页关键字跳转
  • fix: 修复脚标点击跳转问题
  • fix: 修复部分场景下滚动条样式问题
  • fix: 修复部分场景下工具栏展示问题

v1.0.1

2023-04-05

  • feat: 支持插件多开 同时编辑/参考多个文档
  • fix: 修复图片被滚动条遮挡的问题
  • fix: 修复部分场景下标题栏与侧栏的bug

v1.0.0

2023-04-01

  • 插件发布

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/Markdown/shortcut/index.html b/project/Markdown/shortcut/index.html index 094aed1c..c98ea7d4 100644 --- a/project/Markdown/shortcut/index.html +++ b/project/Markdown/shortcut/index.html @@ -18,7 +18,7 @@
Skip to content

快捷键一览

粗体 Cmd-B

斜体 Cmd-I

链接 Cmd-K

图片 Shift-Cmd-I

代码 Shift-Cmd-K

代码块 Shift-Cmd-C

无序列表 Shift-Cmd-U

有序列表 Shift-Cmd-O

Markdown 语法

一级标题 # 标题

二级标题 ## 标题

三级标题 ### 标题

粗体 **粗体文本**

斜体 *斜体文本*

引用 > 引用文本

链接 [链接描述](url)

图片 ![alt](url "图片描述")

代码 代码

代码块 ```编程语言↵

无序列表 - 项目

有序列表 1. 项目

分割线 ---

删除线 ~~文本~~

任务列表 - [ ] 待办事项

行内公式 $公式$

块级公式 $$↵公式↵$$

Mermaid图表 ```mermaid

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/SmartWordBreak/index.html b/project/SmartWordBreak/index.html index 4381fa07..f6f88cbc 100644 --- a/project/SmartWordBreak/index.html +++ b/project/SmartWordBreak/index.html @@ -18,7 +18,7 @@
Skip to content
logo

✨ 智慧分词,快速提取文本关键词。

⭐ 插件发布页🌎 Q&A🚚 更新日志

🔰 开始使用

服务器不会保留处理的数据, 但请避免使用此功能处理敏感数据

  • ✅ 支持选中文本后通过超级面板直接进入分词
  • ✅ 支持直接读取剪贴板文本分词
  • ✅ 支持单选/拖拽/跨段落快速拖选
  • ✅ 支持一键合并复制/粘贴/翻译/搜索
  • ✅ 普通用户有每日免费额度 每日0:00重置
  • ✅ 优雅、迅速的动效与交互 适配深色模式
  • ✅ 可以通过插件内入口获取更多额度
  • ✅ 与超级剪贴板插件集成,一次购买 多处使用

📚 安装方式

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/SmartWordBreak/log/index.html b/project/SmartWordBreak/log/index.html index 6e53dd89..3c404bce 100644 --- a/project/SmartWordBreak/log/index.html +++ b/project/SmartWordBreak/log/index.html @@ -18,7 +18,7 @@
Skip to content

更新日志

v1.0.5

2022-12-17

  • feat: 限免活动 每日免费额度调整为99 移除充值入口
  • fix: 修复历史记录显示问题

v1.0.4

2022-09-25

  • feat: 分词上限提高到了1000个字符
  • feat: token支持本地缓存

v1.0.3

2022-09-19

  • feat: 增加历史记录功能
  • fix: 移除了在tag外按下鼠标拖选的功能

v1.0.2

2022-09-13

  • feat: 支持跨段快速拖选
  • feat: 分词页添加了搜索粘贴功能
  • feat: 优化换行符在分词页的表现 支持包含换行符拖选

v1.0.1

2022-09-13

  • feat: 增加了拖选过程中的动效
  • feat: 调整了按钮样式与颜色
  • fix: 移除了碍事的toast

v1.0.0

2022-09-11

  • release: v1.0.0发布

Released under the MIT License.

- + \ No newline at end of file diff --git a/project/SmartWordBreak/statement/index.html b/project/SmartWordBreak/statement/index.html index 754789b3..4bd01447 100644 --- a/project/SmartWordBreak/statement/index.html +++ b/project/SmartWordBreak/statement/index.html @@ -18,7 +18,7 @@
Skip to content

Q&A

  • Q: 为什么要收费
  • A: 此功能需要服务器资源, 而服务器资源有限, 故暂时只开放部分的免费资源给大家使用, 普通用户每日有部分免费额度, 额度每日0:00重置

  • Q: 我支付了,但是没有收到额度
  • A: 支付成功后,插件会自动获取最新用户信息,如果信息没有自动更新,请尝试退出插件后重新进入,如果仍未收到额度,请通过论坛联系我,我会尽快核实并为你解决

Released under the MIT License.

- + \ No newline at end of file diff --git a/self/index.html b/self/index.html index 55e15b22..41c16765 100644 --- a/self/index.html +++ b/self/index.html @@ -18,7 +18,7 @@
Skip to content

个人介绍

logo

北京交通大学(BeijingJiaoTong University)电子信息工程学院本科在读

  • 熟悉ES6特性, 有前端领域开发经验, 能独立完成基础的前端开发工作;
  • 熟练使用Vue框架及相关工具开发应用, 阅读过部分Vue源码, 了解Vue响应式实现原理;
  • 掌握NodeJS基本使用, 能够基于NodeJS编写开发工具或搭建Web服务;
  • 了解Webpack基本配置和使用, 有Webpack Vite等前端工程化工具使用经验;
  • 了解Typescript, 有Typescript的项目使用经历;
  • 良好的Git操作, 清晰的Commit提交, 保证代码质量;
  • 对前端学习抱有热情, 有良好的学习能力, 能够快速学习掌握新知识;

技术栈

前端技术

Vue Router Vuex Pinia Element Plus Echart WangEditor

Sass/Less Axios Ajax ...

后端技术

相关技能

文档处理:

Microsoft Word Microsoft PowerPoint Microsoft Excel

平面设计:

Adobe PhotoShop Adobe Lightroom

影音制作:

DaVinci Resolve Studio Adobe Premiere Adobe After Effects

专业软件:

MATLAB ICEDA Multisim Keil uVision5

获得奖项

Microsoft Edge 浏览器开拓者大赛 开拓之星

稀土掘金2022编程挑战赛 三等奖

入选“扬帆计划·中央和国家机关大学生实习”

100周年现场志愿者

优秀学生干部、优秀共青团员

社会工作优秀奖学金

相关链接

Github哔哩哔哩稀土掘金CSDNGitee

Released under the MIT License.

- + \ No newline at end of file diff --git a/works/contribution.html b/works/contribution.html index 941dedf2..2d3bc923 100644 --- a/works/contribution.html +++ b/works/contribution.html @@ -18,7 +18,7 @@
Skip to content

社区贡献

稀土掘金助手

稀土掘金助手: 腾讯云函数部署

源代码

B站粉丝牌助手文档

B站粉丝牌助手文档

源代码Demo

Released under the MIT License.

- + \ No newline at end of file diff --git a/works/opensource.html b/works/opensource.html index 8bb07d59..0c0ae0be 100644 --- a/works/opensource.html +++ b/works/opensource.html @@ -18,7 +18,7 @@
Skip to content

个人作品

Vue3+TypeScript后台管理系统

Vue3+TypeScript后台管理系统

源代码

Typein 效率工具集

Typein 是一个运行在现代浏览器上的拓展插件,它可以帮助你在保持专注的情况下,高效完成各种操作。

源代码

宣传视频

Microsoft Edge Add-on Store

A-SOUL浏览器宠物

在浏览器里养一只A-Soul成员当宠物

源代码

宣传视频

北京交通大学课程平台功能增强

北京交通大学课程平台功能增强脚本,实现信息聚合,附件上传,让你高效处理课程信息。

源代码

超级剪贴板

uTools插件,一款强大的剪贴板管理工具。基于Vue3构建

源代码

主页

超级粘贴

uTools插件,将剪切板内容直接粘贴为文件。功能基于NodeJS

源代码

超级分词

uTools插件,前台使用Vue3+ElementPlus构建,后台基于Express封装结巴分词并暴露API接口

超级连点器

uTools插件,界面使用Vue3+AntDesign构建,功能基于Worker

PopNotify

仿 Element UI 的通知卡片。

源代码

Demo

UserScripts

用户脚本合集,生产力工具、效率工具、自动化脚本

源代码

Released under the MIT License.

- + \ No newline at end of file