Vue 3.0 源码分析-数据侦测

  • 作者:robertren
  • 时间:2021-01-09
  • 574人已阅读
导语:作为前端三大主流框架之一的Vue,其作为一个构建数据驱动的渐进式框架,由于其易用,灵活且高效的特性,在国内外一直受到很多前端开发者追捧。

Vue 3.0 源码分析-数据侦测

作为前端三大主流框架之一的Vue,其作为一个构建数据驱动的渐进式框架,由于其易用,灵活且高效的特性,在国内外一直受到很多前端开发者追捧。

2020年前端大事件之一,Vue 3.0终于正式发布了。作为一个大的版本更新,Vue 3 与 Vue 2相比,实现原理,使用方式等均有着不小的改动。本文主要会介绍讲述二块内容,分别是Vue 3.0 的简要介绍,Vue 3.0 数据侦测源码分析。小伙伴们可以根据自己的需求,查看对应的内容,也欢迎各位一起探讨,一起学习。

Vue 3.0 简要介绍

Vue 3 的 “前世今生”

2016年,Vue 2.0 正式发布,时至今日,已经过去了四年的时光。诚然,在这四年中,Vue 2的社区建设一直呈现出一副蓬勃向上的态势,各种第三方包,工具层出不重。但是,在这四年中,前端技术飞速发展,typescript,lerna等新技术新理念变的越来越流行,越来越普及,如何接入并使用这些,成为了vue开发者的一个问题。尤大和他的小伙伴们自然也发现了这个问题。同时,伴随着ES6的标准化,主流浏览器普遍提供了新的JavaScript功能支持,而伴随着时间的迁移,前端应用的处境越来越复杂,要求也不断发生变化,之前 Vue 2 代码库中设计和体系结构问题逐渐暴露了出来,加上其他的种种期望和要求,Vue 3的研发发布被提上了日程。整个研发阶段,大致可以分为以下四个阶段:

  • Prototype 原型设计(2018/02 ~ 2018/09)

    这一阶段主要是对于TypeScript, lerna等技术尝试使用;

    里程碑:实现了 mini 版的 v-dom 运行时环境与独立的基于Proxy实现的响应式API;

  • Exploration 探索阶段(2018/09 ~ 2019/05)

    这一阶段主要是尝试 classify API,Typescript ,Reactive Hooks 与 Time Slicing,研究新的 render 策略;

    里程碑:推出了 class API RFC;

  • Pivot 核心开发阶段(2019/05 ~ 2019/08)

    放弃了 class API,重写了render逻辑等;

    里程碑:推出 Function API RFC 与 Composition API RFC;

  • Feature Parity

    这一阶段主要是支持 Vue 2 API,对于 v-model,transition system,SFC 单文件组件 HMR 热更新等进行优化;

Vue 3 vs Vue 2

作为一个大的版本更新,Vue 3 相比于 Vue 2,自然有着不小的变化;简单点来说呢,主要可以分为以下几个新的特性:

  • Vue 3 使用 TypeScript 开发实现,便于后续维护优化;同时,也能更好的支持用户接入 TypeScript;接入 TypeScript,可以实现强类型检测,提供错误警告与更好的调试支持;

  • 内部模块解耦:使用 monorepo 的项目管理架构,接入 lerna来实现项目 monorepo 化,每个 package 有自身独立的API,类型定义与测试等;查看 vue-next 项目的 packages 文件夹结构,我们可以发现仓库模块化结构如下:

image20201212163402159.png

