Skip to content
On this page

一小时读完《Vue.js 设计与实现》 ​

序 ​

这是一本没有带你阅读一行源码,却可以让你在阅读完成之后,对 vue 3 所有的核心逻辑 了如指掌 的书籍。

无论是 响应性、调度系统、惰性执行 ,还是 **渲染器、diff 算法、编辑器三大步 ** ,甚至是 有限自动状态机 等所有你能想到知识,本书都可以给你答案。

它就是 尤雨溪亲自做序 ,Vue 官方团队成员:霍春阳 编写的 Vue.js 设计与实现。

前言 ​

hello,各位小伙伴,大家好。我是 Sunday。

在当前这个时间段下,关于 vue 3 源码的书籍,主要有两本。

第一本就是,咱们本次要讲的 《Vue.js 设计与实现》。

第二本是,《vue.js 技术内幕》,作者是黄轶。

正巧,两本书我都买来了。

这两本书,虽然都是讲解 vue 3 源码的,但是在讲解的方式上,有非常大的区别。

首先是 《Vue.js 设计与实现》:它不同于市场上纯粹的 “源码分析” 类的书籍。而是 从高层的设计角度,探讨框架需要关注的问题(-尤雨溪序),以 提出问题 - 分析思路 - 解决问题 的方式,来讲解 vue 3 的核心设计。其内部,没有读一行 vue3 的源码,但却可以让我们对整个 vue 3 的核心,拥有一个非常清楚的认知。

其次是 《vue.js 技术内幕》:它是一个标准的 “源码分析” 书籍,内部对 vue 3 的很多源码,进行了逐一的解读。

如果大家想要学习 vue 3 的设计思路,掌握框架设计的思维方式。那么我强烈推荐你看一看《Vue.js 设计与实现》。

如果你想要对 vue 源码进行逐一解读,那么可以看一下《vue.js 技术内幕》。

同时,最后请允许我打一个小广告,如果你不想看书,想要通过视频的方式学习 vue 3 核心逻辑,可以去看一下我在 慕课网的 Vue3源码解析,打造自己的Vue3框架 这门课程。我个人 强烈推荐 !

那么明确好了现在市面上学习 vue 3 源码的方式之后,下面就让我们进入到 《Vue.js 设计与实现》的学习中去吧!

大纲 ​

《Vue.js 设计与实现》的内容一共分为 6 篇, 18 个章节:

  • 首先第一篇:对 vue 的整个框架设计,进行了概述
  • 第二篇:主要讲解了 vue 中的响应式系统,除了大家所熟悉的 proxy 之外,额外还包含了:调度系统 scheduler、惰性执行 lazy、ref 的实现原理
  • 第三篇:主要针对 vue 的渲染器(renderer)进行了讲解,额外还包含了 diff 算法的详细讲解
  • 第四篇:是组件化。包含了 组件的渲染机制,以及对 vue 官方组件 KeepAlive、Teleport、Transition 的实现原理,进行了剖析
  • 第五篇:是编译器(compiler)。在这一篇中,把编译器的三大步:parse、transform、generate 进行了分步的讲解。
  • 最后:是服务端渲染。主要是 CSR、SSR 以及 同构渲染。

第一篇:框架设计概览 ​

整个第一篇分为三个章节:

  1. 权衡的艺术:这里主要涉及到了 vue 框架设计的一些基本概念,也是咱们讲解的重点
  2. 框架设计的核心要素:相对比较杂,都是一些零碎的知识点
  3. Vue.js 3 的设计思路:这一章包含了 vue 框架设计的逻辑主线,也非常重要,但是内容并不多

那么首先咱们先来看第一章。

第一章:权衡的艺术 ​

在这一章中,开头的一句话,描述了框架设计的精髓,这句话也是尤雨溪在多个开发者大会中经常提到的,那就是:框架的设计,本身就是一种权衡的艺术。

在这一章中,书中分别从三个方面来去分析了所谓权衡的艺术,到底是什么意思。

命令式和声明式 ​

首先第一个方面就是:命令式和声明式 的概念。

