总结
1. 响应式数据与副作用函数
副作用函数:可能会产生副作用的函数。例如更改了 body.innerHTML
。其它函数可能也会读取 body 的内容,副作用函数 effect 的执行会直接或间接影响其他函数的执行。
只要涉及设置值、读取值,都是副作用函数,副作用函数很常见。
js
var obj = {text: 'hello world'}
function effect() {
document.body.innerHTML = obj.text
}
响应式数据:obj.text
发生变化,副作用函数自动重新执行。obj 便是响应式数据。
2. 响应式数据的基本实现
- 副作用函数执行时,会触发
obj.text
的读取。 - 修改
obj.text
的值时,会触发obj.text
的设置。
只要拦截对象的读取和设置操作
- 在读取时将副作用函数收集起来。
- 在设置时执行所有收集起来的副作用函数。
如何做到拦截对象
- Object.defineProperty,Vue2
- Proxy,Vue3
收集副作用函数,可以使用 Set
类型,不会重复收集。
收集副作用函数这个行为,文章后续称为依赖收集。收集起来的副作用函数,文章后续称为依赖集合。
sh
收集副作用函数的行为 -> 依赖收集
收集的副作用函数 -> 依赖集合
基本实现上的缺陷
- 依赖收集时,目前是采用固定函数名 effect 指定收集,真实场景中可能没有函数名,函数名的命名也可以任意取。固定函数名不灵活
- 依赖集合只是一个
Set
实例,无法区分收集到的副作用函数,属于对象里的哪个属性,也无法区分属于哪个对象。
3. 设计一个完整的响应系统
解决以上缺陷,可以进行以下措施
- 依赖收集:提供一个注册副作用函数的机制,任何副作用函数都作为参数传给
effect
函数,由 effect 函数统一调用,在副作用函数被调用前,会采用一个全局变量activeEffect
,去存储当前的副作用函数。
js
// 全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {
// 调用effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn
// 执行副作用函数,执行过程中就会被 Proxy 代理的 get,收集 activeEffect 副作用函数
fn()
}
- 收集副作用函数的数据结构:树形结构,对象-属性-副作用函数。jsWeakMap:键为原始对象 target,值为 Map 实例。
WeakMap: target --> Map Map: key --> Set
Map:键为原始对象 target 的 key,值为副作用函数组成的 Set(依赖集合)。
WeakMap 和 Map的区别: WeakMap 的 key 为弱引用,不影响垃圾回收的工作。key 对应的是原始对象 target,只要 target 没有任何引用了,说明用户不再需要他,垃圾回收便会将其回收,WeakMap 中对应的 key 也会消失,节省了内存占用。 Map 的 key 为强引用,会影响垃圾回收工作,key 对应的 target 永远都会被引用,不会被垃圾回收。
- get 拦截函数里把副作用函数收集这部分逻辑,可以用
track
(追踪)函数封装。 - set 拦截函数里的触发副作用函数这部分逻辑,可以用
trigger
函数封装。
4. 分支切换与 cleanup
js
// 原始数据
const data = { ok: true, text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {/* */})
effect(() => {
console.log('effect run')
document.body.innerText = obj.ok ? obj.text : 'not'
})
分支切换:obj.ok
发生变化,代码执行的分支会跟着变化。
分支切换可能会产生遗留的副作用函数,其他值更新时,导致非必要的副作用函数触发。
例如上述例子,副作用函数会被 obj.ok
和 obj.text
收集。
当 obj.ok = false
时,obj.text
的值永远不会用到,但 obj.text
一旦更新,还是会触发副作用函数。