各个packages的功能作用如下:

  • compiler-core:与平台无关的编译器,除了包含可扩展的基础功能,也包含各种插件;

  • compiler-dom:依赖于 compiler-core,是针对浏览器环境开发的编译器;

  • compiler-sfc:依赖于 compiler-core 与 compiler-dom,编译单文件组件;

  • compiler-ssr:依赖于 compiler-dom,服务端渲染编译器;

  • reactivity:数据响应式系统,作为一个独立的系统,其可以搭配任何框架使用,也是本文后续着重讲解的内容;

  • runtime-core:与平台无关的 runtime,其支持虚拟 DOM 渲染器,Vue 组件和各种 API;

  • runtime-dom:针对浏览器的 runtime,支持处理原生 DOM,API,事件等;

  • runtime-test:为了测试编译而写的轻量级 runtime,由于其渲染出来的 DOM 对象实际上是一个 JS 对象,故而可以运行在所有的 JS 环境中。除了用于测试渲染是否正确外,还可以用来序列化 DOM,触发 DOM 事件或记录某次更新中的 DOM 操作;

  • server-renderer:服务端渲染;

  • shared:内部使用公共方法与常量;

  • size-check:私有包,包大小检查;

  • template-explorer:浏览器端实时编译组件,返回render方法;

  • vue:引用上述 runtime 与 compiler,构建完整版 Vue;

  • global.d.ts:全局类型声明文件,了解 TypeScript 的同学一定不会陌生;

  • Composition API:

    组合式 API 可以说是 Vue 3 最重要的几个更新之一。在介绍组合式 API 的优点之前,首先简单介绍一下什么叫做组合式 API,什么叫 选项式 API。

    无论 Vue 2 还是 Vue 3,我们新开发一个组件的时候,该组件的组成都可以分为3个部分,分别是 template 模板,script js代码,style css这三个部分。组合式 API 与 选项式 API的区别简单表面的理解就是 script 部分代码编写风格的区别。

    在 Vue 2 中,如果我们需要写一个 tag 选择的组件,我们的写法可能如下:

image20201212175212041.png

而在 Vue 3中,我们则可以这样写:

image20201212175349672.png

其中, setup 相当于 Vue 2 中的beforeCreate 与 created。除此之外,组合式 API 还新增了生命周期钩子函数,如onBeforeMount, onMounted等。在简单了解了写法之后,我们再来聊聊为什么说组合式 API 要优于之前的选项式 API。在使用选项式 API 时,当我们组件的功能越来越复杂,我们同一代码逻辑将会分散在 components,props,data,computed,methods 和生命周期函数中,因此降低了代码的可阅读性和可维护性。想想吧,当我们开发复杂需求的时候,从如山一般的代码中找到自己想要修改的变量后,又要回头去 methods 中找对应的函数,再去生命周期函数中看调用的场景,是不是很痛苦?但是使用组合式 API 就可以很好的解决这个痛点。除此之外,使用组合式 API 创建的组件,复用时更加方便,相比于Mixins和Scoped插槽更灵活。具体的使用等,由于本文篇幅限制,在此不多加以赘述,推荐各位去官网查看文档食用。

  • 更快更小

    Vue 3 相比于 Vue 2 的优化达到什么程度呢?看了这组数据,相信你就明白了:

    • 资源大小:- 41%
    • 初始渲染:+ 55%
    • 更新速度:+ 33%
    • 内存占用:- 54%

    那么,Vue 3 相比于 Vue 2为什么能有这么大的性能优化呢?

    首先聊一聊资源大小是如何进行优化的。众所周知,框架构建产物的大小作为评价其性能的指标之一,一直是我们web应用开发关注的重点。我们可以注意到,在不同的需求场景中,我们所使用的框架所提供的能力并不相同。如果我们在构建打包的时候,把我们没有用到的代码一起打包进去,就造成了不必要的资源损耗。而从框架层面来说,由于不同需求的差异性,其不可能提供所有的能力支持,如果那样会导致框架变的越来越大,而其中很多的代码都是只有特定场景才会用到;所以就框架层面来说,更倾向于包含大多数用户需要用到的功能。综合以上两点,Vue 3 实现了 Tree Shaking,也就是让我们在构建的时候剔除我们未使用的代码,只打包我们期望使用的代码。Vue 3 借助于全局 API 和 ES Modules 实现了这一点。最终的成果就是,尽管 Vue 3增加了很多新的功能,但是,最终生成的基础版(也就是必须的部分)的大小,仅仅只有大约 10KB,不到 Vue 2 的大小的一半。

    至于渲染优化,Vue 3 尝试了新的渲染策略。在 Vue 2 中,Vue 会生成一个虚拟 DOM 树,我们每次修改之后,又会生成一个新的虚拟 DOM 树,Vue 通过递归遍历的方式,对这两个虚拟 DOM 树上的每一个节点的属性进行对比,判断是否需要更新。说实话,这个算法算是我们刷算法题时的”暴力解法“,缺点很明显,尤其是当我们仅有一点发生改变,却需要递归整个 DOM 树;但是得益于现代 JavaScript 引擎的优化策略,这个算法实际的表现还是很快的。如何进行优化呢?呼之欲出的做法自然就是去除不必要的递归与节点属性比较操作。为了实现这一点,就需要 compiler 和 runtime 协同合作:compiler 编译分析模板的同时,生成优化提示,或者说打上标记,也就是 PatchFlags;而 runtime 收集这些提示,并且根据这些提示进行优化。优化工作主要可以分为以下三点:

    • 树的层面上,引入 Block 块的概念,
    • 静态提升
    • 元素级别,使用 PatchFlags 作为优化标志

    正如上面所说,Vue 3 的优化策略其中很重要的一点就是去除不必要的递归与节点属性比较操作。为了实现这个目的,简单点来说,就是希望只对比动态内容,例如:

    <div>
    	<div>Hello World</div>
    	<div><p>Hello World</p></div>
    	<div>{{ message }}</div>
    </div>
    