所谓 命令式 指的就是:关注过程 的范式。

image-20230207231540415.png

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

image-20230207231536123.png

什么意思呢?我们来举一个小例子:

张三的妈妈,让张三去买酱油。

那么对于张三而言,他就需要:拿钱、出门、下楼、进超市、拿酱油、付钱、回家。

而对于张三的妈妈来说,她完全不需要关心张三做了什么,只需要对张三说一声就可以了。

那么在这个例子中,张三就是一个典型的命令式,他需要完成整件事情的所有过程。

而张三的妈妈,就是典型的声明式,她不关心过程只关心结果。

那么这里大家来想一下,vue 是声明式的?还是命令式的?

对于 vue 而言,它的内部实现一定是 命令式 的,而我们在使用 vue 的时候,则是通过 声明式 来使用的。

也就是说: vue 封装了命令式的过程,对外暴露出了声明式的结果

性能与可维护性的权衡 ​

在明确好了命令式和声明式的概念之后。接下来咱们来看下从 性能 层面,vue 所体现出来的一种权衡的方式。

针对于性能的分析,主要从两个方面去说。

首先第一个方面:大家觉得 是命令式的性能更强,还是声明式的性能更强呢?

答案是:命令式的性能 > 声明式的性能。

其实原因非常简单,对于 命令式 的代码而言,它直接通过 原生的 JavaScript 进行实现,这是最简单的代码,没有比这个更简单的了,我们把它的性能比作 1。

而声明式,无论内部做了什么,它想要实现同样的功能,内部必然要实现同样的命令式代码。所以它的性能消耗一定是 1 + N 的。

那么既然如此,vue 为什么还要对外暴露出声明式的接口呢?

这其实是因为:声明式的可维护性,要远远大于命令式的可维护性。

大家从这两段代码(命令式和声明式代码)中就可以发现,声明式代码比命令式代码要简单的多。

越简单的代码,可维护性就越强

当性能与可维护性产生冲突时,那么舍鱼而取熊掌者也。(注意:在 vue 的性能优化之下,它并不会比纯命令式的性能差太多)

而这样的一种权衡,在 template 模板中,更是体现的淋漓尽致。

在前端领域,想要使用 JavaScript 修改 html 的方式,主要有三种:原生 JavaScript、innerHTML、虚拟 DOM

很多小伙伴都会认为 虚拟 DOM 的性能是最高的,其实不是。

我们来看这个对比。

image-20230207233547097.png

从这个对比我们可以发现,虚拟 DOM 的性能,并不是最高的。

但是它的 心智负担(书写难度)最小, 从而带来了 可维护性最高。所以哪怕它的性能并不是最高的。vue 依然选择了 虚拟 DOM 来进行了渲染层的构建。

这个也是一种性能与可维护性的权衡。

运行时和编译时 ​

第一章的最后一部分,主要讲解的就是 运行时和编译时。

这两个名词,各位小伙伴在日常开发中,应该是经常听到的。

它们两个都是框架设计的一种方式,可单独出现,也可组合使用。

那么下面咱们就分别来介绍一下它们。

首先是 运行时:runtime。

它指的是:利用 render 函数,直接把 虚拟 DOM 转化为 真实 DOM 元素 的一种方式。

在整个过程中,不包含编译的过程,所以无法分析用户提供的内容。

其次是 编译时:compiler:

它指的是:直接把 template 模板中的内容,转化为 真实 DOM 元素。

因为存在编译的过程,所以可以分析用户提供的内容。

同时,没有运行时理论上性能会更好。

目前该方式,有具体的实现库,那就是现在也非常火的 Svelte

但是这里要注意: 它的真实性能,没有办法达到理论数据。

最后是 运行时 + 编译时:

它的过程被分为两步:

  1. 先把 template 模板转化为 render 函数。也就是 编译时
  2. 再利用 render 函数,把 虚拟 DOM 转化为 真实 DOM。也就是 运行时

两者的结合,可以:

在 编译时,分析用户提供的内容 在 运行时,提供足够的灵活性

