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})
// 执行 effect
effect(() => {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.ts
import {baseHandlers} from './handlers'
import {isObject} from './utils'
type Target = object
const 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.ts
import {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 => Deps
type DepsMap = Map<any, Deps>
// 通过 target 去获取 DepsMap,target => DepsMap
const targetMap = new WeakMap<any, DepsMap>()
// 以后正在执行的 effect
let 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.ts
export 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.ts
type 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 库就能够实现例子中的成果了,然而还有一些边界状况须要思考,下篇文章就增加一些常见的边界状况解决。