image20201213174813321.png

很明显,只有 message 所在的 div 是动态的,靶向更新该节点即可。问题是如何从整个 DOM 节点树中,定位到这个节点呢?这个时候,就轮到patchFlags出场了。Vue 3 在 compiler 时,分析模板并提取有效信息,Vue 3 根据这些信息,在创建 VNode 的时候,打上标记,PatchFlags = 1,也就是上图中下发红框处。通过 PatchFlags,Vue 3就可以在 VNode 创建阶段,将所有的动态节点提取出来,并统一存放在一个数组内,也就是 dynamicChildren。说到这里,就不得不提到 Block 块的概念。其实简单理解,一个 Block 就是一个拥有特殊属性的 VNode,其中,就包括 dynamicChildren。上文中的最外层 div,就是一个 Block,它的 dynamicChildren 数组中,会包含其所有子代的动态节点。当我们更新这一个块中的节点时,就不需要再递归遍历整个虚拟 Dom 节点树,跟踪这个块中的动态节点即可。Block Tree 也是以此为基础。当然,一个 Block 是无法组成 Block Tree 的,一个虚拟 DOM 节点树中会有多个 VNode 作为 Block构成Block Tree 。这里,就不得不提到 v-if 条件渲染和 v-for 循环渲染了。

首先说一下 v-if 条件渲染。由于 dynamicChildren 的 diff 判断是忽略了 VDom 树的层级的,如果不做任何处理的话,其只会返回并告知其子树中变化的节点,而忽略其他的。说起来可能有点晦涩,还是看下面的代码能更好的理解:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <div v-else>
    <p>{{ a }}</p>
  </div>
</div>

// 不做处理时,无论真假,此时收集到的动态节点为
cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: ctx.a, patchFlag: 1 }
    ]
}

从这里我们可以看出,尽管 p 标签的父标签已经发生了变化,但是并没有被收集到,这样就会导致 DOM 结构不稳定的问题。Vue 3是如何解决这个问题的呢?Vue 3 将 v-if , v-else 对应的标签都当做一个 Block,从而构成 Block Tree,dynamicChildren 在收集动态节点的时候,也会收集 Block,实际的结果如下:

image20201213213508710.png

说完 v-if,再来聊聊 v-for,其实思路是一致的,解决方法就是是 v-for 的元素作为一个 Block 即可。

image20201213213947501.png

