这是个人剖析 Vue 3 原理的第一篇文章。这篇将会带着你们学习数据响应相关的内容,而且尽量的脱离源码来了解原理,下降你们的学习难度。vue
Vue 3 目前的状态其实很适合阅读,由于代码量很少,而且核心功能是不会有什么大的变更的。react
所以笔者 fork 了目前的源码,而且加以注释。同时为了照顾不怎么熟悉 TS 的人群,笔者也对一些核心的 TS 语法作了解释。git
这份注释不是干巴巴的只对一行代码说明是干什么的,而是结合了上下文来说解它的用处。若是你想读源码可是又怕看不懂的话,能够经过我这个 仓库 来学习。github
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
内部的核心代码简化以下:ui
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
}
}
复制代码
先来解释下三者究竟是什么,这个很重要:
这里笔者把这些内容脱离源码串起来说一下流程。
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
时才会执行回调,而后接下来的操做就和上面的没区别了。
以上是数据响应核心流程的讲解,内容很少,可是你通读源码之后也就是这样一个流程。
若是你对源码有兴趣的话,就结合我这个 仓库 来对照这篇文章吧。
阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流。
另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。
最后,若是你对源码研究也有兴趣或者有问题想问的,能够进群交流。