B站 一小时读完《Vue.js 设计与实现》 笔记
《vue.js设计与实现》
尤雨溪:从高层设计的角度去探讨框架所需要关注的问题
整本书以提出问题、分析问题、解决问题的方式来讲解了vue3的核心设计。
《vue.js技术内幕》
标准的源码分析类书籍
进入《vue.js设计与实现》
本书分为6篇18章
一、框架设计概念
第一章:权衡的艺术
框架的设计,本身就是一种权衡的艺术。
命令式 和 声明式
命令式:关注过程
js
const div = document.querySelector("#app") // 获取div
div.innerText = 'hello world' // 设置文本内容
div.addEventListener('click', () => {alart('ok')}) // 绑定点击事件
声明时:关注结果
html
<div @click="() => {alart('ok')}">hello world</div>
vue 封装了命令式的过程,对外暴露出了声明式的结果
性能与可维护的权衡
命令式 和 声明式
- 命令式:直接修改的性能消耗
- 声明式:找出差异 + 直接修改
命令式的性能 > 声明式的性能
声明式的可维护性 > 命令式的可维护性
代码越少可维护性越高
原生 JavaScript、innerHTML、虚拟 DOM
通过JavaScript修改HTML的方式:原生 JavaScript、innerHTML、虚拟 DOM
心智负担(写代码时所消耗的精力)小带来高可维护性
-
心智负担:虚拟 DOM < innerHTML <原生 JavaScript
-
性能:innerHTML < 虚拟 DOM < 原生 JavaScript
-
可维护性:原生 JavaScript < innerHTML < 虚拟 DOM
运行时 和 编译时
-
运行时:runtime
利用 render 函数,直接把 虚拟 DOM 转化为真实 DOM 元素
没有编译的过程,无法分析用户提供的内容
-
编译时:compiler
直接把 template 模板中的内容,转化为 真实DOM 元素
可以分析用户提供的内容(如: svelte),没有运行时理论上性能会更好
-
运行时 + 编译时
- 先把 template 模板转化为 render 函数
- 再利用 render 函数,把虚拟 DOM 转化为真实 DOM
编译时compiler:分析用户提供的内容
运行时runtime:提供足够的灵活性
(如:vue)
第二章:框架设计的核心要素
构建框架的一些凌乱知识点
控制代码体积
- 环境变量:
__DEV__ - Tree-Shaking:ESM(ES Module)
通过上面两个去控制代码的体积
输出不同的构建产物
构建不同的打包产物来应对不同的一个使用场景
packages/vue/dist
错误提示与处理
vue内部的统一的错误处理接口:callWithErrorHandling
提升用户的开发体验
vue 内部通过 typescript 来进行实现,typescript 可以保证后续的一个可维护性。
- 心智负担低
- 便于调试
良好的可维护性
框架本身的可维护性:使用 TS 编写
对 TS 大的友好支持:大量的类型判断与处理
第三章:vue.js3 的设计思路
描述 UI 的形式
- 声明式的模板描述
html
<div :id="tId" :class="{tClass: true}" @click="onTClick"></div>
- 命令式的 render 函数
json
import {h} from 'vue'
export default {
render() {
return h('h1', {onClick: handler}) // 虚拟 dom
}
}
template 模板会被编辑器进行编译,从而的得到一个渲染函数 render
初始渲染器
渲染器本质:函数 createRenderer的返回值(renderer对象)
renderer对象:包含 render 渲染函数
-
参数
vnode: 虚拟 DOM 对象container:挂载的容器,真实 DOM 元素 -
作用:把
vnode挂载到container上
组件的本质
因为 vue 以 组件 代表最小的颗粒,所以在 vue 的内部渲染中,本质上式大量的组件渲染。
组件本质是一组 dom 元素的封装。一个 JavaScript 对象(vnode),内部封装了 DOM 元素。
渲染一个个组件,就是在渲染一组一组的 dom
vue 本质上是以组件作为介质来完成的。
模板(template)的工作原理
template 不是直接渲染 HTML,而是经过编译变成 render 函数,执行 render 生成虚拟 DOM,再由虚拟 DOM 生成真实 DOM 更新页面。
预编译(构建阶段,vue-loader/Vite):.vue单文件里的 <template>,webpack/vite 在打包阶段就完成编译,打包产物直接是 render 函数,浏览器不需要带编译器,体积更小速度更快
二、响应式系统
第四章:响应系统的作用与实现
副作用函数 与 响应式数据
-
副作用函数
会产生副作用的函数
jslet val = 1 function effect() { // 副作用函数 val = 2 //修改这个值,会导致全局变量val变化,产生副作用 } -
响应式数据
会导致视图变化的数据
响应式数据的实现
-
核心逻辑
数据读取:getter 行为
document.body.innerText = obj.text数据修改:setter行为
obj.text = 'hello vue3'核心 API :vue2 ➡ Object.defineProperty; vue3 ➡ Proxy
-
图示
getter行为
js
function effect() {
document.body.innerText = obj.text // 触发读取操作。
}
一旦触发读取操作,它就会把对应的effect()方法(副作用)f复制到一个桶(存储副作用函数的桶)里
setter行为
obj.text = 'hello vue3' ➡触发设置操作 ➡ 把 effect()从存储副作用函数的桶里取出来并执行
obj.text 重新赋值时,就会触发 effect函数,把新的值赋值到 document.body.innerText,,从而导致整个视图发生变化。
构建完善的响应式系统
掘金:vue3.2源码系列:03-看呆面试官的《手写响应式模块》
调度系统(scheduler)
-
响应性的可调度性
当数据更新的动作,触发副作用函数重新执行时,有能力决定:副作用函数(effect)执行的时机、次数以及方式
有能力调整输出顺序
1 2 '结束了'现在需求有变,输出顺序调整为:
1 '结束了' 2 -
实现原理
基于 Set 构建了队列 jobQueue,利用 Promise 的一部特性,控制执行顺序。
异步:Promise
队列:jobQueue
计算机属性(computed)
本质:一个属性值,当依赖的响应式数据发送变化时,重新计算
计算属性的实现原理 依赖调度系统实现
惰性执行(lazy)
本质:一个 Boolean 值,可以被添加到effect 函数中,用来控制副作用函数的执行
js
if (!lazy) {
// 执行副作用函数
}
watch 的实现原理
本质:观测一个响应式数据,当数据发生变化时,通知并执行相应的回调函数。
很多时候不会立刻执行,需要用到惰性执行来进行控制
实现原理:依赖调度系统和惰性执行
过期的副作用
竞态问题
它是指在描述一个系统或进程的输出,依赖于不受控制的事件出现顺序或出现时机。
(指在多线程或并发执行环境中,由于任务执行的时序不确定导致程序出现不符合预期的结果或行为的情况)
js
let finalData
watch(obj, async() => {
// 发送并等待网络请求
const res = await fetch('/path/to/request')
// 将请求结果赋值给data
finalData = res
})
第一次修改 obj ➡ 发送请求 A
第二次修改 obj ➡ 发送请求 B
将 B 的结果设置给
finalData⬅ 请求 B 返回将 A 的结果设置给
finalData⬅ 请求 A 返回
因为异步的返回结果,没办法预测哪个先返回
解决方式
onInvalidate:该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求
js
watch (obj, async(newValue, oldValue, onInvalidate) => {
// 定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期
let expired = false
// 调用 onInvalidate() 函数注册一个过期回调
onInvalidate(() => {
// 当过期时,将 expired 设置为true
expired = true
})
// 发生网络请求
const res = await fetch('/path/to/request')
// 只有当该副作用函数的执行没有过期时,才会执行后续操作
if (!expired) {
finalData = res
}
})
onInvalidate 原理
副作用函数(effect)重新执行前,先触发 onInvalidate
第五章:非原始值(对象)的响应性方案
Proxy:代理一个对象(被代理对象)的getter和setter行为,得到一个 proxy实例(代理对象)。
Reflect:在proxy中使用 this 时,保证 this 指向昂proxy,从而正确执行次数的副作用。
(动态代理和反射)
第六章:原始值(非对象)的响应性方案
为什么会有 ref
- reactive 方法基于 Proxy 实现,所有只能完成对象的响应式
- 针对 非对象 的响应性,则需要 ref 构建
实现原理
通过 get、set 函数标记符,让函数以属性调用的形式被触发
packages/reactivity/src/ref.ts
tsx
class RefImpl<T> {
private _value:T
private _rawValue:T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {}
get value () {}
set value (newVal) {}
}
当访问 ref.value 属性时,本质上时 value() 函数的执行
三、渲染器
第七章:渲染器和设计
渲染器的基本概念
渲染器与渲染函数
js
function createRenderer() {
function render(vnode, container) {
//...
}
function hydrate(vnode, container) {
//...
}
return {
render,
hydrate
}
}
渲染器: renderer createRenderer的返回值 {render,hydrate}
渲染函数:render createRenderer中 render 函数
自定义渲染器核心思路
js
fuunction baseCreateRenderer() {
// 2000多行代码
// 1. 在浏览器端,利用 DOM API 完成 DOM 操作
// 2. 渲染器不能与宿主环境(浏览器)产生强耦合。因为vue中有服务器渲染和浏览器渲染,如果与浏览器产生强耦合就不好实现服务端的渲染
}
vnode虚拟DOM
一个普通的 JavaScript 对象,代表了渲染的内容。
js
// 该 vnode 用来描述普通标签
const vnode = {
type: 'div'
// ...
}
// 该 vnode 用来描述片段
const vnode = {
type: 'Fragment'
// ...
}
// 该 vnode 用来描述文本节点
const vnode = {
type: 'Text'
// ...
}
第八章:挂载与更新
DOM节点操作
-
挂载:一个节点DOM的初次渲染
通过
createElemrnt新建通过
parentEl.insertBefore插入 -
更新:当响应性数据发生变化时,可能会涉及到 DOM 更新
属性更新(属性节点操作)
-
卸载:该节点不在被需要了
通过
parentEl.removeChild完成
属性节点操作
属性
分类
-
HTML Attributes
定义在 HTML 标签上的属性
<inpu id="my-input" type="text" value="foo" /> -
DOM Properties
DOM 对象下的属性
const el = document.querySelector('#my-input')
正确地设置元素属性
-
el.setAttributes('属性名', '属性值') -
属性赋值
el.属性名 = 属性值el[属性名] = 属性值
js
// 初始状态 <textarea class="test-class" type="text"></textarea>
// 获取 dom 实例
const el = document.querySelector('textarea')
// 1.修改 class
el.setAttributes('class', 'm-class') // 成功
el['class'] = 'm-class' // 失败
el.className ='m-class' // 成功
// 2.修改 type
el.setAttributes('type', 'input') // 成功
el['type'] = 'input' // 失败
// 3.修改 value
el.setAttributes('value', 'hello') // 失败
el['value'] = 'hello' // 成功
事件
-
添加:
el.addEventListener -
删除:
el.removeEventListener -
更新:
vei(vue event invokers)一般是先删除后添加,消耗性能
为
addEventListener回调函数,设置了一个 value 的属性方法,在回调函数中触发这个方法。通过更新该属性方法的形式,达到更新事件的目的。具体代码:
vue-next-mini中 packages/runtime-dom/src/modules/event.ts
第九、十、十一章:Diff算法
本质:一个对比的方法
"旧DOM组" 更新为"新DOM组"时,如何更新才能效率更高
步骤:
- sync from start:自前向后的对比
- sync from end:自后向前的对比
- common sequence + mount:新节点多于旧节点,需要挂载
- common sequence + unmount:旧节点多于新节点,需要卸载
- unknown sequence:乱序(子序列递增的算法,二叉树算法)
源码:packages/runtime-core/src/renderer.ts
四、组件化
第十二章:组件的实现原理
组件对象
本质就是一个JavaScript的对象
js
// MyCompoment 是一个组件,它的值是一个选项对象
const MyCompoment = {
name: 'MyCompoment',
data () {
return {foo: 1}
}
}
组件的 vnode
type 为组件对象的 vnode
js
// 该 vnode 用来描述组件,type 属性存储组件的选项对象
const vnode = {
type: MyCompoment
// ...
}
组件的渲染
一个组件,一个 effect.
render 返回的渲染函数是通过渲染器实现的
js
const MyCompoment = {
name: 'MyCompoment', // 组件名称,可选
// 组件的渲染函数,其返回值必须为虚拟 DOM
render () {
// 返回虚拟 DOM
return {
type: 'div',
children: '我是文本内容'
}
}
}
组件对象中会包含一个 render函数,render函数返回值是一个 vnode。渲染组件就是渲染该 vnode
setup 函数
<script setup>只是一个语法糖
vue3 提出了 composition api也就是 setup函数.
返回值:
-
返回一个函数
直接作为 render 函数渲染
-
返回一个对象
直接在render 中访问暴露出来的对象数据
插槽的工作原理与实现
本质:组件中 innerHTML的内容,在 vnode 中以 children 属性呈现。
原理:针对 children 进行渲染即刻
第十三章:异步组件与函数式组件
异步组件
定义:异步加载的组件
html
<template>
<CompA />
<compoment :is="asyncComp" />
</template>
<script>
import {shallowRef} from 'vue'
import CompA from 'CompA.vue'
export default {
components: {CompA},
setup () {
const asyncComp = shallowRef(null)
// 异步加载 CompB 组件
import('CompB.vue').then(CompB => asyncComp.value = CompB)
return {
asyncComp
}
}
}
</script>
作用:页面性能、拆包、服务端下发组件
函数式组件
没有状态的组件。本质上是一个函数,通过静态属性的形式添加 props 属性。
(少见)
第十四章:内建组件和模块
KeepAlive
作用:缓存一个组件,避免不断地销毁
核心原理:
- 组件被卸载时,把组件保存在一个容器里
- 组件被挂载时,从容器中把组件取出来
Teleport
作用:将插槽的内容渲染到其他位置
核心原理:
- 把
Teleport组件的渲染逻辑,从渲染器抽离出来 - 在指定的地方进行独立渲染
Transition
作用:实现动画逻辑。一个常用的动画组件
核心原理:
- DOM 元素被挂载时,将动效附加到该DOM 元素上
- DOM 元素被卸载时,等在 DOM 元素动效执行完成后,执行卸载 DOM 操作
五、编译器
第十五章:编译器核心技术概述
模板 DSL 的编译器
-
DSL:一种领域下,特定语言的编译器
-
本质:一段程序,可以把A语言翻译成 B 语言,把 template 模板,编译成 render 渲染函数
-
编译流程
- 完整的编译流程
源代码 ➡ 词法分析 ➡ 语法分析 ➡ 语义分析 ➡ 中间代码生成 ➡ 优化 ➡ 目标代码生成 ➡ 目标代码
编译前端:词法分析 ➡ 语法分析 ➡ 语术分析
编译后端:间代码生成 ➡ 优化 ➡ 目标代码生成
- Vue的编译流程
模板 ➡ 词法分析 ➡ 语法分析
⬇
模板 AST (抽象语法树) ➡ Transformer ➡ JavaScript AST ➡ 代码生成 ➡ 渲染函数
Vue 编译流程的三大步
- parse:通过 parse 函数,把模板编译成 AST 对象
- Transformer:通过Transformer 函数,把AST转化成 JavaScript AST
- generate:通过generate函数,把 JavaScript AST 转化为函数 (render)
第十六章:解析器(parse)
第十七章:编译优化
概念:通过编译的手段提取关键信息,并以此知道生成最有优代码的过程。
核心:
- 动态节点:受数据变化所影响的
- 静态节点:不受数据变化所影响的
动态节点 Block 树:
- 本质:虚拟节点树对象
- 核心:dynamic Children,收集所有动态节点
其他优化:
- 金泰提升
- 字符串化
- 缓存内联事件处理函数
- v-once 指令
六、服务的渲染
第十八章:同构渲染
CSR、SSR以及同构渲染
-
CSR:客户端渲染
浏览器拿到极简 HTML(几乎只有一个根节点),下载 JS 打包文件 → JS 在浏览器执行,在客户端生成 DOM,渲染页面。
-
SSR:服务端渲染
请求到达服务端 → 服务端执行组件代码,获取数据,直接生成完整 HTML → 返回完整 HTML 给浏览器。浏览器拿到 HTML 直接渲染出页面;之后下载 JS,激活页面交互(hydrate 水合)。
-
同构渲染(Isomorphic Rendering)
同构 = 一套代码,既可以在服务端运行,也可以在客户端运行,SSR 是同构渲染最典型的落地方式。
- 单独的 CRS
- 单独的 SSR
- CRS + SSR:首次渲染 非首次渲染
服务端渲染,将虚拟 DOM 渲染为 HTML 节点
解析 vnode,进行字符串拼接
服务端渲染,将组件 渲染为 HTML 字符串
解析 vnode,进行字符串拼接
客户端激活的原理 (renderer.hydrate())
- 为页面中的 DOM 元素与虚拟节点对象之间建立联系
- 为页面中的 DOM 元素添加事件绑定