前言
《Vue.js 设计与实现》:它不同于市场上纯粹的 “源码分析” 类的书籍。而是 从高层的设计角度,探讨框架需要关注的问题(-尤雨溪序),以 提出问题 - 分析思路 - 解决问题 的方式,来讲解 vue 3 的核心设计。其内部,没有读一行 vue3 的源码,但却可以让我们对整个 vue 3 的核心,拥有一个非常清楚的认知。
第一篇:框架设计概览
第一章:权衡的艺术
命令式和声明式
首先第一个方面就是:命令式和声明式 的概念。
所谓 命令式 指的就是:关注过程 的范式。

而 声明式 指的就是: 关注结果 的范式。

那么这里大家来想一下,vue 是声明式的?还是命令式的?
对于 vue 而言,它的内部实现一定是 命令式 的,而我们在使用 vue 的时候,则是通过 声明式 来使用的。
也就是说: vue 封装了命令式的过程,对外暴露出了声明式的结果
性能与可维护性的权衡
在明确好了命令式和声明式的概念之后。接下来咱们来看下从 性能 层面,vue 所体现出来的一种权衡的方式。
针对于性能的分析,主要从两个方面去说。
首先第一个方面:大家觉得 是命令式的性能更强,还是声明式的性能更强呢?
答案是:命令式的性能 > 声明式的性能。
其实原因非常简单,对于 命令式 的代码而言,它直接通过 原生的 JavaScript 进行实现,这是最简单的代码,没有比这个更简单的了,我们把它的性能比作 1。
而声明式,无论内部做了什么,它想要实现同样的功能,内部必然要实现同样的命令式代码。所以它的性能消耗一定是 1 + N 的。
那么既然如此,vue 为什么还要对外暴露出声明式的接口呢?
这其实是因为:声明式的可维护性,要远远大于命令式的可维护性。
当性能与可维护性产生冲突时,那么舍鱼而取熊掌者也。(注意:在 vue 的性能优化之下,它并不会比纯命令式的性能差太多)
而这样的一种权衡,在 template 模板中,更是体现的淋漓尽致。
在前端领域,想要使用 JavaScript 修改 html 的方式,主要有三种:原生 JavaScript、innerHTML、虚拟 DOM
很多小伙伴都会认为 虚拟 DOM 的性能是最高的,其实不是。
从这个对比我们可以发现,虚拟 DOM 的性能,并不是最高的。
但是它的 心智负担(书写难度)最小, 从而带来了 可维护性最高。所以哪怕它的性能并不是最高的。vue 依然选择了 虚拟 DOM 来进行了渲染层的构建。
这个也是一种性能与可维护性的权衡。
运行时和编译时
第一章的最后一部分,主要讲解的就是 运行时和编译时。
这两个名词,各位小伙伴在日常开发中,应该是经常听到的。
它们两个都是框架设计的一种方式,可单独出现,也可组合使用。
那么下面咱们就分别来介绍一下它们。
首先是 运行时:runtime。
它指的是:利用 render 函数,直接把 虚拟
DOM转化为 真实DOM元素 的一种方式。在整个过程中,不包含编译的过程,所以无法分析用户提供的内容。
其次是 编译时:compiler:
它指的是:直接把
template模板中的内容,转化为 真实DOM元素。因为存在编译的过程,所以可以分析用户提供的内容。
同时,没有运行时理论上性能会更好。
目前该方式,有具体的实现库,那就是现在也非常火的
Svelte但是这里要注意: 它的真实性能,没有办法达到理论数据。
最后是 运行时 + 编译时:
它的过程被分为两步:
- 先把
template模板转化为render函数。也就是 编译时- 再利用
render函数,把 虚拟DOM转化为 真实DOM。也就是 运行时两者的结合,可以:
在 编译时,分析用户提供的内容
在 运行时,提供足够的灵活性这也是
vue的主要实现方式。
第二章:框架设计的核心要素
- 通过 环境变量 和
TreeShanking控制打包之后的体积 - 构建不同的打包产物,以应用不同的场景
- 提供了
callWithErrorHandling接口函数,来对错误进行统一处理 - 源码通过
TypeScript开发,以保证可维护性。 - 内部添加了大量的类型判断和其他工作,以保证开发者使用时的良好体验。
第三章:Vue.js 3 的设计思路
在这一章中,作者站在一个高层的角度,以 UI 形式、渲染器、组件、编辑器 为逻辑主线进行的讲解。
下面咱们就来捋一捋这条线。
在 Vue 中 UI 形式主要分为两种:
- 声明式的模板描述
 - 命令式的 render 函数

