一小时读完《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
以及 同构渲染。
第一篇:框架设计概览
整个第一篇分为三个章节:
- 权衡的艺术:这里主要涉及到了
vue
框架设计的一些基本概念,也是咱们讲解的重点 - 框架设计的核心要素:相对比较杂,都是一些零碎的知识点
- Vue.js 3 的设计思路:这一章包含了
vue
框架设计的逻辑主线,也非常重要,但是内容并不多
那么首先咱们先来看第一章。
第一章:权衡的艺术
在这一章中,开头的一句话,描述了框架设计的精髓,这句话也是尤雨溪在多个开发者大会中经常提到的,那就是:框架的设计,本身就是一种权衡的艺术。
在这一章中,书中分别从三个方面来去分析了所谓权衡的艺术,到底是什么意思。
命令式和声明式
首先第一个方面就是:命令式和声明式 的概念。
所谓 命令式 指的就是:关注过程 的范式。
而 声明式 指的就是: 关注结果 的范式。
什么意思呢?我们来举一个小例子:
张三的妈妈,让张三去买酱油。
那么对于张三而言,他就需要:拿钱、出门、下楼、进超市、拿酱油、付钱、回家。
而对于张三的妈妈来说,她完全不需要关心张三做了什么,只需要对张三说一声就可以了。
那么在这个例子中,张三就是一个典型的命令式,他需要完成整件事情的所有过程。
而张三的妈妈,就是典型的声明式,她不关心过程只关心结果。
那么这里大家来想一下,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
渲染。
第一篇总结
在整个第一篇中,作者主要就是通过这三章的内容, 自顶向下 的为我们介绍了 vue
的框架设计逻辑。其目的主要就是为了让我们了解, Vue
框架的运行逻辑和一些关键概念。
第二篇:响应式系统
第二篇主要是针对 响应式系统 的讲解。
同样也是被分为三章:
- 首先第一章,也是最重要的一章,就是 响应系统的作用与实现
- 第二章,主要针对 对象的响应性实现原理 进行了讲解
- 第三章,主要针对 非对象的响应性实现原理 进行了讲解
第四章:响应系统的作用与实现
在这一章中,作者从 响应式数据的概念开始,讲解了响应式系统的实现。 然后针对于 计算属性与 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
会被赋上新的值。从而导致视图发生变化。
这是一套构建响应式系统的基础逻辑。这一套逻辑足够应对大家在日常的 面试 或者 工作 中的基本需求。
而这套逻辑说起来简单,做起来还是有一些难度的。如果想要构建出一套完善的响应式系统,那么需要做非常多的工作,篇幅也会非常长。这就不是咱们这一个视频的长度可以解决的了。
所以我在这里给大家提供了两个方案,第一个是:我在掘金上发布的博客《手写响应式模块》。第二个是:我在慕课网的视频《Vue 3 源码解析,打造自己的 vue 框架》,里面也详细的讲解并且实现了响应性模块。大家可以按需进行选择。
调度系统(scheduler)
那么说完了基本的响应性之后,接下来咱们来看 调度系统(scheduler
)
所谓调度系统,指的就是 响应性的可调度性。
而所谓的可调度,指的就是 当数据更新的动作,触发副作用函数重新执行时,有能力决定:副作用函数(effect)执行的时机、次数以及方式
比如,在这段打印中,决定打印的顺序
而想要实现一个调度系统,则需要依赖 异步:Promise
和 队列:jobQueue
来进行实现。咱们需要 基于 Set
构建出一个基本的队列数组 jobQueue
,利用 Promise
的异步特性,来控制执行的顺序
计算属性(computed)
当我们可以控制了执行顺序之后,那么就可以利用这个特性来完成 计算属性(computed) 的实现了。
计算属性本质上是: 一个属性值,当依赖的响应式数据发生变化时,重新计算
那么它的实现就需要彻底依赖于 调度系统(scheduler) 来进行实现。
惰性执行(lazy)
说完计算属性,那么下面我们来看下 watch
监听器。
watch
监听器本质上是 观测一个响应式数据,当数据发生变化时,通知并执行相应的回调函数
这也就意味着,watch
很多时候并不需要立刻执行。
那么此时,就需要使用到 惰性执行(lazy
) 来进行控制。
惰性执行的实现要比调度系统简单。它本质上 是一个 boolean
型的值,可以被添加到 effect
函数中,用来控制副作用的执行。
if (!lazy) {
// 执行副作用函数
}
watch 的实现原理
基于 调度系统 与 惰性执行,那么就可以实现 watch
监听器了。
过期的副作用
watch
监听器的实现非常广泛,有时候我们甚至可以在 watch
中完成一些异步操作。
但是大量的异步操作,极有可能会导致 竞态问题。
所谓的竞态问题,指的是 在描述一个系统或者进程的输出,依赖于不受控制的事件出现顺序或者出现时机。比如咱们来看这段代码
这段代码完成的是一个异步操作。
如果
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
多行的代码。
代码量虽多,但是核心思路并不是特别复杂。总体可以被分为两部分:
- 在浏览器端渲染时,利用
DOM API
完成DOM
操作:比如,如果渲染DOM
那么就使用createElement
,如果要删除DOM
那么就使用removeChild
。 - 渲染器不能与宿主环境(浏览器)产生强耦合:因为
vue
不光有浏览器渲染,还包括了服务端
渲染,所以如果在渲染器中绑定了宿主环境,那么就不好实现服务端渲染了。
在渲染的过程中,还有一个非常重要的概念 vnode
。书中并没有专门的章节来介绍 vnode
。所以为了避免各位小伙伴对 vnode
不了解,咱们单独把 vnode
说一下。
所谓 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
的前世今生基本上都说了一遍。里面涉及到了非常多的代码。
所以说咱们在当前的这个视频中,肯定是没有办法为大家讲解具体算法逻辑的。
针对于这一块,我同样也是准备了另外的博客和视频:
第三篇总结
针对于第三篇渲染器来说,咱们所描述的重点主要是围绕 渲染器的设计 和 DOM
的挂载和更新的逻辑 来去说的。
针对于这两部分而言,大家要明确 渲染器与渲染函数的区别,同时要知道 HTML Attributes
和 DOM Properties
在行为上的差异性。另外关于事件更新的 vei
概念,应该也可以给大家带来一些新的思路。
而针对于 diff
,咱们没有放在当前视频中去说,主要还是因为时长不够的原因。但是我为大家准备了额外的博客和视频,大家可以根据自己需要去进行查看。
第四篇:组件化
第四篇组件化,它应该算是比较简单的一个篇章,也是分为三部分来去讲解:
- 组件的实现原理:这是咱们讲解的重心,但是不用担心,它并不复杂。
- 异步组件与函数式组件:这个比较冷僻,在实际开发中的使用场景有限
- 内建组件和模块:里面主要讲解了
KeepAlive
、Teleport
、Transition
这三个内置组件的实现逻辑
第十二章:组件的实现原理
想要了解 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 AST
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
。
以下是三者的对比图
而针对 vue
的服务端渲染来说,它是 将虚拟 DOM
渲染为 HTML
字符串,本质上是 解析的 vnode
对象,然后进行的 html
的字符串拼接
最后又讲解了客户端激活的原理,大致分为两步:
- 为页面中的
DOM
元素与虚拟节点对象之间建立联系 - 为页面中的
DOM
元素添加事件绑定
这两步主要是通过 renderer.hydrate()
方法进行实现了。
总结
那么到这里,整个 《Vue.js 设计与实现》就已经全部说完了。
整本书中,涉及到的内容是非常全面的,就像咱们开头所说的一样,它是一个 从高层的设计角度,来探讨框架需要关注的问题。
我是 Sunday
, 关注我,不迷路。和大家一起 读书,一起 分享技术知识。
8000 字读完《JavaScript 语言精粹(修订版)》