最近疫情没办法开学,毕业论文写完了等着学校报道,虽然还没怎么用过 vue3,但是也可以满看看 vue3 的源码。我个人认为这类工具最让人好奇的应该就是它的响应式原理了,因此就从 reactivity 入手。
我先看了下截至目前网上的解析 vue3 的代码,感觉大体还是偏复杂,个人感觉其实没那么多必要讲那些边界条件以及各种奇奇怪怪的东西,还是应该先把主要矛盾抓住了,之后用的时候如果具体对哪一块好奇,或者说这一块在实际使用的时候出现问题不合自己意愿再直接 debug 就好。好了废话不多说,直接进入正题。
我们知道响应式的设计最根本的地方就在于劫持+订阅发布,不过在写源码之前我们先搞清楚我们想要实现什么样的功能。
let fucker
let rua
const observed = reactive({ a: 11 })
effect(() => {
console.log(9999)
fucker = observed.a
rua = observed.a
})
// 因为触发了 observer.a 的 set,因此还会触发 effect 中的函数(effect 中触发了 observer.a 的 get)
observed.a = { b: 999 }
上面的代码中最终会打印出两个 999。好,现在我们就以实现这个为目标开始写代码。
const rawToReactive = new WeakMap()
const reactiveToRaw = new WeakMap()
function isObject(val) {
return val !== null && typeof val === 'object'
}
function reactive(target) {
// weakmap 的 key 不能是 primitive value
if (!isObject(target)) {
console.error('the key of weakmap can not be primitive value ')
return target
}
// 如果传进来的 target 就是一个被监听过的 proxy
if (reactiveToRaw.has(target)) {
return target
}
// 如果该 target 已经有了对应的 proxy
let observed = rawToReactive.get(target)
if (observed !== void 0) {
return observed
}
// 设置成 reactive
observed = new Proxy(target, handlers)
rawToReactive.set(target, observed)
reactiveToRaw.set(observed, target)
return observed
}
上面的代码备注很清楚了,用了两个 weakmap 来存储映射关系,分别为原始对象到响应式对象和响应式对象到原始对象,逻辑非常简单,关键就在于 new Proxy 时的 handlers。我们接着来实现这个 handlers,因为咱们不需要管那些 map , set 等数据结构的 proxy,也不需要在乎对遍历啥的劫持(我个人感觉这就至少就省去了1/6的代码),我们只要管 set 和 get,因此实现起来还是很简单的。虽然简单,但是非常有利于我们抓住关键逻辑,或者说抓住响应式的堆栈到底是怎么走下来的。
const handlers = {
set,
get
}
function set(target, key, val, receiver) {
const oldVal = target[key]
const res = Reflect.set(target, key, val, receiver)
// 新值和旧值不相等的时候我们才触发
if (oldVal !== val) {
trigger(target, key)
}
return res
}
function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
}
上面的代码几乎不用我说,正常人都能看懂。proxy 的 get 触发的时候我们会去 track,set 触发的时候我们会去 trigger。让我们接着来实现它们。
// 举例来说,{a: 3} -> (a -> Set)。 也就是 {a: 3} 的属性 a 被一堆 effect 订阅了,这一堆 effect 被存储在 Set 里面。
const targetMap = new WeakMap()
// 表示当前需要被执行的 effect
let activeEffect
function track(target, key) {
if (activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
return
}
depsMap.get(key).forEach(fn => void fn())
}
这里要插一下,我也不知道为什么 vue3 源码里面 void 和 undefined 混用,是出于表示这里以后可能被改造成一个 function 吗,也就是将来可能会被改造成 (void function())?望知道的小伙伴告知下。
继续正题,这里比较麻烦的点就在于 activeEffect 和 targetMap 这两个结构。我已经在代码中给出了备注,因此相信阅读下来就几乎没有难度了。trigger 的时候就是取到当前的 target 的 key 对应的所有 effect,也就是订阅者,批量执行。 而 track 的时候就是将当前的 activeEffect 注入到 targetMap 中的当前依赖的 key 的对应的 set(有点绕,但就是这意思)。
需要注意的是,在实现依赖与被依赖两者的管理的时候,有一个特点就是它们应该是互相知道的,就如之前的 reactive <-> raw 的互相映射。因此这里的 activeEffect 也会存储着它订阅的对象(就是发布者)。
好了,现在就剩下一个 activeEffect 了。回想一下我们最初想要实现的功能,可以发现它实际上就基本等同于 effect 中传递的函数,因此我们在 track 函数的最后面还加了个判断,用于不会重复添加 effect,避免多次执行。
const effectStack = []
function effect(fn) {
const reactiveEffect = (...args) => {
try {
effectStack.push(reactiveEffect)
// 先将 activeEffect 置为当前的 effect
activeEffect = reactiveEffect
// fn 里面一定会去触发 getter,getter 就会去收集依赖,此时就会添加当前的 activeEffect
return fn(...args)
} catch (e) {
// 执行完毕就推出 effect 栈
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
reactiveEffect.deps = []
reactiveEffect()
return reactiveEffect
}
这一看就发现了,这是老响应式了,尤雨溪大佬的老响应式套路,跟 2.x 中一样的套路。
好了,代码到这里就把最核心的堆栈逻辑给走完了。当然 vue3 中具体的实现要比这复杂得多,例如递归 proxy 的时候为了避免出问题,是做了 lazy 的,后续等用到了再进行 reactive 化,而不是一口气进行递归实现。此外还有 shallow, readonly,computed ,markRaw , ref, stop, dep 的 clean 等许多没有这里列举完的逻辑分支功能,但核心逻辑就是上面的代码。
不过我这样做也有一个好处,因为我相信虽然现在只是 beta 版本,但是至少 3.x 中这个核心逻辑是绝对不会变的了。
除此之外,我相信来看这个的也大多和我一样是弱鸡,因此诸如 weakmap 的使用涉及到的 V8 的垃圾回收机制,使用 reflect 来设置 val 的原因(receiver 可以避免 this 问题),TS 中 unknown 的好处,infer 类型推断等等许多知识这里均跳过不提,不过想要提升的话还是需要去了解的。
最后,为了方便你复制黏贴。我直接附上所有代码
const rawToReactive = new WeakMap()
const reactiveToRaw = new WeakMap()
const targetMap = new WeakMap()
const effectStack = []
let activeEffect
const handlers = {
set,
get
}
function set(target, key, val, receiver) {
const oldVal = target[key]
const res = Reflect.set(target, key, val, receiver)
if (oldVal !== val) {
trigger(target, key)
}
return res
}
function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
return
}
depsMap.get(key).forEach(fn => void fn())
}
function track(target, key) {
if (activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
function isObject(val) {
return val !== null && typeof val === 'object'
}
function reactive(target) {
// weakmap 的 key 不能是 primitive value
if (!isObject(target)) {
console.error('the key of weakmap can not be primitive value ')
return target
}
// 如果传进来的 target 就是一个被监听过的 proxy
if (reactiveToRaw.has(target)) {
return target
}
// 如果该 target 已经有了对应的 proxy
let observed = rawToReactive.get(target)
if (observed !== void 0) {
return observed
}
observed = new Proxy(target, handlers)
rawToReactive.set(target, observed)
reactiveToRaw.set(observed, target)
return observed
}
function effect(fn) {
const reactiveEffect = (...args) => {
try {
effectStack.push(reactiveEffect)
// 先将 activeEffect 置为当前的 effect
activeEffect = reactiveEffect
// fn 里面一定会去触发 getter,getter 就会去收集依赖,此时就会添加当前的 activeEffect
return fn(...args)
} catch (e) {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
reactiveEffect.deps = []
reactiveEffect()
return reactiveEffect
}