而针对于 声明式的模板描述 而言,本质上就是咱们常用的 tempalte 模板。它会被 编辑器 编译,得到 渲染函数 render 。
渲染器与渲染函数,并 不是 一个东西。
渲染器是 函数 createRenderer 的返回值,是一个对象。被叫做 renderer。 renderer 对象中有一个方法 render,这个 render ,就是我们常说的渲染函数。
渲染函数接收两个参数 VNode 和 container。
其中 VNode 表示 虚拟 DOM,本质上是一个 JS 对象。container 是一个容器,表示被挂载的位置。而 render 函数的作用,就是: 把 vnode 挂载到 container 上。
同时,因为 Vue 以组件代表最小颗粒度,所以 vue 内部的渲染,本质上是:大量的组件渲染。
而组件本质上是一组 DOM 的集合,所以渲染一个一个的组件,本质上就是在渲染一组这一组的 DOM。也就是说,Vue 本质上是: 以组件作为介质,来完成针对于一组、一组的 DOM 渲染。
第二篇:响应式系统
第四章:响应系统的作用与实现
在这一章中,作者从 响应式数据的概念开始,讲解了响应式系统的实现。 然后针对于 计算属性与 watch 的实现原理,进行了分析。 在分析的过程中,也对其所设计到的 调度系统(scheduler) 和 惰性执行(lazy) 的原理进行了明确。 最后讲解了在 竞态问题下,关于过期的副作用的处理逻辑。
响应式数据
那么首先咱们先来看基本概念 副作用函数 与 响应式数据。
所谓 副作用函数 指的是 会产生副作用的函数,这样的函数非常的多。比如

在这段代码中, effect 的触发会导致全局变化 val 发生变化,那么 effect 就可以被叫做副作用函数。而如果 val 这个数据的变化,导致了视图的变化,那么 val 就被叫做 响应式数据。
那么如果想要实现响应式数据的话,那么它的核心逻辑,必然要依赖两个行为:
- 第一个是
getter行为,也就是 数据读取 - 第二个是
setter行为,也就是 数据修改
在 vue 2 中,这样的两个行为通过 Object.defineProperty 进行的实现。
在 vue 3 中,这样的两个行为通过 Proxy 进行的实现。
那么具体的实现逻辑是什么呢?咱们来看下面的图示:
首先是 getter 形式:
在该函数中,存在一个 effect 方法,方法内部触发了 getter 行为。一旦 getter 行为被触发,则把对应的 effect 方法保存到一个 “桶(数据对象)” 中
当触发 setter 行为时:
则会从 “桶” 中取出 effect 方法,并执行。
那么此时因为 obj.text 的值发生了变化,所以 effect 被执行时 document.body.innerText 会被赋上新的值。从而导致视图发生变化。
调度系统(scheduler)
那么说完了基本的响应性之后,接下来咱们来看 调度系统(scheduler)
所谓调度系统,指的就是 响应性的可调度性。
而所谓的可调度,指的就是 当数据更新的动作,触发副作用函数重新执行时,有能力决定:副作用函数(effect)执行的时机、次数以及方式
比如,在这段打印中,决定打印的顺序

