Skip to content
On this page

总结

1. 响应式数据与副作用函数

副作用函数:可能会产生副作用的函数。例如更改了 body.innerHTML。其它函数可能也会读取 body 的内容,副作用函数 effect 的执行会直接或间接影响其他函数的执行。
只要涉及设置值、读取值,都是副作用函数,副作用函数很常见。

js
var obj = {text: 'hello world'}
function effect() {
    document.body.innerHTML = obj.text
}

响应式数据:obj.text 发生变化,副作用函数自动重新执行。obj 便是响应式数据。

2. 响应式数据的基本实现

  1. 副作用函数执行时,会触发 obj.text 的读取。
  2. 修改 obj.text 的值时,会触发 obj.text 的设置。

只要拦截对象的读取和设置操作

  1. 在读取时将副作用函数收集起来。
  2. 在设置时执行所有收集起来的副作用函数。

如何做到拦截对象

  1. Object.defineProperty,Vue2
  2. Proxy,Vue3

收集副作用函数,可以使用 Set 类型,不会重复收集。
收集副作用函数这个行为,文章后续称为依赖收集。收集起来的副作用函数,文章后续称为依赖集合。

sh
收集副作用函数的行为  -> 依赖收集
收集的副作用函数      -> 依赖集合

基本实现上的缺陷

  1. 依赖收集时,目前是采用固定函数名 effect 指定收集,真实场景中可能没有函数名,函数名的命名也可以任意取。固定函数名不灵活
  2. 依赖集合只是一个 Set 实例,无法区分收集到的副作用函数,属于对象里的哪个属性,也无法区分属于哪个对象。

3. 设计一个完整的响应系统

解决以上缺陷,可以进行以下措施

  1. 依赖收集:提供一个注册副作用函数的机制,任何副作用函数都作为参数传给 effect 函数,由 effect 函数统一调用,在副作用函数被调用前,会采用一个全局变量 activeEffect ,去存储当前的副作用函数。
js
// 全局变量存储被注册的副作用函数
let activeEffect

// effect 函数用于注册副作用函数
function effect(fn) {
    // 调用effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
    activeEffect = fn

    // 执行副作用函数,执行过程中就会被 Proxy 代理的 get,收集 activeEffect 副作用函数
    fn()
}

  1. 收集副作用函数的数据结构:树形结构,对象-属性-副作用函数。
    js
    WeakMap: target --> Map    
    Map: key --> Set     
    
    WeakMap:键为原始对象 target,值为 Map 实例。
    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.okobj.text 收集。
obj.ok = false 时,obj.text 的值永远不会用到,但 obj.text 一旦更新,还是会触发副作用函数。

疑问