这也是 vue 的主要实现方式。

第二章:框架设计的核心要素 ​

这一章主要讲解了,框架设计时一些凌乱的注意点。

比如:

  1. 通过 环境变量 和 TreeShanking 控制打包之后的体积
  2. 构建不同的打包产物,以应用不同的场景
  3. 提供了 callWithErrorHandling 接口函数,来对错误进行统一处理
  4. 源码通过 TypeScript 开发,以保证可维护性。
  5. 内部添加了大量的类型判断和其他工作,以保证开发者使用时的良好体验。

这些东西都是基于一个个的小点单独去说的,整体之间并没有一个完成的线性逻辑。

所以大家可以根据具体感兴趣或者不了解的点,单独去看就可以。

第三章:Vue.js 3 的设计思路 ​

在这一章中,作者站在一个高层的角度,以 UI 形式、渲染器、组件、编辑器 为逻辑主线进行的讲解。

下面咱们就来捋一捋这条线。

在 Vue 中 UI 形式主要分为两种:

  • 声明式的模板描述

image-20230208170232727.png

  • 命令式的 render 函数

image-20230208170236795.png

而针对于 声明式的模板描述 而言,本质上就是咱们常用的 tempalte 模板。它会被 编辑器 编译,得到 渲染函数 render 。

渲染器与渲染函数,并 不是 一个东西。

渲染器是 函数 createRenderer 的返回值,是一个对象。被叫做 renderer。 renderer 对象中有一个方法 render,这个 render ,就是我们常说的渲染函数。

渲染函数接收两个参数 VNode 和 container。

其中 VNode 表示 虚拟 DOM,本质上是一个 JS 对象。container 是一个容器,表示被挂载的位置。而 render 函数的作用,就是: 把 vnode 挂载到 container 上。

同时,因为 Vue 以组件代表最小颗粒度,所以 vue 内部的渲染,本质上是:大量的组件渲染。

而组件本质上是一组 DOM 的集合,所以渲染一个一个的组件,本质上就是在渲染一组这一组的 DOM。也就是说,Vue 本质上是: 以组件作为介质,来完成针对于一组、一组的 DOM 渲染。

第一篇总结 ​

在整个第一篇中,作者主要就是通过这三章的内容, 自顶向下 的为我们介绍了 vue 的框架设计逻辑。其目的主要就是为了让我们了解, Vue 框架的运行逻辑和一些关键概念。

第二篇:响应式系统 ​

第二篇主要是针对 响应式系统 的讲解。

同样也是被分为三章:

  • 首先第一章,也是最重要的一章,就是 响应系统的作用与实现
  • 第二章,主要针对 对象的响应性实现原理 进行了讲解
  • 第三章,主要针对 非对象的响应性实现原理 进行了讲解

第四章:响应系统的作用与实现 ​

在这一章中,作者从 响应式数据的概念开始,讲解了响应式系统的实现。 然后针对于 计算属性与 watch 的实现原理,进行了分析。 在分析的过程中,也对其所设计到的 调度系统(scheduler) 和 惰性执行(lazy) 的原理进行了明确。 最后讲解了在 竞态问题下,关于过期的副作用的处理逻辑。

响应式数据 ​

那么首先咱们先来看基本概念 副作用函数 与 响应式数据。

所谓 副作用函数 指的是 会产生副作用的函数,这样的函数非常的多。比如

image-20230208190429676.png

在这段代码中, effect 的触发会导致全局变化 val 发生变化,那么 effect 就可以被叫做副作用函数。而如果 val 这个数据的变化,导致了视图的变化,那么 val 就被叫做 响应式数据。

那么如果想要实现响应式数据的话,那么它的核心逻辑,必然要依赖两个行为:

  • 第一个是 getter 行为,也就是 数据读取
  • 第二个是 setter 行为,也就是 数据修改

在 vue 2 中,这样的两个行为通过 Object.defineProperty 进行的实现。

在 vue 3 中,这样的两个行为通过 Proxy 进行的实现。

那么具体的实现逻辑是什么呢?咱们来看下面的图示:

