干货说Vue3响应式实现
这个问题怎么问?
关键词: 响应式、实时更新、双向绑定
- Vue3是怎么实现响应式的?
- 谈一谈对双向绑定的理解?
一. 常见问题
-
为什么vue3要用proxy,而不是继续使用
Object.defineProperty
Object.defineProperty
不能监听到对象的新增和删除。Object.defineProperty
需要给对象每一个属性一个一个预先设置好,这就需要最开始递归所有属性,有性能负担。
二. Reactive API
主要就是四步:判断是否是对象,判断是否白名单,保证响应式实现为单例,为对象创建响应式;
其中白名单的数据类型:object、array、map、weakmap、set、weakset
三. Proxy get
Proxy get 的实现有三个重点:
-
为什么要对数组的查找方法进行包装?
- 设想一下,如果不进行包装,一个计算属性(computed effect)依赖了某个响应式数组的
indexOf()
的查找结果,proxy是这个数组的代理,当计算属性执行时,没有访问到该数组的任何一个成员,这就导致了该数组的任何一个成员都没有将computed effect作为依赖收集,那么当数组成员发生变化的时候,computed effect自然就不会得到执行,这显然不是我们的预期结果,因为我们期望让computed effect依赖于数组成员,而不是数组的indexOf方法!所以需要在使用indexOf的时候,需要Vue内部同时遍历所有数组成员,让所有数组成员也伴随收集依赖。
- 设想一下,如果不进行包装,一个计算属性(computed effect)依赖了某个响应式数组的
-
为什么设置对象的值的时候要使用Reflect?
-
当返回值是对象或者数组时,延时创建响应式
四. get track
track的实现,需要着重了解 两个变量,一个概念,一个过程。
-
两个变量
- activeEffect:当前全局唯一激活的副作用函数
- targetMap:依赖收集表,是一个weakMap, 因为原始变量一旦被删除,和它相关的所有副作用函数就都没用了,会被垃圾清除,用Map,则需要手动清理。
-
一个概念:
- effect副作用函数:effect是一个高阶函数,解决了函数在执行时,内部依赖的响应式变量不知道自己在哪个函数里被访问的问题。所有想要收集响应式依赖的函数,都需要被effect函数包裹。
-
一个过程:
- 在targetMap中为target创建Map,然后为key创建depSet,将当前全局唯一的activeEffect作为依赖add进入depSet中,之后将depSet作为target的值。形成一个target[key]的依赖收集表:
track执行流程图:
五. 什么是effect副作用函数
你需要知道:
- 为什么需要副作用函数?
- 这个辅佐用函数其实就是高阶函数,因为在函数执行时候,如果没有外面包一层的高阶函数,依赖收集的过程就不知道当前所在执行环境是什么。
- 两个全局变量:activeEffect、effectStack
- effect的执行过程: 先清除effect引用、将effect推入effectStack、activeEffect指向当前effect,执行原函数。
- 为什么要在入栈前先清除effect引用:
- 因为最新的副作用函数,比如 render effect,最新的模板已经不再依赖先前依赖的某个响应式变量。当这个响应式变量发生改变时,render effect此时就不再需要执行了。
你还需要知道:
-
reactiveEffect
函数是有options配置的,在effect执行时带入,createReactiveEffect
时候赋值给reactiveEffect.options
:-
lazy:默认true,也就是在执行effect的时候,只会创建并返回reactiveEffect,不会执行reactiveEffect。为false时,不仅返回,还会当即执行reactiveEffect。
TIP:在创建computed Effect的时,lazy为true,表示需要延迟执行。
-
scheduler:调度器,是一个函数,默认没有scheduler,主要在trigger后,通过run触发reactiveEffect执行时,如果有reactiveEffect.option.schedule存在,就会执行它的scheduler,否则就会执行reactiveEffect本身。
-
effect流程图
六. Set trigger
重点需要知道:
- 从 depset 中取出所有 effect,并用执行器 run 遍历执行的过程。
- effect函数有调度scheduler的概念,调度器 run 会先判断是否有调度。
七. Ref Api
Ref api 做的事情非常简单,重点知道它解决的两个问题:
- 因为基本类型不能通过proxy直接监听,所以需要给基本类型包一层对象,然后通过给这个对象value属性设置getter 和 setter的方式去监听。getter就是去track,set就是去trigger。
- 如果是对象或者数组或者是WeakMap等...响应式白名单中的数据类型,那么value就会直接转换成reactive的返回结果。
下面是Ref Api的大体实现:
function ref(value) {
return createRef(value)
}
const convert = (val) => isObject(val) ? reactive(val) : val
function createRef(rawValue) {
if (isRef(rawValue)) {
// 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
return rawValue
}
// 如果是对象或者数组类型,则转换一个 reactive 对象。
let value = convert(rawValue)
const r = {
__v_isRef: true,
get value() {
// getter
// 依赖收集,key 为固定的 value
track(r, "get" /* GET */, 'value')
return value
},
set value(newVal) {
// setter,只处理 value 属性的修改
if (hasChanged(toRaw(newVal), rawValue)) {
// 判断有变化后更新值
rawValue = newVal
value = convert(newVal)
// 派发通知
trigger(r, "set" /* SET */, 'value', void 0)
}
}
}
return r
}
转载自:https://juejin.cn/post/7372031054818410515