设计完善的响应系统

165 阅读5分钟

副作用函数与响应式数据

认识副作用函数与响应式数据

副作用函数

  • 定义
    • 指的是会产生副作用的函数
  • 示例
      • effect 函数执行时,它会设置 body 的文本内容
      • 但是除了effect 函数之外的任何函数都可以读取或设置 body 的文本内容
      • effect 函数的执行会直接或间接影响其他函数的执行
      • effect 函数产生了副作用
    • 修改了全局数据

响应式数据

  • 定义
    • 对象中某个属性的值变化后,和该属性值相关的副作用函数会自动执行,该对象就是响应式数据
  • 示例
    • 副作用函数 effect 会设置 body 元素的innerText 属性,其值为 obj.text
    • obj.text 的值发生变化时,副作用函数 effect 会重新执行

响应式数据的基本实现

实现思路

  • 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作
  • 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作
  • 拦截一个对象的读取和设置操作

实现方案

  • 当读取字段 obj.text 时,我们可以把副作用函数 effect 存储到一个“桶”里
  • 当设置 obj.text 时,再把副作用函数 effect 从“桶”里取出并执行

  • 如何才能拦截一个对象属性的读取和设置操作
    • ES2015前,通过 Object.defineProperty 函数实现,Vue2实现方式
    • ES2015+后使用Proxy,Vue3实现

实现代码

    • 创建用于存储副作用函数的桶 bucket,它是Set 类型
    • 定义原始数据 data
    • obj 是原始数据的代理对象
    • 分别设置 get 和 set 拦截函数,用于拦截读取和设置操作
      • 读取属性时将副作用函数 effect 添加到桶里bucket.add(effect),然后返回属性值
      • 设置属性值时先更新原始数据,将副作用函数从桶里取出并重新执行

设计一个完善的响应系统

构造一个更加完善的响应系统

注册副作用函数的机制

  • 原因
    • 硬编码副作用函数的名字(effect),一旦副作用函数的名字不叫 effect,代码就不能正确地工作
    • 副作用函数是一个匿名函数,也能够被正确地收集到“桶”中
  • 实现
      • 定义了一个全局变量 activeEffect,初始值是undefined,它的作用是存储被注册的副作用函数
      • 重新定义了effect 函数,它变成了一个用来注册副作用函数的函数
        • effect 函数接收一个参数 fn,即要注册的副作用函数
          • 使用一个匿名的副作用函数作为 effect 函数的参数
          • 当 effect 函数执行时,首先会把匿名的副作用函数 fn 赋值给全局变量 activeEffect
          • 接着执行被注册的匿名副作用函数 fn
          • 这将会触发响应式数据 obj.text 的读取操作
          • 进而触发代理对象Proxy 的 get 拦截函数
      • 副作用函数已经存储到了activeEffect 中
      • 在 get 拦截函数内应该把 activeEffect收集到“桶”中
      • 响应系统就不依赖副作用函数的名字了

重新设计“桶”

副作用函数与被操作字段之间没有建立明确的联系
  • 在响应式数据 obj 上设置一个不存在的属性时
      • 匿名副作用函数内部读取了字段 obj.text 的值,其与字段 obj.text 之间会建立响应联系
      • 开启了一个定时器,一秒钟后为对象 obj 添加新的 notExist 属性
      • 在匿名副作用函数内并没有读取 obj.notExist 属性的值
        • obj.notExist 并没有与副作用建立响应联系
        • 定时器内语句的执行不应该触发匿名副作用函数重新执行
“桶”的数据结构设置
  • 分析注册副作用函数
      • 代码中的角色
        • 被操作(读取)的代理对象 obj
        • 被操作(读取)的字段名 text
        • 使用 effect 函数注册的副作用函数 effectFn
      • 注册副作用函数的依赖关系分析
        • 定义
          • target 表示一个代理对象所代理的原始对象
          • key表示被操作的字段名
          • effectFn 来表示被注册的副作用函数
        • 角色关系
        • 两个副作用函数同时读取同一个对象的属性值
        • 一个副作用函数中读取了同一个对象的两个不同属性
        • 不同的副作用函数中读取了两个不同对象的不同属性
  • 代码实现
    • 用WeakMap 代替 Set 作为桶的数据结构
    • 修改 get/set 拦截器代码
        • 数据结构
          • WeakMap 由 target --> Map 构成
            • WeakMap 的键是原始对象 target
            • WeakMap 的值是一个Map 实例
          • Map 由 key --> Set 构成
            • Map 的键是原始对象 target 的 key
            • Map 的值是一个由副作用函数组成的 Set
            • 上图中的Set 数据结构所存储的副作用函数集合称为 key 的依赖集合
        • WeakMap 和 Map 的区别
          • 示例代码
              • 定义了 map 和 weakmap 常量,分别对应 Map 和WeakMap 的实例
              • 定义了一个立即执行的函数表达式(IIFE)
              • 在函数表达式内部定义了两个对象:foo 和 bar
              • 这两个对象分别作为 map 和 weakmap 的 key
              • 当该函数表达式执行完毕后
                • 对于对象foo ,它仍然作为 map 的 key 被引用着,因此垃圾回收器不会把它从内存中移除,仍然可以通过map.keys 打印出对象 foo
                • 对于对象 bar,由于 WeakMap的 key 是弱引用,它不影响垃圾回收器的工作,所以一旦表达式执行完毕,垃圾回收器就会把对象 bar 从内存中移除,并且我们无法获取weakmap 的 key 值,也就无法通过 weakmap 取得对象 bar
          • WeakMap 对 key 是弱引用,不影响垃圾回收器的工作
封装较为完善的响应系统代码
  • 技术方案
    • 将在 get 拦截函数里把副作用函数收集到“桶”的这部分逻辑封装成为track函数
      • track表达追踪的含义
    • 把触发副作用函数重新执行的逻辑封装到 trigger 函数中
  • 代码实例