vue3 响应式
这节着重探究vue3 是如何实现响应式的,以及和vue2响应式的区别。
vue3响应式的改进
- proxy对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
vue3 响应式的实现
在介绍vue3响应式实现中,会穿插关于vue2响应式的回忆。
vue3响应式实现的关键在于如何才能拦截一个对象属性的读取和设置操作。在 ES2015 之前,只能通过 Object.defineProperty 函数实现,这也是 vue2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现,这就是 vue3 所采用的方式。
一个响应系统的工作流程如下: ● 当读取操作发生时,将副作用函数收集到“桶”中; ● 当设置操作发生时,从“桶”中取出副作用函数并执行。
对应到代码中流程:
- 定义副作用函数
- 执行副作用函数,触发读取
- 后续修改响应式数据
明确工作流程后有几个概念需要定义出,以免混淆:
- 依赖集合:收集副作用函数的集合
- 副作用函数的deps属性:一个数组,用来存储所有包含当前副作用函数的依赖集合;当从所有元素中删除当前副作用函数时,该属性长度也就变为0
一个好的思路需要通过合适的数据结构来实现:
分别使用了WeakMap, Map, Set
● WeakMap 由 target --> Map 构成; ● Map 由 key --> Set 构成。
从功能上,有两点因素:
- 要针对不同的对象收集副作用函数
- 要针对对象不同的属性收集副作用函数
那么为什么要使用weakMap呢?这涉及到weakMap和Map的区别
简单地说,WeakMap 对 key 是弱引用,不影响垃圾回收器的工作,当target不再需要时,尽管WeakMap对key有引用,target同样会被回收,相应的键和值都不能访问;但Map会影响垃圾回收,导致尽管对target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。
03 分支切换问题可能会导致不必要的更新
如何避免不必要的更新 每次副作用函数执行时,先把它从所有与之关联的依赖集合中删除。 维护了一个deps属性,该属性是一个数组,用来存储所有包含当前副作用函数的依赖集合: 原来的副作用函数:
let activeEffect
function effect(fn){
activeEffect = fn
fn()
}而要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它,因此需要重新设计副作用函数
let activeEffect
function effect(fn){
// 为了不污染fn用户函数,包装了一层再赋予deps属性
const effectFn = () => {
activeEffect = fn
fn()
}
effectFn.deps = []
effectFn()
}04 嵌套的effect与effect栈
什么情况下会发生嵌套呢? 当在一个组件内渲染另外一个组件时,会发生嵌套。
在嵌套的情况下,因此需要维护一个栈使得栈顶元素永远是当前属性所对应的副作用函数。
05 避免无限递归循环
在trigger的同时,副作用函数也在执行时,导致无限递归调用自身,栈溢出。
06 可调度性
调度执行的意义在于当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。 次数上可以获取更好的性能,比如Vue.js 中连续多次修改响应式数据但只会触发一次更新,实际上 Vue.js 内部实现了一个更加完善的调度器。 具体的实现方式就是给定一个选项参数,设置一个scheduler属性,用户可以自定义这个属性的值,trigger中会去调用它!
07 计算属性 computed 和 lazy
基于上述的介绍就可以实现一个非常重要且有特色的能力-计算属性
- proxy对象实现属性监听
- 副作用函数
- scheduler调度器?
08 09 watch的实现原理
过期的副作用
proxy 和 reflect
proxy
proxy的定义:使用proxy可以创建一个代理对象。代理又是指的什么呢,指的是对一个对象基本语义的代理。什么是基本语义呢,基本语义指的就是对对象进行的一些基本操作,比如读取属性值、设置属性值。不过proxy只能够拦截对一个对象的基本操作;创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是直接指定被代理对象的内部方法和行为的。
proxy是一个异质对象,常规对象和异质对象的分类就是对象内部部署到方法是否按照具体ECMA规范实现
访问对象属性时做一次拦截,而且代理的是整个对象 相对于vue2中的define, 它的性能更好,不用循环,浏览器性能优化
reflect
源码实现中为什么使用Reflect方法,
- 正确处理this指向:当使用 Proxy 拦截对象操作时,如果直接在 handler 中使用原始对象方法,this 的指向可能会出现问题。Reflect 方法可以确保正确地将操作转发给代理对象,并保持正确的 this 上下文;
- 保持操作的原始行为和返回值:Reflect 方法与对应的 Object 方法行为一致,但提供了更可靠的返回值,使得在拦截操作时能够正确地模拟原始行为;
- 处理继承关系:当代理对象涉及到原型链时,Reflect 方法可以正确处理属性访问和方法调用,确保继承关系得到正确维护;
- 简化代码和提高一致性;
- 处理特殊情况和边界条件。
如何代理Object
需要考虑到所有针对对象的读取行为。
代理数组
数组也是一个异质对象,是因为数组对象的 [[DefineOwnProperty]] 内部方法与常规对象不同。 数组的length属性被修改后,for...in循环对数组的遍历结果就会改变。
代理集合类型
集合类型包含Map、Set、WeakMap、WeakSet。源码中会区分目标对象的类型,再根据类型指定对应的代理处理器。 使用 Proxy 代理集合类型的数据不同于代理普通对象,因为集合类型数据的操作与普通对象存在很大的不同。
访问器属性的含义:属性具有setter和getter两个方法,通过这两个方法来存取值。