响应系统
副作用函数
- 副作用:某条语句的执行,会影响其他语句的结果
- 副作用函数:函数的执行会影响其他函数的执行
响应式数据
- 修改数据的值时,除了值本身变化之外,与该值相关的语句全部重新执行
- Vue2中,我们通过defineProperty来实现对象属性的响应式
- Vue3中,我们通过Proxy来实现对象属性的响应式
响应系统的实现
- 当读取数据时,将副作用函数收集到桶中
- 为什么读取数据的函数会成为副作用函数呢?
- 读取操作发生时,会触发属性的getter方法,而getter方法内可能执行了某些操作,读写均会使其成为副作用函数
- 当设置数据时,从桶中取出副作用函数执行
- 副作用函数的存储结构:
- 使用weakmap在原始对象和map实例建立映射
- 使用map在属性和副作用函数set建立映射
- 使用set收集属性对应的副作用函数
特殊情况处理
-
副作用函数内存在分支语句
- 问题:根据给定条件的不同,会执行不同的分支,那么随着条件的变化,会将当前的副作用函数绑定到涉及的所有属性上,但是给定条件一旦不再变化,那么只会执行固定分支,其余分支属性修改不应该再触发当前副作用函数的重新执行。
- 解决方法:每次取出执行副作用函数前,先将当前副作用函数从所有保存过当前副作用函数的set中删除,然后重新执行副作用函数,这样就能保证每次执行副作用函数时,总是最新有效的依赖关系。
- 实现:在当前副作用函数身上添加deps属性,每次被track成功后,将执行了track的set集合保存到deps中。在执行真正的副作用函数前,先获取deps,遍历所有set,并移除当前副作用函数。
- 问题:虽然可以解决副作用函数的遗留问题,但是trigger遍历执行当前属性相关的副作用函数时,每个副作用函数,都会将自己从集合移除,在添加,导致遍历无限循环。
- 解决方法:trigger不直接操作set集合,而是克隆一份,遍历克隆的set,这样既能保证无效的副作用不被执行,也能完成遍历所有当前副作用函数的需求
-
嵌套的副作用函数
- 问题:当前副作用函数并不支持嵌套调用,因为我们在全局采用了唯一变量activeEffect来保存当前的副作用函数,一旦发生嵌套,内层会对activeEffect重新赋值,导致外层副作用函数的引用丢失。然而嵌套的副作用函数很常见,比如渲染函数的嵌套调用
- 解决方法:保存当前副作用函数到栈中,每次绑定取栈顶元素即可
-
避免无限递归循环
- 问题:在一个副作用函数内,同时对一个数据进行读写,那么读取时,将当前副作用函数收集。赋值时将当前副作用函数取出执行,但此时当前副作用函数并未执行完,就又在内部重新执行,导致了无限递归
- 解决方法:取出执行副作用函数时,与当前所在函数进行比较,如果为同一函数则不执行
-
调度执行
- 问题:在trigger重新执行副作用函数时,我们无法控制其何时执行,如何执行。不够灵活
- 解决方法:在副作用包装函数中传入配置项参数schedule,trigger函数遍历执行副作用函数时,将当前的副作用函数传入schedule中。供用户控制
- 设置定时器控制其执行时机,设置JobQueue控制其执行次数
计算属性和lazy
-
调用computed函数,传入getter运算函数,其创建并返回一个对象,该对象属性value为访问器属性,值为传入的getter
-
computed返回对象的value属性被称为计算属性。与直接调用getter获取结果的不同在于,getter在computed内部被作为了副作用函数,实现了computed返回对象value属性的响应式
- 内部将收到的getter作为副作用函数传入副作用包装函数,并传入lazy配置项。
- 保存副作用包装函数的返回的副作用函数
- 创建obj对象,并设置其访问器属性value,值为副作用函数的执行结果
- 将该obj对象返回
- 我们读取计算属性的值时,本质是读取obj的访问器属性value,其结果为副作用函数的执行结果
- 除了在需要的时候拿到其结果,我们也需要对结果进行缓存来提高性能
- 在computed中维护value保存计算结果,维护dirty保存更新标志,副作用包装函数传入调度器,在副作用函数被trigger执行时,将dirty改为需要更新。则可以执行并获取最新值,否则使用旧值即可
-
在副作用函数内访问计算属性时,会发现对于计算属性依赖的属性的修改,不会引起当前副作用函数的更新
- 因为发生了副作用函数的嵌套,computed收到的getter,会被包装为副作用函数,计算属性依赖的修改只会引起getter包装函数的响应式,对于外层没有收集。
- 可以通过在读取计算属性的值前手动调用track函数来收集,在getter被重新执行时,在schedule内手动调用trigger进行触发外层的副作用函数。
watch
-
调用watch函数,传入对象,或者getter函数,其会对getter函数内部依赖的属性,或者传入对象的所有属性进行监视,一旦发生改变,在schedule内部去执行watch的第二个回调函数,也是利用了副作用函数的响应式
- 执行回调时,schedule会给回调传入oldValue和newValue
- 给副作用包装函数传入lazy配置项,这样就能在执行副作用函数后获得新值并保存
-
watch配置项
- immediate来指定回调函数是否需要立即执行,在watch内判断,若为true,则先执行一次回调即可
- flush来指定回调函数的执行时机,post放入微任务,pre组件更新前
-
watch执行回调函数时,除了传入oldValue和newValue外,也会传入第三个参数OnInvalidate函数
- 我们可以给OnInvalidate传入回调来控制watch的expired标志位
- OnInvalidate会在副作用函数重新执行前被调用,进而改变标志位
- 这样当副作用函数重复执行时,可通过expired来判断当前副作用函数是否过期,保证始终获取最新数据