首先是 getter 形式:

image-20230208191120105.png

在该函数中,存在一个 effect 方法,方法内部触发了 getter 行为。一旦 getter 行为被触发,则把对应的 effect 方法保存到一个 “桶(数据对象)” 中

当触发 setter 行为时:

image-20230208191257788.png

则会从 “桶” 中取出 effect 方法,并执行。

那么此时因为 obj.text 的值发生了变化,所以 effect 被执行时 document.body.innerText 会被赋上新的值。从而导致视图发生变化。

这是一套构建响应式系统的基础逻辑。这一套逻辑足够应对大家在日常的 面试 或者 工作 中的基本需求。

而这套逻辑说起来简单,做起来还是有一些难度的。如果想要构建出一套完善的响应式系统,那么需要做非常多的工作,篇幅也会非常长。这就不是咱们这一个视频的长度可以解决的了。

所以我在这里给大家提供了两个方案,第一个是:我在掘金上发布的博客《手写响应式模块》。第二个是:我在慕课网的视频《Vue 3 源码解析,打造自己的 vue 框架》,里面也详细的讲解并且实现了响应性模块。大家可以按需进行选择。

调度系统(scheduler) ​

那么说完了基本的响应性之后,接下来咱们来看 调度系统(scheduler)

所谓调度系统,指的就是 响应性的可调度性。

而所谓的可调度,指的就是 当数据更新的动作,触发副作用函数重新执行时,有能力决定:副作用函数(effect)执行的时机、次数以及方式

比如,在这段打印中,决定打印的顺序

image-20230208192343242.png

而想要实现一个调度系统,则需要依赖 异步:Promise 和 队列:jobQueue 来进行实现。咱们需要 基于 Set 构建出一个基本的队列数组 jobQueue,利用 Promise 的异步特性,来控制执行的顺序

计算属性(computed) ​

当我们可以控制了执行顺序之后,那么就可以利用这个特性来完成 计算属性(computed) 的实现了。

计算属性本质上是: 一个属性值,当依赖的响应式数据发生变化时,重新计算

那么它的实现就需要彻底依赖于 调度系统(scheduler) 来进行实现。

惰性执行(lazy) ​

说完计算属性,那么下面我们来看下 watch 监听器。

watch 监听器本质上是 观测一个响应式数据,当数据发生变化时,通知并执行相应的回调函数

这也就意味着,watch 很多时候并不需要立刻执行。

那么此时,就需要使用到 惰性执行(lazy) 来进行控制。

惰性执行的实现要比调度系统简单。它本质上 是一个 boolean 型的值,可以被添加到 effect 函数中,用来控制副作用的执行。

js
if (!lazy) {
  // 执行副作用函数
}

watch 的实现原理 ​

基于 调度系统 与 惰性执行,那么就可以实现 watch 监听器了。

过期的副作用 ​

watch 监听器的实现非常广泛,有时候我们甚至可以在 watch 中完成一些异步操作。

但是大量的异步操作,极有可能会导致 竞态问题。

所谓的竞态问题,指的是 在描述一个系统或者进程的输出,依赖于不受控制的事件出现顺序或者出现时机。比如咱们来看这段代码

image-20230208194352049.png

这段代码完成的是一个异步操作。

如果 obj 连续被修改了两次,那么就会发起两个请求。我们最终的期望应该是 data 被赋值为 请求B 的结果。

但是,因为异步的返回结果我们无法预计。所以,如果 请求 B 先返回,那么最终 data 的值就会变为 请求 A 的返回值。

这个咱们的期望是不一样的。

那么这样的问题,就是 竞态问题

而如果想要解决这问题,那么就需要使用到 watch 回调函数的第三个参数 onInvalidate,它本身也是一个回调函数。并且 该回调函数(onInvalidate)会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求

而 onInvalidate 的实现原理也非常简单,只需要 在副作用函数(effct)重新执行前,先触发 onInvalidate 即可。

第四章总结 ​