而想要实现一个调度系统,则需要依赖 异步:Promise 和 队列:jobQueue 来进行实现。咱们需要 基于 Set 构建出一个基本的队列数组 jobQueue,利用 Promise 的异步特性,来控制执行的顺序
计算属性(computed)
当我们可以控制了执行顺序之后,那么就可以利用这个特性来完成 计算属性(computed) 的实现了。
计算属性本质上是: 一个属性值,当依赖的响应式数据发生变化时,重新计算
那么它的实现就需要彻底依赖于 调度系统(scheduler) 来进行实现。
惰性执行(lazy)
说完计算属性,那么下面我们来看下 watch 监听器。
watch 监听器本质上是 观测一个响应式数据,当数据发生变化时,通知并执行相应的回调函数
这也就意味着,watch 很多时候并不需要立刻执行。
那么此时,就需要使用到 惰性执行(lazy) 来进行控制。
惰性执行的实现要比调度系统简单。它本质上 是一个 boolean 型的值,可以被添加到 effect 函数中,用来控制副作用的执行。
if (!lazy) {
// 执行副作用函数
}
过期的副作用
watch 监听器的实现非常广泛,有时候我们甚至可以在 watch 中完成一些异步操作。
但是大量的异步操作,极有可能会导致 竞态问题。
所谓的竞态问题,指的是 在描述一个系统或者进程的输出,依赖于不受控制的事件出现顺序或者出现时机。比如咱们来看这段代码

