Vue 3 原理剖析:数据响应系统

这是个人剖析 Vue 3 原理的第一篇文章。这篇将会带着你们学习数据响应相关的内容,而且尽量的脱离源码来了解原理,下降你们的学习难度。vue

文章相关资料

Vue 3 目前的状态其实很适合阅读,由于代码量很少,而且核心功能是不会有什么大的变更的。react

所以笔者 fork 了目前的源码,而且加以注释。同时为了照顾不怎么熟悉 TS 的人群,笔者也对一些核心的 TS 语法作了解释。git

这份注释不是干巴巴的只对一行代码说明是干什么的,而是结合了上下文来说解它的用处。若是你想读源码可是又怕看不懂的话,能够经过我这个 仓库 来学习。github

先导知识

Vue 3 代码的写法有了很大的变化,若是你还不清楚这方面的内容,推荐先阅读 Vue Function-based API RFC数组

数据响应机制

众所周知,在 Vue 3 中使用了 Proxy 替换了原先的 Object.defineproperty 来实现数据响应。markdown

另外若是你不熟悉 Proxy 的用法,推荐先阅读 文档数据结构

咱们先来学习下如何使用这个 API 吧。函数

const value = reactive({ num: 0 })
// 须要注意的一点,这个回调中用到了 value.num
// 那么只有当外部给 value.num 赋值才会触发回调
effect(() => {
  console.log(value.num)
})
value.num = 7
复制代码

很简单,上述代码就实现了数据的响应式,而且能在数据改变之后执行相应的回调。oop

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 来讲,最主要的是劫持了 getset 行为,这两个行为同时也能原生劫持数组下标修改值及对象新增属性的行为,这两个行为相关的内容会在下文中说到。

最后就是构造一个 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 回调的时候会把这个回调保存起来,用于下面的依赖收集。在调用的过程当中会触发 counterget 函数,内部调用了 track 函数,这个函数会使用到 targetMap

这里首先经过 targettargetMap 中取到一个对象,这个对象也就是 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 时才会执行回调,而后接下来的操做就和上面的没区别了。

最后

以上是数据响应核心流程的讲解,内容很少,可是你通读源码之后也就是这样一个流程。

若是你对源码有兴趣的话,就结合我这个 仓库 来对照这篇文章吧。

阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流。

另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。

最后,若是你对源码研究也有兴趣或者有问题想问的,能够进群交流。