那么到这里,咱们就把 响应性系统的大致核心逻辑 明确完成了。从这个逻辑中,我们知道想要实现响应性数据,那么核心就是通过 Proxy 实现。

那么这个 proxy 具体怎么做呢?

接下来,咱们来看第五章。

第五章:非原始值(对象)的响应性方案 ​

书中的第五章整体而言非常简单,主要就介绍了两个接口,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() 函数的执行。

第二篇总结 ​

那么到这里咱们整个响应式系统的大概流程,就已经描述完成了。其核心逻辑主要就是在第四章中。

至于第五章和第六章,更多的偏向于具体的细节和代码逻辑。

第三篇:渲染器 ​

那么下面咱们来看 第三篇:渲染器 。

第三篇一共被分为 5 个章节。但是只讲解了三部分内容。

  • 首先第七章,主要讲解了渲染器的设计。
  • 第八章,主要讲解了 DOM 的挂载和更新的逻辑。
  • 而 第九、十、十一 这三章,主要讲解了 Diff 算法

第七章:渲染器的设计 ​

在之前咱们说过 渲染器与渲染函数不是一个东西

  • 渲染器 是 createRenderer 的返回值,是一个对象。
  • 渲染函数 是渲染器对象中的 render 方法

在 vue 3.2.37 的源码内部,createRenderer 函数的具体实现是通过 baseCreateRenderer 进行的。它的代码量非常庞大,涉及到了 2000 多行的代码。

image-20230209085155034.png

代码量虽多,但是核心思路并不是特别复杂。总体可以被分为两部分:

  1. 在浏览器端渲染时,利用 DOM API 完成 DOM 操作:比如,如果渲染 DOM 那么就使用 createElement,如果要删除 DOM 那么就使用 removeChild。
  2. 渲染器不能与宿主环境(浏览器)产生强耦合:因为 vue 不光有浏览器渲染,还包括了 服务端 渲染,所以如果在渲染器中绑定了宿主环境,那么就不好实现服务端渲染了。

在渲染的过程中,还有一个非常重要的概念 vnode。书中并没有专门的章节来介绍 vnode。所以为了避免各位小伙伴对 vnode 不了解,咱们单独把 vnode 说一下。

所谓 vnode 本身是 一个普通的 JavaScript 对象,代表了渲染的内容。对象中通过 type 表示渲染的 DOM。比如 type === div:则表示 div 标签、type === Framgnet 则表示渲染片段(vue 3 新增)、type === Text 则表示渲染文本节点。

第八章:挂载与更新 ​

对于渲染器而言,它做的最核心的事情就是 对节点进行挂载、更新的操作。作者在第八章中,详细的介绍了对应的逻辑。

整个第八章分为两部分来讲解了这个事情:

  1. DOM 节点操作
  2. 属性节点操作

DOM 节点操作 ​

首先先来看 DOM 节点操作。DOM 节点的操作可以分为三部分:

  • 挂载:所谓挂载表示节点的初次渲染。比如,可以直接通过 createElement 方法新建一个 DOM 节点,再利用 parentEl.insertBefore 方法插入节点。
  • 更新:当响应性数据发生变化时,可能会涉及到 DOM 的更新。此时的更新本质上是属于 属性的更新。咱们等到属性节点操作那里再去说。
  • 卸载:所谓卸载表示旧节点不在被需要了。那么此时就需要删除旧节点,比如可以通过 parentEl.removeChild 进行。

以上三种类型,是 vue 在进行 DOM 操作时的常见逻辑。基本上覆盖了 DOM 操作 90% 以上 的常见场景

属性节点操作 ​

看完了 DOM 操作之后,接下来咱们来看属性节点操作。

针对于属性而言,大体可以分为两类:

  1. 属性:比如 class、id、value、src...
  2. 事件:比如 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 的设置一共被分为两种:

  1. el.setAttribute('属性名', '属性值')
  2. . 属性赋值 : el.属性名 = 属性值 或者 el[属性名] = 属性值 都属于 .属性赋值

我们来看这段代码:

image-20230209093545078.png

