Vue3 和 Vue2 的响应式有很大的不同,因为 Vue3 应用 Proxy 代替了 defineProperty,使得 Vue3 比 Vue2 在响应式数据处理方面有着更好的性能,更简洁高效的解决形式,还实现了诸多在 Vue2 上无奈实现的性能。此外 Vue3 的响应式库 reactivity 是一个独自的包,它能够不依赖 Vue 运行,意味着咱们能够将它运行在其余框架里。事实上,Vue3 的响应式库的实现形式以及市面上其余的大多数响应式库(如 observer-util,meteor 等)的实现形式都是相似的,Vue 也是参考这些库实现的,所以咱们还是很有必要去钻研一下的,毕竟咱也不能掉队了 ????,那么各位小伙伴们上面就跟我一起来看下这个 @vue/reactivity
到底是怎么实现的。
本文章的源码曾经发在了我的 git 上,能够返回查看:reactivity
浏览本文章之前你要先理解以下知识点
- WeakMap
- Set
- Reflect
- Proxy
下面这些有不理解的同学能够间接点链接查看具体的文档,文章外面就不再解释了。
--
咱们首先看一个应用 reactivity 的例子
// 创立一个响应式对象const state = reactive({ count: 1 })// 执行effecteffect(() => { console.log(state.count)})state.count = 2 // count扭转时执行了effect内的函数,控制台输入2
这个例子通过 reactive 创立了一个响应式对象 state,而后调用 effect 执行函数,这个函数外部拜访了 state 的属性,随后咱们更改这个 state 的属性,这时,effect 内的函数会再次执行。
这样一个响应式数据的通常实现的形式是这样的
- 定义一个数据为响应式(通常通过 defineProperty 或者 Proxy 拦挡 get、set 等操作)
- 定义一个副作用函数(effect),这个副作用函数外部拜访到响应式数据时会触发 1 中的 getter,进而能够在这里将 effect 收集起来
- 批改响应式数据时,就会触发 1 中的 setter,进而执行 2 中收集到的 effect 函数
对于 effect: effect 在 Vue 里通常叫做副作用函数,因为这种函数内通常执行组件渲染,计算属性等其余工作。在其余库外面可能叫观察者函数(observe)或其余,集体能了解到是什么意思就好,因为本篇文章是剖析 Vue3 的,所以对立叫副作用函数(effect)
依据以上的思路,咱们就能够开始入手实现了
reactive
首先咱们须要有一个 reactive 函数来将咱们的数据变为响应式。
// reactive.tsimport { baseHandlers } from './handlers'import { isObject } from './utils'type Target = objectconst proxyMap = new WeakMap()export function reactive<T extends object>(target: T): T { return createReactiveObject(target)}function createReactiveObject(target: Target) { // 只对对象增加reactive if (!isObject(target)) { return target } // 不能反复定义响应式数据 if (proxyMap.has(target)) { return proxyMap.get(target) } // 通过Proxy拦挡对数据的操作 const proxy = new Proxy(target, baseHandlers) // 数据增加进ProxyMap中 proxyMap.set(target, proxy) return proxy}
这里次要对数据做了简略的判断,要害是在const proxy = new Proxy(target, baseHandlers)
中,通过 Proxy 对数据进行解决,这里的baseHandlers
就是对数据的 get,set 等拦挡操作,上面来实现下baseHandlers
get 收集依赖
首先实现下拦挡 get 操作,使得拜访数据的某一个 key 时,能够收集到拜访这个 key 的函数(effect),并把这个函数储存起来。
// handlers.tsimport { track } from './effect'import { reactive, Target } from './reactive'import { isObject } from './utils'export const baseHandlers: ProxyHandler<object> = { get(target: Target, key: string | symbol, receiver: object) { // 收集effect函数 track(target, key) // 获取返回值 const res = Reflect.get(target, key, receiver) // 如果是对象,要再次执行reactive并返回 if (isObject(res)) { return reactive(res) } return res }}
这里咱们拦挡到 get 操作后,通过 track 收集依赖,track 函数做的事件就是把以后的 effect 函数收集起来,执行完 track 后,再获取到 target 的 key 的值并返回,留神这里是判断了下 res 是否是对象,如果是对象的话要返回reactive(res)
,是因为思考到可能有多个嵌套对象的状况,而 Proxy 只能批改到到以后对象,并不能批改到子对象,所以在这里要解决下,上面咱们须要再实现track
函数
// effect.ts// 存储依赖type Deps = Set<ReactiveEffect>// 通过key去获取依赖,key => Depstype DepsMap = Map<any, Deps>// 通过target去获取DepsMap,target => DepsMapconst targetMap = new WeakMap<any, DepsMap>()// 以后正在执行的effectlet activeEffect: ReactiveEffect | undefined// 收集依赖export function track(target: object, key: unknown) { if (!activeEffect) { return } // 获取到这个target对应的depsMap let depsMap = targetMap.get(target) // depsMap不存在时新建一个 if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 有了depsMap后,再依据key去获取这个key所对应的deps let deps = depsMap.get(key) // 也是不存在时就新建一个 if (!deps) { depsMap.set(key, (deps = new Set())) } // 将activeEffect增加进deps if (!deps.has(activeEffect)) { deps.add(activeEffect) }}
留神有两个 map 和一个 set,targetMap => depsMap => deps,这样就能够使咱们通过 target 和 key 精确地获取到这个 key 所对应的 deps(effect),把以后正在执行的 effect(activeEffect)存起来,这样在批改target[key]
的时候,就又能够通过 target 和 key 拿到之前收集到的所有的依赖,并执行它们,这里有个问题就是这个activeEffect
它是从哪里来的,get 是怎么晓得以后正在执行的 effect 的?这个问题能够先放一放,咱们前面再将,上面咱们先实现这个 set。
实现 set
// handlers.tsexport const baseHandlers: ProxyHandler<object> = { get() { //... }, set(target: Target, key: string | symbol, value: any, receiver: object) { // 设置value const result = Reflect.set(target, key, value, receiver) // 告诉更新 trigger(target, key, value) return result }}
咱们在方才的baseHandlers
上面再加一个 set,这个 set 外面次要就是赋值而后告诉更新,告诉更新通过trigger
进行,咱们须要拿到在 get 中收集到的依赖,并执行,上面来实现下 trigger 函数
// effect.ts// 告诉更新export function trigger(target: object, key: any, newValue?: any) { // 获取该对象的depsMap const depsMap = targetMap.get(target) // 获取不到时阐明没有触发过getter if (!depsMap) { return } // 而后依据key获取deps,也就是之前存的effect函数 const effects = depsMap.get(key) // 执行所有的effect函数 if (effects) { effects.forEach((effect) => { effect() }) }}
这个 trigger 就是获取到之前收集的 effect 而后执行。
其实除了 get 和 set,还有个罕用的操作,就是删除属性,当初咱们还不能拦挡到删除操作,上面咱们来实现下
实现 deleteProperty
export const baseHandlers: ProxyHandler<object> = { get() { //... }, set() { //... }, deleteProperty(target: Target, key: string | symbol) { // 判断要删除的key是否存在 const hadKey = hasOwn(target, key) // 执行删除操作 const result = Reflect.deleteProperty(target, key) // 只在存在key并且删除胜利时再告诉更新 if (hadKey && result) { trigger(target, key, undefined) } return result }}
咱们在方才的baseHandlers
外面再加一个deleteProperty
,它能够拦挡到对数据的删除操作,在这里咱们须要先判断下删除的 key 是否存在,因为可能用户会删除一个并不存在 key,而后执行删除,咱们只在存在 key 并且删除胜利时再告诉更新,因为如果 key 不存在时,这个删除是无意义的,也就不须要更新,再有就是如果删除操作失败的话,也不须要更新,最初间接触发trigger
就能够了,留神这里的第三个参数即 value 是undefined
当初咱们曾经实现了get
,set
,deleteProperty
这三种操作的拦挡,还记不记得在track
函数中的activeEffect
,那里留了个问题,就是这个activeEffect
是怎么来的?,在最开始的例子外面,咱们要通过 effect 执行函数,这个activeEffect
就是在这里设置的,上面咱们来实现下这个effect
函数。
// effect.tstype ReactiveEffect<T = any> = () => T// 存储effect的调用栈const effectStack: ReactiveEffect[] = []export function effect<T = any>(fn: () => T): ReactiveEffect<T> { // 创立一个effect函数 const effect = createReactiveEffect(fn) return effect}function createReactiveEffect<T = any>(fn: () => T): ReactiveEffect<T> { const effect = function reactiveEffect() { // 以后effectStack调用栈不存在这个effect时再执行,防止死循环 if (!effectStack.includes(effect)) { try { // 把以后的effectStack增加进effectStack effectStack.push(effect) // 设置以后的effect,这样Proxy中的getter就能够拜访到了 activeEffect = effect // 执行函数 return fn() } finally { // 执行完后就将以后这个effect出栈 effectStack.pop() // 把activeEffect复原 activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect<T> return effect}
这里次要是通过createReactiveEffect
创立一个 effect 函数,fn 就是调用 effect 时传入的函数,在执行这个 fn 之前,先通过effectStack.push(effect)
把这个 effect 推入 effectStack 栈中,因为 effect 可能存在嵌套调用的状况,保留下来就能够获取到一个残缺的 effect 调用栈,就能够通过下面的effectStack.includes(effect)
判断是否存在循环调用的状况了,而后再activeEffect = effect
设置 activeEffect,设置完之后再执行 fn,因为这个 activeEffect 是全局惟一的,所以咱们执行 fn 的时候,如果外部拜访了响应式数据,就能够在 getter 里拿到这个 activeEffect,进而收集它。
当初基本上是实现了,当初通过咱们写的这个 reactivity 库就能够实现例子中的成果了,然而还有一些边界状况须要思考,下篇文章就增加一些常见的边界状况解决。