前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue3 源码解析

vue3 源码解析

作者头像
EchoROne
发布2022-08-15 08:35:26
7530
发布2022-08-15 08:35:26
举报
文章被收录于专栏:玩转大前端

一、核心变化

首先,Vue 3 全面拥抱了 TypeScript。一方面源码全部使用 TpeScript 进行了重写,另一方面,开发者使用 TypeScript 进行开发时也能进行更好地类型推导,获得更好的开发体验。关于这方面,我们不做过多的解析。

第二个重要的改变是,Vue 3 在基本兼容 Vue 2 的使用方式之外,还提供了一套名为 Compostion API 的新接口

1、Composition API

Composition API 是指可以在组件初始化的时候使用一个setup()方法,数据、方法等都可以由setup()方法统一初始化后返回

代码语言:javascript
复制
<template>
  <button @click="increment">{{count}}</button>
</template>

<script>
export default {
  setup(){
    const count = ref(0);

    function increment(){
      count.value++;
    }

    return {
      count,
      increment
    }
  }
}
</script>

上面的例子只演示了 datamethod 在 Composition API 中是如何等价表达,其它的如 computedwatch 等也有相应的表达。这套 API 带来的好处比较多:

  1. 对 TypeScript 类型推导友好,编程体验较好
  2. 提供了类似 React Hooks 的复用机制,从而可以在不引入新的组件的情况下,将组件内部的代码再次合理进行抽取,即在组件内部又提供了一种组织代码的方法

官方例子:

使用 Vue 2 的源代码位于 https://github.com/vuejs/vue-cli/blob/a09407dd5b9f18ace7501ddb603b95e31d6d93c0/packages/@vue/cli-ui/src/components/folder/FolderExplorer.vue

基于 Vue 3 Composition API 重构的代码位于 https://gist.github.com/yyx990803/8854f8f6a97631576c14b63c8acd8f2e

2、基于 Proxy 的 reactivity

Vue 2 中响应式数据是通过 ES5 的 getter/setter 来实现的,在前面的章节中我们做了比较详尽的介绍。这套响应式数据机制受限于语言机制,有一些不能完全覆盖到数据变更的地方,例如当动态向对象上添加 key,或者通过下标的方式修改数组,就不能触发数据变更的检测机制。

Vue 3 中响应式数据使用了一个不同的机制 ——Proxy,这个机制我们在前文介绍响应式的实现机制时有专门介绍过,在这里可以再复习一下。

Proxy 可以为任何一个对象设置一个代理对象,并在代理对象中设置一些自定义的逻辑。Vue 3 使用这个机制来监测数据变更:首先针对需要监听的数据对象设置一个代理对象,在代理对象中拦截对数据赋值的操作,从而获知数据变化:

代码语言:javascript
复制
const dataObj = {};

const dataProxy = new Proxy(dataObj, {
    set(target, key, value){
        // 进行对应界面的更新
        Reflact.set(target, key, value);
    }
});

值得注意的是,因为只有在 ES6 的环境中才可以使用 Proxy,且这个特性无法通过编译到 ES5 的方式进行降级,所以目前兼容性相对来说比较差,这也是 Vue 3 不支持 IE 浏览器的主要原因

3、tree-shaking

Vue 2 的体积其实已经不算大了,但是使用 Vue 3 时可以让打包体积更小。这一方面利益于 Composition API 的设计,另一方面也是因为随着 ESM 模块化规范(和 TS 模块化规范)的成熟,tree-shaking 被使用得越来越广泛了。

tree-shaking 是指针对代码中模块的依赖关系进行静态分析,将完全没有使用到的模块在打包时直接移除掉,只打包使用到的模块,从而减少体积。

得益于 Vue 3 良好的模块划分,开发者在使用 Vue 3 时可以按需选择需要的模块引入,而不用一次性将所有的代码全部引入,这样在打包时 Vue 3 中没有被引用的源码将被移除。

在 Vue 的仓库中专门有一个 @vue/size-check 的包,就是用来测试 tree-shaking 的:

代码语言:javascript
复制
import { h, createApp } from '@vue/runtime-dom'

// The bare minimum code required for rendering something to the screen
createApp({
  render: () => h('div', 'hello world!')
}).mount('#app')

这个例子中只使用了 h 和 createApp,其它的诸如 watch、computed、reactive 等模块都没有被引用,因此打包时也将只包含使用到的源码,从而使整个应用的打包体积变得更小

4、渲染性能优化

Vue 2 引入了虚拟 DOM 来进行对比计算,从而确定 DOM 中需要更新的节点。虽然虚拟 DOM 是在 JavaScript 中进行运算,速度足够快了,但是实际上这个对比计算的过程仍然有非常多不必要的步骤。