具体的优化策略,可以参考这篇文章:Vue3 Compiler 优化细节,如何手写高性能渲染函数,讲的很详细。

  • 新增 Teleport,provide/inject,suspence等

    Vue 3 还新增了 Teleport, provide/inject,suspence 等方便我们日常的开发,具体的使用参考官方文档即可,本文就不越俎代庖了。

  • vite

    说的 Vue 3 就绕不过去的东西还有一样,就是 Vite,毕竟尤大自己都发推宣布用了之后就再也回不去 webpack 了。vite 和 webpack最大的区别,或者说感觉最直接的区别,正如其名,"快"。Vite 是一个基于浏览器原生 ESM 的开发服务器,它在服务器端实现按需编译返回,跳过了打包的过程,从而实现快速冷启动,即时模块热更新,按需编译等能力。Vite 除了搭配 Vue 3使用外,还提供 react,preact 的模板。要讲 Vite,就不得不先提一下什么叫 ESM。

    ESM,script module 是 ES 模块在浏览器端的实现。目前主流的浏览器都已经支持。其最大的特点就是可以在浏览器端,使用 import,export 的方式导入和导出模块。为了支持这样导入导出模块,需要我们在 script 标签中设置 type="module"。

    <script type="module">
      import { createApp } from './main.js‘;
      createApp();
    </script>
    

    浏览器识别到该 script 标签后,会将其当做 ES 模块,会发起 http 请求去获取其中 import 的内容,拿到的就是 main.js export 导出的 createApp 函数,这就是 Vite 实现上述种种特性的基本原理。当然,实际的实现不可能如此简单,不然我们之间使用浏览器的 ESM 不就好了,Vite 还做了一些其他的工作。

    首先,正如上文所说,在浏览器中使用 ESM 是通过 http 请求获取 import 的内容的,所以,我们必须得有一个本地服务器,去对这些资源模块进行代理,不然浏览器是无法直接请求得到这些本地的模块的,Vite 就是通过 koa 启动了一个 web server,代理了这些模块(源码地址)。

image20201213224031851.png

但是仅仅启动服务还是远远不够的,在平时的项目开发过程中,我们都会安装使用各种依赖。在使用中,我们往往只需要写 import xxx from xxx 这种代码,就可以引入并使用,剩下的工作,也是找到这些文件,并将之打包进产物中,都有 webpack 帮我们做好了。但是由于 Vite 使用的是浏览器的 ESM,浏览器并不知道我们这些依赖会安装在 node-modules 里面,它只会根据相对/绝对定位来找这些文件,这自然是无法找到的。所以,为了避免找不到模块导致的问题,vite 需要自己去拦截浏览器对模块的请求并且返回处理后的结果。Vite 写了一个 koa 中间件,拿到 import 资源的信息后,判断其请求的是不是 npm 模块,如果是,则会对 import xxx from xxx 这种语法进行处理,给它加上一个 @modules 的路径前缀,然后走后续的逻辑。同样,也是先拿到资源路径,判断是否已 @modules 开头,如果是,则会拿出包名,并且去 node_modules 中去处理返回对应内容。到这里,vite 的大部分工作就已经完成了,剩下的还有诸如 vue,css,ts 等文件的处理,就留给诸位自行探索了,这里给各位安利一个其他大佬写的 vite 源码解析,希望对大家有所帮助。如果小伙伴们对于浏览器解析有兴趣的,推荐大家了解一下 snowpack 这款首次提出浏览器 ESM 的工具。

零零总总说了这么多,第一部分 Vue 3.0 的简单介绍总算说完了,其实总结起来就是一句话:Vue 3 相比于 Vue 2,性能更好,理念更先进,开发更友好,十分推荐大家使用。

Vue 3.0 数据侦测源码分析与手动实现

前置知识

在了解 Vue 3.0 的数据侦测之前,我们最好先了解一下 Vue 2 的数据侦测和有关的前置知识。

Vue 2 数据绑定实现

Vue 2 面世已经足够久了,相信看这边文章的小伙伴们都对其的数据绑定逻辑十分的熟悉了解了。不管怎么样,我们这里还是简要介绍一下。为了收集数据依赖,监听数据变化并更新视图,vue2.0 通过 Object.defineProperty 这个方法,对 对象的 set 与 get 属性访问描述符进行了劫持;多层对象,则是通过递归的方式,来添加劫持;针对数组,则是通过对数组总共有 8 个原型方法进行了改写;就总体的设计模式而言,是通过实现观察者模式实现数据监听,并在此之上,实现的MVVM模式;下图就是对应的代码:

image20201214224026508.png
然而,这种实现方式存在一定的局限性,其中,最老生常谈的,就是对于对象和数组动态添加的属性,无法进行监听,如Array[1] = 111这种修改,这也是为什么我们在vue开发中,遇见这种情况,常使用vue.$set与slice等方法的原因;除此之外,由于对于对象需要做递归遍历的操作,导致性能并没有达到最优;无法对 Map,WeakMap 等数据结构进行数据绑定等。

Reflect & Proxy

而说到 Vue 3 的数据侦测的实现,无论如何都绕不过 Reflect 和 Proxy。这两者都是 ES 6 新增的。其中,Reflect 作为一个内置的对象,负责提供拦截 JavaScript 操作的方法,这些方法和 proxy handler 的方法相同;而 proxy 的作用则是在目标对象的外层,设置一层拦截,外界对于这个对象的访问,都会过这个拦截,从而可以实现对外界访问,响应的过滤和改写。Proxy 有两个入参,分别是 target 目标对象,handler 拦截处理函数。Proxy 和 Reflect 搭配使用,由 Proxy 负责对访问进行拦截,Reflect 则负责对数据进行操作。在能够收集到数据变化后,就可以根据变化去通知视图进行相应的更新,这也就是 ”发布-订阅者“最简单的实现。数据是发布者,视图是订阅者,视图得知数据变化后,就会进行更新。

源码分析

Vue 3 的数据响应式 API 都是在 reactivity 里实现并暴露出来的,所以我们这次主要看的是这个 package 的代码。

image20201216230119426.png

reactivity 向外暴露出的 API 主要是可以划分为四类,分别是 computed,effect,reactive,ref,其中,我们这次主要了解的是 reactive 基于 Proxy 实现的响应式数据。

简单点来理解呢,reactive 和 ref 都可以说是返回数据的响应式副本,供我们在页面上进行使用,视图会随着其数值的变化而更新,以下是一个简单的使用示例:

image20201216231358009.png

在此之后,我们就可以在模板中直接使用 list 或者 user 变量,而不需要像使用 ref 那样通过 list.value 来使用。ref 实际上是一个数据容器,实现的方法和 reactive 还是有区别的。

reactive

首先呢,我们来看一下 reactive.ts 文件中暴露出来的 reactive 方法。

image20201216231735453.png

我们可以看到,reactive 方法接受一个函数,也就是我们的目标对象。方法内首先会对传入的目标对象做一个检测,检测其是否是一个 readonly 只读类型的数据,对于只读类型数据,自然没有什么必要实现数据响应,毕竟不会发生变化嘛;如果非只读类型数据,reactive 方法就会调用一个名为 createReactiveObject 的方法来给目标对象创建一个响应式的对象副本。这个副本对象中自动接入了ref,所以我们不需要在通过 .value 拿到并使用其属性,直接通过 key 就可以使用。

createReactiveObject

那么,重头戏来了,让我们看看 createReactiveObject 方法是如何创建响应式副本的。

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
参数分析

首先, createReactiveObject 方法接收 4 个参数,分别是 target,isReadonly,baseHandlers,collectionHandlers。

  • target:目标对象,Vue 3 接入了 TypeScript,这里定义了 对象的类型 Target,了解即可;

    export interface Target {
      [ReactiveFlags.SKIP]?: boolean
      [ReactiveFlags.IS_REACTIVE]?: boolean
      [ReactiveFlags.IS_READONLY]?: boolean
      [ReactiveFlags.RAW]?: any
    }
    
  • isReadonly:是否只读;

  • baseHandlers:这里的 ts 声明表示它是 ProxyHandler 类型,对于 TypeScript 不是很了解的同学可能会对 ProxyHandler 比较陌生。这次我们不需要完全理解其和 ProxyConstructor 的具体含义,我们可以简单的理解为是一个对象,这个对象可能具有 get,set,deleteProperty,defineProperty等属性的对象。具体可能有哪些属性,可以参考下面的 typescript 源码:

    interface ProxyHandler<T extends object> {
        getPrototypeOf? (target: T): object | null;
        setPrototypeOf? (target: T, v: any): boolean;
        isExtensible? (target: T): boolean;
        preventExtensions? (target: T): boolean;
        getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
        has? (target: T, p: PropertyKey): boolean;
        get? (target: T, p: PropertyKey, receiver: any): any;
        set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
        deleteProperty? (target: T, p: PropertyKey): boolean;
        defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
        ownKeys? (target: T): PropertyKey[];
        apply? (target: T, thisArg: any, argArray?: any): any;
        construct? (target: T, argArray: any, newTarget?: any): object;
    }
    

    这里的 baseHandlers 其实就是 Proxy 的 handler 拦截处理函数。

  • collectionHandlers:collectionHandlers 与 baseHandlers 都是用于拦截处理,区别就在于其使用的场景不同。