在这段代码中,我们为 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 步不跟大家读了,因为咱们没头没尾的读一遍,其实对大家也没有什么帮助):

  1. sync from start:自前向后的对比
  2. sync from end:自后向前的对比
  3. common sequence + mount:新节点多于旧节点,需要挂载
  4. common sequence + unmount:旧节点多于新节点,需要卸载
  5. unknown sequence:乱序

而,针对于书中的这三章来说,本质上是按照 简单 diff 算法、双端 diff 算法、快速 diff 算法 的顺序把整个 diff 的前世今生基本上都说了一遍。里面涉及到了非常多的代码。

所以说咱们在当前的这个视频中,肯定是没有办法为大家讲解具体算法逻辑的。

针对于这一块,我同样也是准备了另外的博客和视频:

  • 首先是 免费的博客版 ,我把它发布到了掘金。在评论区,会通过评论的形式为大家提供地址。
  • 其次是 收费的视频版,是在慕课网发布的。同样链接会放到评论区。

第三篇总结 ​

针对于第三篇渲染器来说,咱们所描述的重点主要是围绕 渲染器的设计 和 DOM 的挂载和更新的逻辑 来去说的。

针对于这两部分而言,大家要明确 渲染器与渲染函数的区别,同时要知道 HTML Attributes 和 DOM Properties 在行为上的差异性。另外关于事件更新的 vei 概念,应该也可以给大家带来一些新的思路。

而针对于 diff,咱们没有放在当前视频中去说,主要还是因为时长不够的原因。但是我为大家准备了额外的博客和视频,大家可以根据自己需要去进行查看。

第四篇:组件化 ​

第四篇组件化,它应该算是比较简单的一个篇章,也是分为三部分来去讲解:

  1. 组件的实现原理:这是咱们讲解的重心,但是不用担心,它并不复杂。
  2. 异步组件与函数式组件:这个比较冷僻,在实际开发中的使用场景有限
  3. 内建组件和模块:里面主要讲解了 KeepAlive、Teleport、Transition 这三个内置组件的实现逻辑

第十二章:组件的实现原理 ​

想要了解 vue 中组件的实现,那么首先我们需要知道什么是组件。

组件本质上就是一个 JavaScript 对象,比如,以下对象就是一个基本的组件

image-20230209105953064.png

而对于组件而言,同样需要使用 vnode 来进行表示,当 vnode 的 type 属性是一个 自定义对象 时,那么这个 vnode 就表示组件的 vnode

image-20230209110548502.png

而组件的渲染,本质上是 组件包含的 DOM 的渲染。 对于组件而言,必然会包含一个 render 渲染函数。如果没有 render 函数,那么 vue 会把 template 模板编译为 render 函数。而组件渲染的内容,其实就是 render 函数返回的 vnode。具体的渲染逻辑,全部都通过渲染器执行。

image-20230209110919710.png

vue 3 之后提出了 composition API,composition API 包含一个入口函数,也就是 setup 函数。 setup 函数包含两种类型的返回值:

  1. 返回一个函数:当 setup 返回一个函数时,那么该函数会被作为 render 函数直接渲染。
  2. 返回一个对象:当 setup 返回一个对象时,那么 vue 会直接把该对象的属性,作为 render 渲染时的依赖数据

同时,对于组件来说还有一个 插槽 的概念。插槽的实现并不神奇。插槽本质上 是一段 innerHTML 的内容,在 vnode 中以 children 属性进行呈现。当插槽被渲染时,只需要渲染 children 即可。

对于组件来说,除了咱们常用的 对象组件 之外,vue 还提供了额外的两种组件,也就是 异步组件与函数式组件。

第十三章:异步组件与函数式组件 ​

所谓异步组件,指的是: 异步加载的组件 。

比如服务端返回一个组件对象,那么我们也可以拿到该对象,直接进行渲染。

异步组件在 优化页面性能、拆包、服务端下发组件 时,会比较有用。

而对于 函数式组件 来说,相对就比较冷僻了。函数式组件指的是 没有状态的组件。本质上是一个函数,可以通过静态属性的形式添加 props 属性 。在实际开发中,并不常见。

