书阅读地址
本书内容
本书内容并非“源码解读”,而是建立在笔者对 Vue.js 框架设计的理解之上,以由简入繁的方式介绍如何实现 Vue.js 中的各个功能模块。本书将尽可能地从规范出发,实现功能完善且严谨的 Vue.js 功能模块。例如,通过阅读 ECMAScript 规范,基于 Proxy 实现一个完善的响应系统;通过阅读WHATWG 规范,实现一个类 HTML 语法的模板解析器,并在此基础上实现一个支持插件架构的模板编译器。
除此之外,本书还会讨论以下内容:
- 框架设计的核心要素以及框架设计过程中要做出的权衡;
- 三种常见的虚拟 DOM(Virtual DOM)的 Diff 算法;
- 组件化的实现与 Vue.js 内建组件的原理;
- 服务端渲染、客户端渲染、同构渲染之间的差异,以及同构渲染的原理。
笔记
第一篇 (框架设计概述)
vue3描述ui的两种方式 模板描述 和 虚拟dom描述
// 模板描述
<div @click="hander">{{1}}</div>
// 虚拟dom描述
import {h} from "vue"
export default {
render(){
return h('div',{onClick:hander})
}
}
虚拟 DOM,它其实就是用 JavaScript 对象来描述真实的 DOM 结构
h函数的作用是创建一个vnode。
第二篇(响应系统)
代码
笔记
1、vue响应系统的实现核心是借助了两个api,Object.defineProperty(vue2.0)
和Proxy(vue3.0)
它的工作流程:
2、computed原理是在响应函数的基础上进行二次封装的,它内部设置了dirty、cache、obj。通过调度参数options的
lazy
与scheduler
来获取响应函数执行副作用的状态,从而修改cahce。
function computed(fn) {
let cache;
let dirty = true;
let cuttentEffect = effect(fn,{
lazy:true,
scheduler(effectFn){
dirty = true
effectFn()
}
})
let obj = {
get value(){
if(dirty) {
console.log('脏数据,重新计算')
cache = cuttentEffect()
dirty = false;
}
return cache
}
}
return obj
}
let obj1 = computed(()=>{
return proxyObj.key1 + proxyObj.key2
})
console.log('computed',obj1.value)
console.log('computed',obj1.value)
proxyObj.key1 = 20
console.log('computed',obj1.value)
2、vue3中reactive
、ref
、toRef
、toRefs
的关系
1、reactive是用proxy来进行代理实现的,
因此它不能监听原始数据类型string、number、boolean的变化。
也不能属性赋值或解构赋值
2、ref是对reactive函数的二次封装,使它支持了对原始数据的监听
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,为后面的拖ref服务
Object.defineProperty(wrapper,'__v_isRef',{value:true})
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
let a = ref(1)
3、toRef是对`响应式数据`中键值的封装,使单独的键值具有响应式。
// 工作原理
function toRef(obj,key) {
const wrapper = {
get value(){
return obj[key]
}
set value(val){
obj[key] = val
}
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,为后面的拖ref服务
Object.defineProperty(wrapper,'__v_isRef',{value:true})
return wrapper
}
let obj = reactive({a:1})
let a = toRef(obj,'a')
4、toRefs是对`响应式数据`中所有键值的封装,使它的响应式支持属性赋值或解构赋值。
// 工作原理
function toRefs(obj) {
const ret = {}
for(const key in obj) {
ret[key] = toRef(obj,key)
}
return ret
}
let obj = reactive({a:1})
let obj1 = toRefs(obj)
let {a} = obj1
3、为什么我们可以在模板中
直接访问/设置一个 ref 的值,而无须通过 value 属性来访问
<p>{{a}}</p>
<p>{{a = 20}}</p>
是因为使用了proxyRefs函数,使其具有自动脱ref的能力。在编写 Vue.js 组件时,组件中的 setup 函数所返回的数据会在内部传递给proxyRefs 函数进行处理.这样用户在模板中
使用响应式数据时,将不再需要关心哪些是 ref,哪些不是 ref。
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
// 通过 target 读取真实值
const value = target[key]
// 如果值是 Ref,则设置其对应的 value 属性值
if (value.__v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
})
}
第三篇(渲染器)
代码
笔记
Diff 算法:
当新旧 vnode 的子节点都是一组节点时,
为了以最小的性能开销完成更新操作,需要比较两组子节点,
用于比较的算法就叫作 Diff 算法:
简单diff算法的思想
遍历新旧两组子节点中数量较少的那一组,并逐个调用 patch 函数进行打补丁,
然后比较新旧两组子节点的数量,如果新的一组子节点数量更多,说明有新子节点需要挂载;
否则说明在旧的一组子节点中,有节点需要卸载。
在更新时,渲染器通过 key 属性找到可复用的节点,然后尽可能地通过 DOM 移动操作来完成更新.
简单 Diff 算法的核心逻辑是,拿新的一组子节点中的节点去旧的一组子节点中寻找可复用的节点。
如果找到了,则记录该节点的位置索引。我们把这个位置索引称为最大索引。
在整个更新过程中,如果一个节点的索引值小于最大索引,则说明该节点对应的真实DOM 元素需要移动。
双端diff算法
双端 Diff 算法指的是,在新旧两组子节点的四个端点之间分别进行比较,并试图找到可复用的节点。
相比简单 Diff 算法,双端 Diff 算法的优势在于,对于同样的更新场景,执行的DOM 移动操作次数更少。
快速diff算法
快速 Diff 算法在实测中性能最优。
它借鉴了文本 Diff 中的预处理思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。
当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的节点来完成更新,
则需要根据节点的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
第四篇(组件化)
keepAlive组件
Teleport组件
Transition组件
第五篇(编译器)
代码
简单版本的模板
到模板AST
再到JavaScriptAST
最后到字符串代码
的流程
code.juejin.cn/pen/7165902…
笔记
编译器的作用其实就是将模板编译为渲染函数。
从图可知,template 转 render
模板编译器的工作流程:
- 分析模板,将其解析为模板 AST。
- 将模板 AST 转换为用于描述渲染函数的 JavaScript AST。
- 根据 JavaScript AST 生成渲染函数代码。
第六篇(服务端渲染)
代码
笔记
传统的服务端渲染的用户体验非常差,任何一个微小的操作都可能导致页面刷新。
同构渲染中的首次渲染页面是纯静态的,这意味着用户还不能与页面进行任何交互,因为整个应用程序的脚本还没有加载和执行。
对于同构渲染来说,组件的代码会在服务端和客户端分别执行一次,这意味着,此时页面中已经存在对应的 DOM 元素。同时,该组件还会被打包到一个 JavaScript 文件中,并在客户端被下载到浏览器中解释并执行。这时问题来了,当组件的代码在客户端执行时,会再次创建 DOM 元素吗?答案是“不会”。由于浏览器在渲染了由服务端发送过来的 HTML 字符串之后,页面中已经存在对应的 DOM 元素了,所以组件代码在客户端运行时,不需要再次创建相应的 DOM 元素。但是,组件代码在客户端运行时,仍然需要做两件重要的事:
- 在页面中的 DOM 元素与虚拟节点对象之间建立联系;
- 为页面中的 DOM 元素添加事件绑定。
使用跨平台的 API。由于组件的代码既要在浏览器中运行,也要在服务器中运行,所以编写组件代码时,要额外注意代码的跨平台性。 import.meta.env.SSR 来做代码守卫。通常我们在选择第三方库的时候,会选择支持跨平台的库,例如使用 Axios 作为网络请求库。