逻辑分析

好了,在说完入参之后,我们就可以来看一下主要的代码逻辑了。

creativeReactiveObject 方法首先会对传入的目标对象进行分析:如果传入的值并非对象,或传入的对象本身已经是一个响应式的对象,又或该值的数据类型并不在支持数据侦测的白名单上时,会直接返回该值;在不满足上述条件的情况下,才会利用 new Proxy 给目标对象生成一个 Proxy 实例。在生成 Proxy 实例时,会先对 target 的数据类型进行判断,判断其是否为 Map,Set,WeakMap,WeakSet,如果是,则使用 collectionHandlers,不是则使用 baseHandlers。那么,问题来了,collectionHandlers 与 baseHandlers 的区别在哪里呢?

还是看回 reactive 方法,我们可以看到 baseHandlers 和 collectionHandlers 实际的值分别是 mutableHandlers 与 mutableCollectionHandlers。下面分别看一下这二者的代码。

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)
}

让我们先不管 createInstrumentationGetter 这个方法,先对比一下 mutableHandlers 和 mutableCollectionHandlers,我们会发现mutableCollectionHandlers 只有 get,这个是给不需要派发更新的变量使用的;而 mutableHandlers 则有 get,set 等,就是我们真正需要使用的 handler。接下来,让我们看一下 get,set等方法。

const get = /*#__PURE__*/ createGetter()

const set = /*#__PURE__*/ createSetter()

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

其中,createGetter负责依赖收集,createSetter负责变化通知。

createGetter 方法返回一个 get 方法,这个方法接收3个入参,分别是 target,key,receiver。get 方法首先会对 key 值进行校验,根据 key 值和 ReactiveFlags 返回特殊值;如果没有匹配到,才会继续判断 target 是否是数组,并且对数组类型的 target 的 get 操作进行设置;如果是对象类型的数据,且非只读,则会递归调用 reactive 方法去创建响应式的副本,相较于 Vue 2 在应用初始化时就递归劫持所有的 get 方法而言,这种处理方式有着很明显的优化。整个逻辑并不复杂,这里我们需要注意的是一个名为 track 的方法,这个方法将 activeEffect 添加到数据属性的依赖列表中,完成依赖的收集工作(依赖实质上是一个个的 effect 方法,通过 WeakMap 进行存储)。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key) // 依赖收集
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

createSetter方法负责通知变化,也就是通过调用 trigger 更新数据,并通知依赖进行处理。需要注意的是,在进行处理时,会区分是此时的操作是动态添加属性,还是对属性进行更新。trigger 方法是在修改值时,通过 target 对象,从全局 weak map 对象中取出对应的depMap 对象,然后再根据修改的 key 取出对应的 dep 依赖集合,并遍历并执行该集合中所有的 effect,从而实现的数据更新与通知,具体的代码我就不放出来了,有兴趣的小伙伴可以自行了解一下。

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
      	// 动态添加属性
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
      	// 属性更新
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

到了这里,我们就已经很简单的把 Vue 3 数据侦测的代码过了一遍了。

参考资料

The process: Making Vue 3

Vue3 Compiler 优化细节,如何手写高性能渲染函数

vite design

有了 vite,还需要 webpack 么?

Top