这段代码完成的是一个异步操作。
如果
obj连续被修改了两次,那么就会发起两个请求。我们最终的期望应该是data被赋值为 请求B 的结果。但是,因为异步的返回结果我们无法预计。所以,如果 请求 B 先返回,那么最终
data的值就会变为 请求 A 的返回值。这个咱们的期望是不一样的。
那么这样的问题,就是 竞态问题
而如果想要解决这问题,那么就需要使用到 watch 回调函数的第三个参数 onInvalidate,它本身也是一个回调函数。并且 该回调函数(onInvalidate)会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求
而 onInvalidate 的实现原理也非常简单,只需要 在副作用函数(effct)重新执行前,先触发 onInvalidate 即可。
第五章:非原始值(对象)的响应性方案
书中的第五章整体而言非常简单,主要就介绍了两个接口,Proxy 和 Reflect。
这两个接口通常会一起进行使用,其中:
Proxy可以 代理一个对象(被代理对象)的 getter 和 setter 行为,得到一个 proxy 实例(代理对象)Reflect可以 在 Proxy 中使用 this 时,保证 this 指向 proxy,从而正确执行次数的副作用
第六章:原始值(非对象)的响应性方案
如果大家熟悉 proxy 的话,那么可以知道,针对于 proxy 而言,它只能代理复杂数据类型。这就意味着,简单数据类型无法具备响应性。
但是,在 vue 中,我们可以通过 ref 构建简单数据类型的响应。
那么 ref 是如何进行实现的呢?
这里大家要注意:针对于最新的 vue 3.2 而言,书中在 《6.1 引入 ref 的概念》中所讲解的 ref 实现原理存在 “落后性”。 vue 3.2 已经修改了 ref 的实现,这得益于 @basvanmeurs 的贡献
在最新的 vue 3.2 代码中,vue 通过 **get 、set 函数标记符,让函数以属性调用的形式被触发。**这两个修饰符,可以让我们 像调用属性一样,调用方法。 所以当我们平时 访问 ref.value 属性时,本质上是 value() 函数的执行。
第三篇:渲染器
第七章:渲染器的设计
在之前咱们说过 渲染器与渲染函数不是一个东西
- 渲染器 是
createRenderer的返回值,是一个对象。 - 渲染函数 是渲染器对象中的
render方法
在 vue 3.2.37 的源码内部,createRenderer 函数的具体实现是通过 baseCreateRenderer 进行的。它的代码量非常庞大,涉及到了 2000 多行的代码。
总体可以被分为两部分:
- 在浏览器端渲染时,利用
DOM API完成DOM操作:比如,如果渲染DOM那么就使用createElement,如果要删除DOM那么就使用removeChild。 - 渲染器不能与宿主环境(浏览器)产生强耦合:因为
vue不光有浏览器渲染,还包括了服务端渲染,所以如果在渲染器中绑定了宿主环境,那么就不好实现服务端渲染了。
所谓 vnode 本身是 一个普通的 JavaScript 对象,代表了渲染的内容。对象中通过 type 表示渲染的 DOM。比如 type === div:则表示 div 标签、type === Framgnet 则表示渲染片段(vue 3 新增)、type === Text 则表示渲染文本节点。
第八章:挂载与更新
对于渲染器而言,它做的最核心的事情就是 对节点进行挂载、更新的操作。作者在第八章中,详细的介绍了对应的逻辑。
整个第八章分为两部分来讲解了这个事情:
DOM节点操作- 属性节点操作
DOM 节点操作
首先先来看 DOM 节点操作。DOM 节点的操作可以分为三部分:
- 挂载:所谓挂载表示节点的初次渲染。比如,可以直接通过
createElement方法新建一个DOM节点,再利用parentEl.insertBefore方法插入节点。 - 更新:当响应性数据发生变化时,可能会涉及到
DOM的更新。此时的更新本质上是属于 属性的更新。咱们等到属性节点操作那里再去说。 - 卸载:所谓卸载表示旧节点不在被需要了。那么此时就需要删除旧节点,比如可以通过
parentEl.removeChild进行。
以上三种类型,是 vue 在进行 DOM 操作时的常见逻辑。基本上覆盖了 DOM 操作 90% 以上 的常见场景
属性节点操作
看完了 DOM 操作之后,接下来咱们来看属性节点操作。
针对于属性而言,大体可以分为两类:
- 属性:比如
class、id、value、src… - 事件:比如
click、input…
那么咱们就先来看 非事件的属性部分。
想要了解 vue 中对于属性的处理,那么首先咱们需要先搞明白一个很重要的问题。那就是 浏览器中的属性分类。
在浏览器中 DOM 属性其实被分为了两类:
- 第一类叫做
HTML Attributes:直接定义在HTML 标签上的属性,都属于这一类。 - 第二类叫做
DOM Properties:它是拿到DOM对象后定义的属性。咱们接下来主要要说的就是它。
HTML Attributes 的定义相对而言比较简单和直观,但是问题在于 它只能在 html 中进行操作。
而如果想要在 JS 中操作 DOM 属性,就必须要通过 DOM Properties 来进行实现。但是因为 JS 本身特性的问题,会导致某些 DOM Properties 的设置存在特殊性。比如 class、type、value 这三个。
所以为了保证 DOM Properties 的成功设置,那么我们就必须要知道 **不同属性的 DOM Properties 定义方式 **。
下面咱们来看一下。
DOM Properties 的设置一共被分为两种:
el.setAttribute('属性名', '属性值'). 属性赋值:el.属性名 = 属性值或者el[属性名] = 属性值都属于.属性赋值
我们来看这段代码:
在这段代码中,我们为
textarea利用DOM Properties的方式设置了三个不同的属性:
- 首先是
class:class在属性操作中是一个非常特殊的存在。它有两个名字class和className。如果我们直接通过el.setAttribute的话,那么必须要用class才可以成功,而如果是通过. 属性的形式,那么必须要使用className才可以成功。- 第二个是
type:type仅支持el.setAttribute的方式,不支持.属性的方式- 第三个是
value:value不支持直接使用el.setAttribute设置,但是支持.属性的设置方式
除了这三个属性之外,其实还有一些其他的属性也需要进行特殊处理,咱们这里就不再一一赘述了。
事件
接下来,咱们来看 vue 对事件的处理操作。
事件的处理和属性、DOM 一样,也是分为 添加、删除、更新 三类。
- 添加:添加比较简单,主要利用
el.addEventListener进行实现即可。 - 删除:主要利用
el.removeEventListener进行处理。 - 更新:但是对于更新来说,就比较有意思了。下面咱们主要来看的就是这个更新操作。
通常情况下,我们所认知的事件更新应该是 删除旧事件、添加新事件 的过程。但是如果利用 el.addEventListener 和 el.removeEventListener 来完成这件事情,是一件非常消耗性能的事。
那么怎么能够节省性能,同时完成事件的更新呢?
这时,vue 对事件的更新提出了一个叫做 vei 的概念,这个概念的意思是: 为 addEventListener 回调函数,设置了一个 value 的属性方法,在回调函数中触发这个方法。通过更新该属性方法的形式,达到更新事件的目的。
这个代码比较多,大家如果想要查看具体代码的话,可以 在 github 搜索 vue-next-mini,进入到 packages/runtime-dom/src/modules/events.ts 路径下查看。
第九、十、十一章:Diff 算法
整个渲染器最后的三个章节全部都用来讲解了 diff 算法。
针对于 diff 而言,它的本质其实就是一个对比的方法,其描述的核心就是: “旧 DOM 组”更新为“新 DOM 组”时,如何更新才能效率更高。
目前针对于 vue 3.2.37 的版本来说,整个的 diff 算法被分为 5 步(这 5 步不跟大家读了,因为咱们没头没尾的读一遍,其实对大家也没有什么帮助):
sync from start:自前向后的对比sync from end:自后向前的对比common sequence + mount:新节点多于旧节点,需要挂载common sequence + unmount:旧节点多于新节点,需要卸载unknown sequence:乱序
而,针对于书中的这三章来说,本质上是按照 简单 diff 算法、双端 diff 算法、快速 diff 算法 的顺序把整个 diff 的前世今生基本上都说了一遍。里面涉及到了非常多的代码。
第四篇:组件化
第十二章:组件的实现原理
想要了解 vue 中组件的实现,那么首先我们需要知道什么是组件。
组件本质上就是一个 JavaScript 对象,比如,以下对象就是一个基本的组件
而对于组件而言,同样需要使用 vnode 来进行表示,当 vnode 的 type 属性是一个 自定义对象 时,那么这个 vnode 就表示组件的 vnode
而组件的渲染,本质上是 组件包含的 DOM 的渲染。 对于组件而言,必然会包含一个 render 渲染函数。如果没有 render 函数,那么 vue 会把 template 模板编译为 render 函数。而组件渲染的内容,其实就是 render 函数返回的 vnode。具体的渲染逻辑,全部都通过渲染器执行。
vue 3 之后提出了 composition API,composition API 包含一个入口函数,也就是 setup 函数。 setup 函数包含两种类型的返回值:
- 返回一个函数:当
setup返回一个函数时,那么该函数会被作为render函数直接渲染。 - 返回一个对象:当
setup返回一个对象时,那么vue会直接把该对象的属性,作为render渲染时的依赖数据
同时,对于组件来说还有一个 插槽 的概念。插槽的实现并不神奇。插槽本质上 是一段 innerHTML 的内容,在 vnode 中以 children 属性进行呈现。当插槽被渲染时,只需要渲染 children 即可。
对于组件来说,除了咱们常用的 对象组件 之外,vue 还提供了额外的两种组件,也就是 异步组件与函数式组件。
第十三章:异步组件与函数式组件
所谓异步组件,指的是: 异步加载的组件 。
比如服务端返回一个组件对象,那么我们也可以拿到该对象,直接进行渲染。
异步组件在 优化页面性能、拆包、服务端下发组件 时,会比较有用。
而对于 函数式组件 来说,相对就比较冷僻了。函数式组件指的是 没有状态的组件。本质上是一个函数,可以通过静态属性的形式添加 props 属性 。在实际开发中,并不常见。
第十四章:内建组件和模块
这一章中,主要描述了 vue 的三个内置组件。
keepAlive
首先第一个是 KeepAlive。
这是我们在日常开发中,非常常用的内置组件。它可以 缓存一个组件,避免该组件不断地销毁和创建。
看起来比较神奇,但是它的实现原理其实并不复杂,主要围绕着 组件卸载 和 组件挂载 两个方面:
- 组件卸载:当一个组件被卸载时,它并不被真正销毁,而是把组件保存在一个容器中
- 组件挂载:因为组件被保存了。所以当这个组件需要被挂载时,就不需要在重新创建,而是直接从容器中获取即可。
Teleport
Teleport 是 vue 3 新增的组件,作用是 将 Teleport 插槽的内容渲染到其他的位置。比如我们可以把 dialog 渲染到 body 根标签之下。
它的实现原理,主要也是分为两部分:
- 把 Teleport 组件的渲染逻辑,从渲染器中抽离
- 在指定的位置进行独立渲染
Transition
Transition 是咱们常用的动画组件,作用是 实现动画逻辑。
其核心原理同样被总结为两点:
DOM元素被挂载时,将动效附加到该DOM元素上DOM元素被卸载时,等在DOM元素动效执行完成后,执行卸载DOM操作
第五篇:编译器
第十五章:编译器核心技术概述
在编译器核心技术概述,主要包含两个核心内容:
- 模板
DSL的编译器 Vue编译流程三大步
模板 DSL 的编译器
在任何一个编程语言中,都存在编译器的概念。 vue 的编译器是在 一种领域下,特定语言的编译器 ,那么这种编译器被叫做 DSL 编译器。
而编译器的本质是 通过一段程序,可以把 A 语言翻译成 B 语言。在 vue 中的体现就是 把 tempalte 模板,编译成 render 渲染函数
一个完整的编译器,一个分为 两个阶段、六个流程:
- 编译前端:
- 词法分析
- 语法分析
- 语义分析
- 编译后端:
- 中间代码生成
- 优化
- 目标代码生成

