副作用函数与响应式数据
认识副作用函数与响应式数据
副作用函数
- 定义
-
- 指的是会产生副作用的函数
- 示例
-
-
- 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 函数中
- 代码实例