Vue.js3 原理剖析:数据响应系统
Vue 3 目前的状态其实很适合阅读,因为代码量不多,并且核心功能是不会有什么大的变动的。
因此笔者 fork 了目前的源码,并且加以注释。同时为了照顾不怎么熟悉 TS 的人群,笔者也对一些核心的 TS 语法做了解释。
这份注释不是干巴巴的只对一行代码说明是干什么的,而是结合了上下文来讲解它的用处。如果你想读源码但是又怕看不懂的话,可以通过我这个 仓库 来学习。
先导知识
Vue 3 代码的写法有了很大的变化,如果你还不清楚这方面的内容,推荐先阅读 Vue Function-based API RFC
数据响应机制
众所周知,在 Vue 3 中使用了 Proxy
替换了原先的 Object.defineproperty
来实现数据响应。
另外如果你不熟悉 Proxy 的用法,推荐先阅读 文档。
我们先来学习下如何使用这个 API 吧。
const value = reactive({ num: 0 })
// 需要注意的一点,这个回调中用到了 value.num
// 那么只有当外部给 value.num 赋值才会触发回调
effect(() => {
console.log(value.num)
})
value.num = 7
复制代码
很简单,上述代码就实现了数据的响应式,并且能在数据改变以后执行相应的回调。
reactive
内部的核心代码简化如下:
function reactive(target) {
if (!isObject(target)) {
return target
}
if (!canObserve(target)) {
return target
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
return observed
}
复制代码
首先判断传入的参数类型是否可以用于观察,目前支持的类型为 Object|Array|Map|Set|WeakMap|WeakSet
。
接下来判断参数的构造函数,根据类型获得不同的 handlers
。这里我们就统一使用 baseHandlers
,因为这个已经覆盖 99% 的情况了。只有 Set, Map, WeakMap, WeakSet
才会使用到 collectionHandlers
。
对于 baseHandlers
来说,最主要的是劫持了 get
和 set
行为,这两个行为同时也能原生劫持数组下标修改值及对象新增属性的行为,这两个行为相关的内容会在下文中说到。
最后就是构造一个 Proxy
对象完成数据的响应式。相比 Object.defineproperty
一开始就要递归遍历整个对象的做法来说,使用 Proxy
性能会好得多。
接下来当我们去使用 value
这个对象的时候,就能劫持到内部的行为。
比如说 console.log(value.num)
就会触发 get
函数;value.num = 2
就会触发 set
函数。
以下是这两个函数的核心剖析:
function get(target: any, key: string | symbol, receiver: any) {
// 获得结果
const res = Reflect.get(target, key, receiver)
track(target, OperationTypes.GET, key)
// 判断是否为对象,是的话将对象包装成 proxy
return isObject(res) ? reactive(res) : res
}
复制代码
对于 get
函数来说,获取值肯定是最核心的一步骤了。接下来是调用 track
,这个和 effect
有关,下文再说。最后是判断值的类型,如果是对象的话就继续包装成 Proxy
。
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
const result = Reflect.set(target, key, value, receiver)
if (是否新增 key) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
return result
}
复制代码
对于 set
函数来说,设置值是第一步骤,然后调用 trigger
,这也是 effect
中的内容。
简单来说,如果某个 effct
回调中有使用到 value.num
,那么这个回调会被收集起来,并在调用 value.num = 2
时触发。
那么怎么收集这些内容呢?这就要说说 targetMap
这个对象了。它用于存储依赖关系,类似以下结构,这个结构会在 effect 文件中被用到
{
target: {
key: Dep
}
}
复制代码
先来解释下三者到底是什么,这个很重要:
- target 就是被 proxy 的对象
- key 是对象触发 get 行为以后的属性。比如 counter.num 触发了 get 行为,num 就是 key
- dep 是回调函数,也就是 effect 中调用了 counter.num 的话,这个回调就是 dep,需要收集起来下次使用
这里笔者把这些内容脱离源码串起来讲一下流程。
const counter = reactive({ num: 0 })
effect(() => {
console.log(counter.num)
})
counter.num = 7
复制代码
首先创建一个 Proxy
对象,targetMap
会把这个对象收集起来当做 key。
接下来调用 effect 回调的时候会把这个回调保存起来,用于下面的依赖收集。在调用的过程中会触发 counter
的 get
函数,内部调用了 track
函数,这个函数会使用到 targetMap
。
这里首先通过 target
从 targetMap
中取到一个对象,这个对象也就是 target
所有的依赖关系。那么对于 counter.num
来说,num
就是这个对象的 key(这里如果有点模糊的话可以先看下上面的数据结构),值是一个依赖回调的集合,因为 counter.num
可能会被多个地方依赖到。
回调执行完毕以后会把保存的回调销毁掉。
当我们调用 counter.num = 7
时,触发 set
函数,内部调用 trigger
函数,同样会使用到 targetMap
。
同样通过 target
取到一个对象,然后通过 key 也就是 num
去取出依赖集合,最后遍历这个集合执行里面所有的回调函数。
另外对于 computed
来说,内部也是使用到了 effect
,无非它的回调不会在调用 effect
后立即执行,只有当触发 get
行为以后才会执行回调并进行依赖收集,举个例子:
const value = reactive({ num: 0 })
const cValue = computed(() => value.num)
value.num = 1
复制代码
对于以上代码来说,computed
的回调永远不会执行,只有当使用到了 cValue.value
时才会执行回调,然后接下来的操作就和上面的没区别了。
作者:yck
链接:https://juejin.im/post/5d996e3e6fb9a04e3043cc5b
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。