Vue 3 在这里进行了更为精细的性能优化,主要包括:

以 v-if 和 v-for 作为边界,将模板分成很多 “块”,每个块中的结构是完全固定的,这样块中就不需要进行树的遍历,只需要对比绑定的值即可

将静态节点、子树等渲染代码移到渲染函数之外,这样可以避免每次渲染时重新创建这些不会变化的对象

将元素的更新类型进行细分,例如动态绑定的部分如果只涉及到 class,则在对比时只需要对比 class 即可,不需要对比它的内容

这些优化大大改进了 Vue 3 的渲染性能,从各种公开的数据来看,基本上能有 5 到 10 倍的提升

除了上述几点变化以外,Vue 还有不少的变化,如官方支持了Teleport组件、模板中支持了多根节点的组件等等。但相比上述变化,这些都显得不是那么重要,因此不花更多篇幅进行解读

二、Vue3整体结构和源码目录

1、单体仓库模式

我们非常熟悉的模式是一个 npm 包对应一个 git 代码仓库,这样做的好处是代码仓库中的代码边界清晰、职责单一,在开发维护的时候也一定程度上会强迫开发人员认真思考每个包的功能和 API。但是如果代码间拆分的包过多,或者包之间耦合较多,这种模式在日常开发和维护时就显得不太方便了,因为很有可能一个小小的改动就需要动到好几个 npm 包的代码。

单体仓库就是为解决这个问题而出现的一种解决方案。具体而言,就是各个包从逻辑上互相独立,通过 npm 包的形式来互相依赖,但是各个包的代码都放在同一个 git 仓库中。

Vue 3 从代码结构上进行了梳理,更加模块化,而且将很多之前属于内部实现的部分也抽象成了模块,作为单独的 npm 包发布。这样开发者还可以不引用整个框架,而只使用框架中用到的各个独立的模块。这样一来,Vue 3 最终要发布的包数量就多了很多,因此也选择了使用单体仓库模式

2、整体结构

Vue 3 的代码仓库根目录基本上就只有 packagesscripts 两个目录,后者主要用于一些工程操作(检查环境、打包等),因此真正的源码都位于 packages 目录中。这个目录下一共有 13 个包:

  • compiler-core 模板解析核心,与具体环境无关,主要生成 AST,并根据 AST 生成 render() 方法
  • compiler-dom 浏览器环境中的模板解析逻辑,如处理 HTML 转义、处理 v-model 等指令
  • compiler-sfc 负责解析 Vue 单文件组件,在前面 vue-loader 的解析中有讲解过
  • compiler-ssr 服务端渲染环境中的模板解析逻辑
  • reactivity 响应式数据相关逻辑
  • runtime-core 与平台无关的运行时核心,包括 render
  • runtime-dom 浏览器环境中的运行时核心
  • runtime-test 用于自动化测试的相关配套
  • server-renderer 用于 SSR 服务端渲染的逻辑
  • shared 一些各个包之间共享的公共工具
  • size-check 一个用于测试 tree shaking 后代码大小的示例库
  • template-explorer 用于检查模板编译后的输出,主要用于开发调试
  • vue Vue 3 的主要入口,包括运行时和编译器,包括几个不同的入口(开发版本、runtime 版本、full 版本)

上述包中,剔除服务端渲染相关代码、开发调试、测试相关代码,并将它们的依赖关系体现出来,则大致是这样的(来自 Vue 3 仓库):

可见 Vue 3 的模块关系拆分得非常清楚:

  • compiler 模块负责 Vue 模板的解析、生成 render 方法
  • runtime 模块负责调用 render 方法生成虚拟 DOM 并渲染,同时处理用户交互、更新应用状态等等
  • vue 作为入口,分别依赖 compiler 和 runtime 2 个大模块(如果是 runtime only 的版本,则只依赖 runtime)。这两个大模块内部又将核心剥离出去,然后将浏览器环境相关的部分再放到一个单独的包中

我们很关注的响应式数据的部分 reactivity 也是作为 runtime 的一部分,用于处理运行时数据发生变化的情况

三、Reactivity的原理和实现方式

Vue 3 将响应式数据的逻辑单独提出来作为一个独立的模块了,即源码中的reactivity包,在 npm 上是@vue/reactivity

  1. 当一个数据被包装成响应式数据时,它的读取和修改方法会被 Proxy 拦截
  2. 数据被读取时,Proxy 记录 effect 与当前数据的依赖关系
  3. 数据被修改时,触发 effect 运行
  4. 如果调用一下effect(fn),则之后任何fn()中依赖的数据发生变化,fn()都会被再次调用。这便是 Vue 3 的响应式数据的原理