而对于 vue 的编译器而言,因为它是一个特定领域下的编译器,所以流程会进行一些优化,一共分为三大步

parse:通过parse函数,把模板编译成AST对象transform:通过transform函数,把AST转化为JavaScript ASTgenerate:通过generate函数,把JavaScript AST转化为 渲染函数(render)
第十六章:解析器(parse)
这一章,主要详细讲解了 parse 解析逻辑。是在三大步中的 parse 逻辑的基础上,进行了一个加强。
所以这里咱们也按下不表
第十七章:编译优化
最后就是编译优化。
编译优化也是一个非常大的概念,其核心就是 通过编译的手段提取关键信息,并以此知道生成最优代码的过程。
它的核心优化逻辑,主要是 把节点分为两类:
- 第一类是 动态节点:也就是会 受数据变化影响 的节点
- 第二类是 静态节点:也就是 不受数据变化影响 的节点
优化主要的点,就是 动态节点。
优化的方式主要是通过 Block 树 进行优化。
Block 树 本质上就是一个 虚拟节点数对象,内部包含一个 dynamicChildren 属性,用来 收集所有的动态子节点,以达到提取关键点进行优化的目的。
除此之外,还有一些小的优化手段,比如:
- 静态提升
- 预字符串化
- 缓存内联事件处理函数
v-once指令- …
第六篇:服务端渲染
最后一篇只有一个章节,就是 同构渲染。
想要了解同构渲染,那么需要先搞明白 CSR、SSR 的概念。
CSR:所谓CSR指的是 客户端渲染。- 浏览器向服务器发起请求
- 服务器查询数据库,返回数据
- 浏览器得到数据,进行页面构建
SSR:表示 服务端渲染- 览器向服务器发起请求
- 服务器查询数据库,根据数据,生成
HTML,并进行返回 - 浏览器直接渲染
HTML
两种方式各有利弊,所以同构渲染,指的就是 把 CSR 和 SSR 进行合并。既可以单独 CSR ,也可以单独 SSR,同时还可以 结合两者,在首次渲染时,通过 SSR,在非首次渲染时,通过 CSR。
以下是三者的对比图
而针对 vue 的服务端渲染来说,它是 将虚拟 DOM 渲染为 HTML 字符串,本质上是 解析的 vnode 对象,然后进行的 html 的字符串拼接
最后又讲解了客户端激活的原理,大致分为两步:
- 为页面中的
DOM元素与虚拟节点对象之间建立联系 - 为页面中的
DOM元素添加事件绑定
这两步主要是通过 renderer.hydrate() 方法进行实现了。
视频出处:【一小时读完《Vue.js 设计与实现》】 https://www.bilibili.com/video/BV1K24y1q7eJ/?share_source=copy_web&vd_source=a9f0fd4630ebe41da19ca2c83eb295e6
视频文档出处:https://juejin.cn/post/7197980894363156540
作者:LGD_Sunday