第十四章:内建组件和模块 ​

这一章中,主要描述了 vue 的三个内置组件。

keepAlive ​

首先第一个是 KeepAlive。

这是我们在日常开发中,非常常用的内置组件。它可以 缓存一个组件,避免该组件不断地销毁和创建。

看起来比较神奇,但是它的实现原理其实并不复杂,主要围绕着 组件卸载 和 组件挂载 两个方面:

  • 组件卸载:当一个组件被卸载时,它并不被真正销毁,而是把组件保存在一个容器中
  • 组件挂载:因为组件被保存了。所以当这个组件需要被挂载时,就不需要在重新创建,而是直接从容器中获取即可。

Teleport ​

Teleport 是 vue 3 新增的组件,作用是 将 Teleport 插槽的内容渲染到其他的位置。比如我们可以把 dialog 渲染到 body 根标签之下。

它的实现原理,主要也是分为两部分:

  1. 把 Teleport 组件的渲染逻辑,从渲染器中抽离
  2. 在指定的位置进行独立渲染

Transition ​

Transition 是咱们常用的动画组件,作用是 实现动画逻辑。

其核心原理同样被总结为两点:

  1. DOM 元素被挂载时,将动效附加到该 DOM 元素上
  2. DOM 元素被卸载时,等在 DOM 元素动效执行完成后,执行卸载 DOM 操作

第四篇总结 ​

整个第四篇,主要围绕着组件来去讲。所以内容并不复杂。

对于咱们的日常的开发与面试而言,其实只需要搞清楚 组件的原理 与 内建组件原理 即可。

第五篇:编译器 ​

编译器是一个非常复杂的环节。作者主要通过 编辑器核心逻辑、解析器、编译优化 这三个方向进行了说明。

其中对于我们日常开发与面试来说,最核心的就是 第十五章:编译器核心技术概述 。这也是咱们在这一篇中的主要章节。

第十五章:编译器核心技术概述 ​

在编译器核心技术概述,主要包含两个核心内容:

  1. 模板 DSL 的编译器
  2. Vue 编译流程三大步

模板 DSL 的编译器 ​

在任何一个编程语言中,都存在编译器的概念。 vue 的编译器是在 一种领域下,特定语言的编译器 ,那么这种编译器被叫做 DSL 编译器。

而编译器的本质是 通过一段程序,可以把 A 语言翻译成 B 语言。在 vue 中的体现就是 把 tempalte 模板,编译成 render 渲染函数

一个完整的编译器,一个分为 两个阶段、六个流程:

  • 编译前端:
    • 词法分析
    • 语法分析
    • 语义分析
  • 编译后端:
    • 中间代码生成
    • 优化
    • 目标代码生成

image-20230209113241592.png

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

image-20230209113421705.png

  1. parse:通过 parse 函数,把模板编译成 AST 对象
  2. transform:通过 transform 函数,把 AST 转化为 JavaScript AST
  3. generate:通过 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。

以下是三者的对比图

image-20230209121227934.png

而针对 vue 的服务端渲染来说,它是 将虚拟 DOM 渲染为 HTML 字符串,本质上是 解析的 vnode 对象,然后进行的 html 的字符串拼接

最后又讲解了客户端激活的原理,大致分为两步:

  1. 为页面中的 DOM 元素与虚拟节点对象之间建立联系
  2. 为页面中的 DOM 元素添加事件绑定

这两步主要是通过 renderer.hydrate() 方法进行实现了。

总结 ​

那么到这里,整个 《Vue.js 设计与实现》就已经全部说完了。

整本书中,涉及到的内容是非常全面的,就像咱们开头所说的一样,它是一个 从高层的设计角度,来探讨框架需要关注的问题。

我是 Sunday, 关注我,不迷路。和大家一起 读书,一起 分享技术知识。


如何高薪入职心仪的公司

速读《软技能 代码之外的生存指南》

8000 字读完《JavaScript 语言精粹(修订版)》

Vue 3.2 源码系列

​

Released under the MIT License.