Vue 作者还写了一个小项目vue-lithttps://github.com/yyx990803/vue-lit),这个项目使用了reactivity,仅仅用很少量的代码就搭出了一个非常轻量的组件化框架。这个项目也可以帮忙我们更好地理解独立出来的reactivity模块的用法。

四、compiler源码解析

和 Vue 2 一样,compiler 的主要作用是解析 Vue 中的模板部分,最终将模板转换成render()方法

整个编译过程分为 3 步:

  1. 调用 baseParse() 方法,解析传入的模板,生成 AST(即 ast 变量)
  2. 调用 transform() 方法对 AST 进行转换
  3. 调用 generate() 方法,根据转换后的 AST 生成 render() 方法并返回

第 2 点需要特别注意,在 Vue 2 中,生成 AST 后会有一个 optimize 的过程,而在 Vue 3 中,optimize “升级” 了一下,变成了 transform 的过程。transform 会做一些额外的逻辑处理,也会为了后续渲染性能的优化预先做一些处理并反映到 AST 中。

1、parse

首先看模板解析并生成 AST 的过程,Vue 3 的模板解析是非常典型的代码解析方法,即从源码的第 1 个字符开始逐个字符进行扫描,每当找到可以识别的对象则将该对象放入 AST,并接着之前的源码位置往下继续解析

2、transform

transform 的作用是针对上一步生成的 AST 进行一些变换,例如针对 v-ifv-for 增加一些特殊的节点,以及为了后续渲染性能优化增加一些属性等等

  1. 首先使用 tranverseNode() 对 AST 进行遍历,并在每次遍历退出前调用 hook,以便相关逻辑对相应的节点进行修改
  2. 如果需要进行静态节点提升,则调用 hoistStatic() 方法提升静态节点
  3. 调用 createRootCodegen() 方法生成 codegenNode,以便在后续 generate 阶段使用

3、generate

和前面的 parse、transform 一样,generate 的过程也是先生成一个 context,然后再处理。

首先它会做一堆前置处理:

  • 生成模块化相关包装代码
  • 根据是否是服务端渲染,应用不同的处理参数生成函数签名
  • 处理 scopeIdisSetupInlined 相关逻辑
  • 处理组件、指令等

接下来调用 genNode(),很机械地生成代码,不做详细解析,值得注意的是传入的参数正是 ast.codegenNode,而不是 ast 本身

五、runtime 源码解析

在 Vue 3 中,不再使用new App()的方式来新建 Vue APP,而是使用createApp()方法来创建。这个入口就属于runtime

1、block

上一节我们提到了一个概念叫 block,在上面的代码中也再次出现了 block 的概论。这个概念是 Vue 3 为了提升渲染性能而提出来的。

在 Vue 2 中,虚拟 DOM 的更新需要对新、旧两棵 VNode 树进行全量遍历和对比,然后才能确定发生变动的地方。但是,Vue 的虚拟 DOM 多数情况下是由模板编译而来,而模板的结构大部分地方是确定的,也就是树的结构大部分地方不会发生变化,变化的可能只是属性、文字等,真正会使结构发生变化的基本上只有 v-ifv-for 等少数几个指令。

因此 Vue 3 增加了一个 block 的概念,一个 block 中就是一个确定的树状结构。我们以 v-if 为例,尽管它会导致组件的整体结构发生变更,但它自己的子节点却是稳定的,因此它可以当作是一个完整的 block。当其它的 block 中碰到 v-if 的时候,只需要知道 “这里有一个 block” 就好了。通过这样的方式,虚拟 DOM 的对比就会非常高效,因为 block 的结构是固定的,block 内部就没有必要去对比是否结构发生了变化。

具体到实现中,如果 VNode 节点上存在 dynamicChildren,则说明它是一个 block。再回到前面生成的 render() 函数

Vue 3 runtime 模块的主要功能:

  1. 提供 createApp()API,完成初始化的工作
  2. 调用 render() 方法完成渲染
  3. 如何对虚拟节点进行对比和更新

在上面我们也看到了 Vue 3 是如何通过 ShapeFlagPatchFlag、block 等机制实现高性能渲染的

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、核心变化
  • 1、Composition API
  • 2、基于 Proxy 的 reactivity
  • 3、tree-shaking
  • 4、渲染性能优化
  • 二、Vue3整体结构和源码目录
  • 1、单体仓库模式
  • 2、整体结构
  • 三、Reactivity的原理和实现方式
  • 四、compiler源码解析
  • 1、parse
  • 2、transform
  • 3、generate
  • 五、runtime 源码解析
  • 1、block
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档