Vue.js 设计与实现
书籍链接
前言
Vue.js 3.0 在模块的拆分和设计上做得非常合理。模块之间的耦合度非常低,很多模块可以独立安装使用,而不需要依赖完整的 Vue.js 运行时,例如 @vue/reactivity 模块。
Vue.js 3.0 在设计内建组件和模块时也花费了很多精力,配合构建工具以及 Tree-Shaking 机制,实现了内建能力的按需引入,从而实现了用户 bundle 的体积最小化。
Vue.js 3.0 的扩展能力非常强,我们可以编写自定义的渲染器,甚至可以编写编译器插件来自定义模板语法。同时,Vue.js 3.0 在用户体验上也下足了功夫。
另外,Vue.js 3.0 中很多功能的设计需要谨遵规范。例如,想要使用 Proxy 实现完善的响应系统,就必须从 ECMAScript 规范入手,而 Vue.js 的模板解析器则遵从 WHATWG 的相关规范。所以,在理解 Vue.js 3.0 核心设计思想的同时,我们还能够间接掌握阅读和理解规范,并据此编写代码的方法。
本书内容并非“源码解读”,而是建立在笔者对 Vue.js 框架设计的理解之上,以由简入繁的方式介绍如何实现 Vue.js 中的各个功能模块。
本书将尽可能地从规范出发,实现功能完善且严谨的 Vue.js 功能模块。例如,通过阅读 ECMAScript 规范,基于 Proxy 实现一个完善的响应系统;通过阅读 WHATWG 规范,实现一个类 HTML 语法的模板解析器,并在此基础上实现一个支持插件架构的模板编译器。
除此之外,本书还会讨论以下内容:
● 框架设计的核心要素以及框架设计过程中要做出的权衡;
● 三种常见的虚拟 DOM(Virtual DOM)的 Diff 算法;
● 组件化的实现与 Vue.js 内建组件的原理;
● 服务端渲染、客户端渲染、同构渲染之间的差异,以及同构渲染的原理。
本书结构
第一篇(框架设计概览)
- 第 1 章主要讨论了命令式和声明式这两种范式的差异,以及二者对框架设计的影响,还讨论了虚拟 DOM 的性能状况,最后介绍了运行时和编译时的相关知识,并介绍了 Vue.js 3.0 是一个运行时 + 编译时的框架。
- 第 2 章主要从用户的开发体验、控制框架代码的体积、Tree-Shaking 的工作机制、框架产物、特性开关、错误处理、TypeScript 支持等几个方面出发,讨论了框架设计者在设计框架时应该考虑的内容。
- 第 3 章从全局视角介绍 Vue.js 3.0 的设计思路,以及各个模块之间是如何协作的。
第二篇(响应系统)
- 第 4 章从宏观视角讲述了 Vue.js 3.0 中响应系统的实现机制。从副作用函数开始,逐步实现一个完善的响应系统,还讲述了计算属性和 watch 的实现原理,同时讨论了在实现响应系统的过程中所遇到的问题,以及相应的解决方案。
- 第 5 章从 ECMAScript 规范入手,从最基本的 Proxy、Reflect 以及 JavaScript 对象的工作原理开始,逐步讨论了使用 Proxy 代理 JavaScript 对象的方式。
- 第 6 章主要讨论了 ref 的概念,并基于 ref 实现原始值的响应式方案,还讨论了如何使用 ref 解决响应丢失问题。
第三篇(渲染器)
- 第 7 章主要讨论了渲染器与响应系统的关系,讲述了两者如何配合工作完成页面更新,还讨论了渲染器中的一些基本名词和概念,以及自定义渲染器的实现与应用。
- 第 8 章主要讨论了渲染器挂载与更新的实现原理,其中包括子节点的处理、属性的处理和事件的处理。当挂载或更新组件类型的虚拟节点时,还要考虑组件生命周期函数的处理等。
- 第 9 章主要讨论了“简单 Diff 算法”的工作原理。
- 第 10 章主要讨论了“双端 Diff 算法”的工作原理。
- 第 11 章主要讨论了“快速 Diff 算法”的工作原理。
第四篇(组件化)
- 第 12 章主要讨论了组件的实现原理,介绍了组件自身状态的初始化,以及由自身状态变化引起的组件自更新,还介绍了组件的外部状态(props)、由外部状态变化引起的被动更新,以及组件事件和插槽的实现原理。
- 第 13 章主要介绍了异步组件和函数式组件的工作机制和实现原理。对于异步组件,我们还讨论了超时与错误处理、延迟展示 Loading 组件、加载重试等内容。
- 第 14 章主要介绍了 Vue.js 内建的三个组件的实现原理,即 KeepAlive、Teleport 和 Transition 组件。
第五篇(编译器)
- 第 15 章首先讨论了 Vue.js 模板编译器的工作流程,接着讨论了 parser 的实现原理与状态机,以及 AST 的转换与插件化架构,最后讨论了生成渲染函数代码的具体实现。
- 第 16 章主要讨论了如何实现一个符合 WHATWG 组织的 HTML 解析规范的解析器,内容涵盖解析器的文本模式、文本模式对解析器的影响,以及如何使用递归下降算法构造模板 AST。在解析文本内容时,我们还讨论了如何根据规范解码字符引用。
- 第 17 章主要讨论了 Vue.js 3.0 中模板编译优化的相关内容。具体包括:Block 树的更新机制、动态节点的收集、静态提升、预字符串化、缓存内联事件处理函数、v-once 等优化机制。
第六篇(服务端渲染)
- 第 18 章主要讨论了 Vue.js 同构渲染的原理。首先探讨了 CSR、SSR 以及同构渲染等方案各自的优缺点,然后探讨了 Vue.js 进行服务端渲染和客户端激活的原理,最后总结了编写同构代码时的注意事项。
书中代码实现部分
响应系统的作用与实现
function traverse(value, seen = new Set()) {
// 如果要读取地数据是原始值,或者已被读取过,那么什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设 value就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}
// watch
function watch(source, cb, options = {}) {
// 定义 getter
let getter
// 如果 source 是函数,说明用户传递的是 getter,所以直接把 source 赋值给 getter
if (typeof source === 'function') {
getter = source
} else {
// 否则调用 traverse 递归地读取
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// cleanup 用来存储用户注册的过期回调
let cleanup
// 定义 onInvalidate 函数
function onInvalidate(fn) {
// 将过期回调存储到 cleanup 中
cleanup = fn
}
// 提取 scheduler 调度函数作为一个独立的 job 函数
const job = () => {
// 在 sheduler 中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数 cb 之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数 cb, 将旧值和新值作为参数,将 onInvalidate 作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次慧得到错误的旧值
oldValue = newValue
}
// 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到 effectFn 中以便后续手动调用
const effectFn = effect(() => getter(), {
lazy: true,
// 使用 job 函数作为调度器函数
scheduler: () => {
// 当 flush 的值为 'post' 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
},
})
if (options.immediate) {
// 当 immediate 为 true 时立即执行 job, 从而触发回调执行
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 计算属性
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志,用来标识是否需要重新计算值,为 true 则意味着“脏”,需要计算
let dirty = true
// 把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
// 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应
trigger(obj, 'value')
},
})
const obj = {
// 当读取 value 时才执行 effectFn
get value() {
// 只有“脏”时才计算值,并将得到的值缓存到 value 中
if (dirty) {
value = effectFn()
// 将 dirty 设置为false, 下一次访问直接使用缓存到 value 中的值
dirty = false
}
// 当读取 value 时,手动调用 track 函数进行追踪
track(obj, 'value')
return value
},
}
return obj
}
// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
// 如果队列正在刷新,则什么都不做
if (isFlushing) return
// 设置为 true, 代表正在刷新
isFlushing = true
// 在微任务队列中刷新 jobQueue 队列
p.then(() => {
jobQueue.forEach((job) => job())
}).finally(() => {
isFlushing = false
})
}
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 如果没有 activeEffect,直接 return
if (!activeEffect) return
// 根据target 从“桶”中取得depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target)
// 如果不存在depsMap,新建一个 Map 并与 target 建立关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 再根据 key 从 depsMap 中取得 deps, 它是一个 Set 类型,里面存储着所有与当前 key 关联的副作用函数:effects
let deps = depsMap.get(key)
// 如果不存在deps, 新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把当前激活的副作用函数添加到依赖集合 deps 中
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps) // 新增
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶里取得 depsMap,它是 key -> effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 根据key取得所有的副作用函数 effects
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行副作用函数
effectFn()
}
})
}
// 用一个全局变量存储当前激活的副作用函数
let activeEffect
// effect 栈
const effectStack = []
/**
* cleanup 函数接收副作用函数作为参数,遍历副作用函数的 effectFn.deps 数组,该数组的每一项都是一个依赖集合,
* 然后将该副作用函数从依赖集合中移除,
* 最后重置 effectFn.deps 数组。
*
*/
function cleanup(effectFn) {
// 遍历effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn)
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
// effect函数用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn) // 新增
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将 fn 的执行结果存储到 res 中
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 返回结果
return res
}
// 将 options 挂载到 effectFn 上
effectFn.options = options // 新增
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 只有非 lazy 的时候,才执行
if (!options.lazy) {
effectFn()
}
// 将副作用函数作为返回值返回
return effectFn
}
// 存储副作用函数的"桶"
const bucket = new WeakMap()
// 原始数据
const data = {
foo: 1,
bar: 2,
}
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶中取出并执行
trigger(target, key)
},
})
非原始值的响应式方案
// 唯一key标识
const ITERATE_KEY = Symbol()
const MAP_KEY_ITERATE_KEY = Symbol()
// 存储副作用函数的"桶"
const bucket = new WeakMap()
function traverse(value, seen = new Set()) {
// 如果要读取地数据是原始值,或者已被读取过,那么什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设 value就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}
// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
// 如果队列正在刷新,则什么都不做
if (isFlushing) return
// 设置为 true, 代表正在刷新
isFlushing = true
// 在微任务队列中刷新 jobQueue 队列
p.then(() => {
jobQueue.forEach((job) => job())
}).finally(() => {
isFlushing = false
})
}
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 如果没有 activeEffect 或者 禁止追踪 直接 return
if (!activeEffect || !shouldTrack) return
// 根据target 从“桶”中取得depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target)
// 如果不存在depsMap,新建一个 Map 并与 target 建立关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 再根据 key 从 depsMap 中取得 deps, 它是一个 Set 类型,里面存储着所有与当前 key 关联的副作用函数:effects
let deps = depsMap.get(key)
// 如果不存在deps, 新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把当前激活的副作用函数添加到依赖集合 deps 中
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps) // 新增
}
const TriggerType = {
SET: 'SET',
ADD: 'ADD',
DELETE: 'DELETE',
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key, type, newVal) {
// 根据 target 从桶里取得 depsMap,它是 key -> effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 根据key取得所有的副作用函数 effects
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 操作类型为 ADD 或 DELETE 时,并且为 Map 类型数据
if (
(type === TriggerType.ADD || type === TriggerType.DELETE) &&
Object.prototype.toString.call(target) === '[object Map]'
) {
const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
// 只有当操作类型为 ADD 或 DELETE 时,才触发与 ITERATE_KEY 相关联的副作用函数重新执行
// 如果操作类型为SET, 并且目标对象为 Map 类型的数据。也应该触发那些与 ITERATE_KEY相关联的副作用函数重新执行
if (
type === TriggerType.ADD ||
type === TriggerType.DELETE ||
(type === TriggerType.SET &&
Object.prototype.toString.call(target) === '[object Map]')
) {
// 取得与 TERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
// 当操作类型为 ADD 并且目标对象是数组时,应该取出并执行那些与 length 属性
// 相关联的副作用函数
if (type === TriggerType.ADD && Array.isArray(target)) {
// 取出与 length 相关联的副作用函数
const lengthEffects = depsMap.get('length')
// 将这些副作用函数添加到 effectsToRun 中,待执行
lengthEffects &&
lengthEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
// 如果操作目标是数组,并且修改了数组的 length 属性
if (Array.isArray(target) && key === 'length') {
// 对于索引大于或等于新的 length 值的元素,
// 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
depsMap.forEach((effects, key) => {
// 字符串不是一个有效的数字,会将其转换为特殊的 NaN, NaN与任何数值进行比较都会返回false。
if (key >= newVal) {
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
})
}
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行副作用函数
effectFn()
}
})
}
// 用一个全局变量存储当前激活的副作用函数
let activeEffect
// effect 栈
const effectStack = []
/**
* cleanup 函数接收副作用函数作为参数,遍历副作用函数的 effectFn.deps 数组,该数组的每一项都是一个依赖集合,
* 然后将该副作用函数从依赖集合中移除,
* 最后重置 effectFn.deps 数组。
*
*/
function cleanup(effectFn) {
// 遍历effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn)
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
// effect函数用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn) // 新增
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将 fn 的执行结果存储到 res 中
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 返回结果
return res
}
// 将 options 挂载到 effectFn 上
effectFn.options = options // 新增
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 只有非 lazy 的时候,才执行
if (!options.lazy) {
effectFn()
}
// 将副作用函数作为返回值返回
return effectFn
}
function readonly(obj) {
return createReactive(obj, false, true)
}
function shallowReadonly(obj) {
return createReactive(obj, true, true)
}
// 定义一个 Map 实例,存储原始对象到代理对象的映射
const reactiveMap = new Map()
function reactive(obj) {
// 优先通过原始对象 obj 寻找之前创建的代理对象,如果找到了,直接返回已有的代理对象
const existionProxy = reactiveMap.get(obj)
if (existionProxy) return existionProxy
// 否则,创建新的代理对象
const proxy = createReactive(obj)
// 存储到 Map 中,从而避免重复创建
reactiveMap.set(obj, proxy)
return proxy
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
const arrayInstrumentations = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach((method) => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
// this 是代理对象,先在代理对象中查找,将结果存储到 res 中
let res = originMethod.apply(this, args)
if (res === false || res == -1) {
// res 为 false 说明没找到,通过 this.raw 拿到原始数组,再去其中查找并更新 res 值
res = originMethod.apply(this.raw, args)
}
// 返回最终结果
return res
}
})
// 一个标记变量,代表是否进行追踪,默认值为 true, 即允许追踪
let shouldTrack = true
// 重写数组的 push 方法
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach((method) => {
// 取得原始 push 方法
const originMethod = Array.prototype[method]
// 重写
arrayInstrumentations[method] = function (...args) {
// 在调用原始方法前,禁止追踪
shouldTrack = false
// push 方法的默认行为
let res = originMethod.apply(this, args)
// 在调用原始方法之后,恢复原来的行为,即允许追踪
shouldTrack = true
return res
}
})
// 抽离为独立函数,便于复用
function iterationMethod() {
// 获取原始数据对象 target
const target = this.raw
// 获取原始迭代器方法
const itr = target[Symbol.iterator]()
const wrap = (val) =>
typeof val === 'object' && val !== null ? reactive(val) : val
// 调用 track 函数建立响应联系
track(target, ITERATE_KEY)
// 将其返回
return {
next() {
// 调用原始迭代器的 next 方法获取 value 和 done
const { value, done } = itr.next()
return {
// 如果 value 不是 undefined, 则对其进行包裹
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done,
}
},
// 实现可迭代协议
[Symbol.iterator]() {
return this
},
}
}
function valuesIterationMethod() {
// 获取原始数据对象 target
const target = this.raw
// 通过 target.values 获取原始迭代器方法
const itr = target.values()
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
track(target, ITERATE_KEY)
return {
next() {
const { value, done } = itr.next()
return {
value: wrap(value),
done,
}
},
[Symbol.iterator]() {
return this
},
}
}
function keysIterationMethod() {
// 获取原始数据对象 target
const target = this.raw
// 通过 target.values 获取原始迭代器方法
const itr = target.keys()
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
// 调用 track 函数追踪依赖,在副作用函数与 MAP_KEY_ITERATE_KEY 之间建立响应联系
track(target, MAP_KEY_ITERATE_KEY)
return {
next() {
const { value, done } = itr.next()
return {
value: wrap(value),
done,
}
},
[Symbol.iterator]() {
return this
},
}
}
// 定义一个对象,将自定义的 add 方法定义到该对象下
const mutableInstrumentations = {
add(key) {
// this 仍指向代理对象,通过 raw 属性获取原始数据对象。
const target = this.raw
const hadKey = target.has(key)
// 通过原始数据对象执行 add 方法添加具体的值。
// 注意,这里不再需要.bind 了,因为是直接通过 target 调用并执行的
const res = target.add(key)
if (!hadKey) {
// 调用 trigger 函数触发响应,并指定操作类型为 ADD
trigger(target, key, TriggerType.ADD)
}
// 返回操作结果
return res
},
delete(key) {
const target = this.raw
const hadKey = target.has(key)
const res = target.delete(key)
// 当要删除的元素确实存在时,才触发响应
if (hadKey) {
trigger(target, key, TriggerType.DELETE)
}
return res
},
get(key) {
// 获取原始对象
const target = this.raw
// 判断读取的 key 是否存在
const had = target.has(key)
// 追踪依赖,建立响应联系
track(target, key)
// 如果存在,则返回结果。这里要注意的是,如果得到的结果 res 仍然是可代理的数据
// 则要返回使用 reactive 包装后的响应式数据
if (had) {
const res = target.get(key)
return typeof res === 'object' ? reactive(res) : res
}
},
set(key, value) {
const target = this.raw
const had = target.has(key)
// 获取旧值
const oldValue = target.get(key)
// 获取原始数据,由于 value 本身可能已经是原始数据,所以此时 value.raw 不存在,则直接使用 value
const rawValue = value.raw || value
// 设置新值
target.set(key, rawValue)
// 如果不存在,则说明是 ADD 类型的操作,意味着新增
if (!had) {
trigger(target, key, TriggerType.ADD)
} else if (
oldValue !== value ||
(oldValue === oldValue && value === value)
) {
// 如果不存在,且值改变,则是 SET 类型的操作,意味着修改
trigger(target, key, TriggerType.SET)
}
},
forEach(callback, thisArg) {
const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
const target = this.raw
track(target, ITERATE_KEY)
target.forEach((v, k) => {
callback.call(thisArg, wrap(v), wrap(k), this)
})
},
[Symbol.iterator]: iterationMethod,
entries: iterationMethod,
values: valuesIterationMethod,
keys: keysIterationMethod,
}
// 封装 createReactive 函数,接收一个参数 isShallow,代表是否为浅响应,默认为false,即非浅响应
// 第三个参数 isReadonly,代表是否只读,默认为 false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
// 代理对象可以通过 raw 属性访问原始数据
if (key === 'raw') {
return target
}
if (key === 'size') {
track(target, ITERATE_KEY)
return Reflect.get(target, key, target)
}
// 返回定义在 mutableInstrumentations 对象下的方法
return mutableInstrumentations[key]
// 如果操作的目标对象是数组,并且 key 存在于 arrayInstrumentations
// 那么返回定义在 arrayInstrumentations 上的值
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 非只读,且key的类型不为 symbol 的时候才需要建立响应联系
if (!isReadonly && typeof key !== 'symbol') {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
}
// 使用 Reflect.get 返回读取到的属性值
const res = Reflect.get(target, key, receiver)
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
// 调用 reactive 将结果包装成响应式数据并返回
return isReadonly ? readonly(res) : reactive(res)
}
// 正常返回 res
return res
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
// 如果代理目标是数组,则检测被设置的索引值是否小于数组长度
// 如果是,则视作 SET 操作,否则是 ADD 操作
const type = Array.isArray(target)
? Number(key) < target.length
? TriggerType.SET
: TriggerType.ADD
: Object.prototype.hasOwnProperty.call(target, key)
? TriggerType.SET
: TriggerType.ADD
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// target === receiver.raw 说明 receiver 就是 target 的代理对象
if (target === receiver.raw) {
// 比较新值和旧值,只有不全等,并且不都是 NaN 时才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 把副作用函数从桶中取出并执行,把 type 作为第三个参数传递给 trigger 函数
trigger(target, key, type, newVal)
}
}
return res
},
// 拦截删除操作
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key} 是只读的`)
return true
}
// 检查被操作的属性是否是对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用 Reflect.deleteProperty 完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, TriggerType.DELETE)
}
},
// 拦截 in 操作
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
// 拦截 for ... in 操作
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联, 如果操作目标 target 是数组,则使用 length 属性作为 key 并建立响应联系
track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
},
})
}
原始值的响应式方案
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val,
}
// 实现区分一个数据为ref:在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为true
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
})
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
set value(val) {
obj[key] = val
},
}
// 定义 __v_isRef 属性
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
})
return wrapper
}
function toRefs(obj) {
const ret = {}
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key)
}
return ret
}
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 自动脱 ref 实现,如果读取的值是 ref, 则返回它的 value 属性值
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)
},
})
}
挂载与更新
// 文本节点的 type 标识
const Text = Symbol()
// 注释节点的 type 标识
const Comment = Symbol()
// 片段节点的 type 标识
const Fragment = Symbol()
// 返回 false 说明不应该作为 DOM Properties 设置
function shouldSetAsProps(el, key, value) {
// 特殊处理 form 为只读属性,只能通过setAttribute设置
if (key === 'form' && el.tagName === 'INPUT') {
return false
}
// 兜底
return key in el
}
function createRenderer(options) {
// 通过 options 得到操作DOM的API
const { createElement, insert, setElementText, patchProps } = options
function patchChildren(n1, n2, container) {
// 判断新子节点的类型是否为文本节点
if (typeof n2.children === 'string') {
// 旧子节点的类型有三种可能性:无子节点, 文本子节点,数组子节点
// 只有当旧子节点为数组子节点时,才需要逐个卸载,其他情况下什么都不做
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 说明新子节点是数组子节点
// 判断旧子节点是否也是数组子节点
if (Array.isArray(n1.children)) {
// 核心diff算法
// 简单做法:卸载旧的,挂载新的
n1.forEach((c) => unmount(c))
setElementText(container, n2.children)
} else {
// 旧子节点为文本子节点或不存在
setElementText(container, '')
n2.children.forEach((c) => patch(null, c, container))
}
} else {
// 新子节点不存在
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
} else if (typeof n1.children === 'string') {
setElementText(container, '')
}
// 如果旧子节点也不存在,什么都不需要做
}
}
function patchElement(n1, n2) {
const el = (n2.el = n1.el)
const oldProps = n1.props
const newProps = n2.props
// 1) 更新props
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
// 2) 更新 children
patchChildren(n1, n2, el)
}
function mountElement(vnode, container) {
// 让 vnode.el 引用真实 DOM 元素
const el = (vnode.el = createElement(vnode.type))
// 如果 children 是字符串类型,直接设置为文本内容
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
// 如果 children 为数组类型,分发新增子节点
vnode.children.forEach((child) => {
patch(null, child, el)
})
}
// 如果 vnode.props 存在才处理它
if (vnode.props) {
for (const key in vnode.props) {
// 调用 patchProps 函数即可
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container)
}
function patch(n1, n2, container) {
// 如果 n1 存在,则对比 n1 和 n2 的类型
if (n1 && n1.type !== n2.type) {
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// 普通标签元素
if (!n1) {
mountElement(n2, container)
} else {
patchElement(n1, n2)
}
} else if (typeof type === 'object') {
// 如果 n2.type 的值的类型是对象,则它描述的是组件
} else if (type === Text) {
// 如果新 vnode 的类型是 Text,说明该 vnode 描述的是文本节点
// 如果没有旧节点,则进行挂载
if (!n1) {
// 使用 createTextNode 创建文本节点
const el = (n2.el = createText(n2.children))
// 将文本节点插入到容器中
insert(el, container)
} else {
// 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
const el = (n2.el = n1.el)
if (n2.children !== n1.children) {
setText(el, n2.children)
}
}
} else if (type === Fragment) {
// 处理 Fragment 类型的 vnode
if (!n1) {
// 旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
n2.children.forEach((c) => patch(null, c, container))
} else {
// 旧 vnode 存在,则只需要更新 Fragment 的 children 即可
patchChildren(n1, n2, children)
}
}
}
function unmount(vnode) {
// 在卸载时,如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
if (vnode.type === Fragment) {
vnode.children.forEach((c) => unmount(c))
return
}
// 获取 el 的父元素
const parent = vnode.el.parentNode
// 调用removeChild 移除元素
if (parent) parent.removeChild(el)
}
function render(vnode, container) {
if (vnode) {
// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
// 无新 vnode, 有旧 vnode, 卸载操作
unmount(container._vnode)
}
}
// 存储 vnode
container._vnode = vnode
}
function hydrate(vnode, container) {}
return {
render,
hydrate,
}
}
const renderer = createRenderer({
createElement(tag) {
return document.createElement(tag)
},
setElementText(el, text) {
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
},
// 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
let invokers = el._vei || (el._vei = {})
let invoker = invokers[key]
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
// vei:vue event invoker
// 将事件处理函数缓存到 el._vei[key] 下,避免覆盖
invoker = el._vei[key] = (e) => {
// e.timestamp 是事件发生的时间
// 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
if (e.timeStamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
// 否则直接作为函数调用
invoker.value(e)
}
}
// 当真正的事件处理函数执行时,会执行真正的事件处理函数
invoker.value = nextValue
// 添加invoker.attached 属性,存储事件处理函数被绑定的时间
invoker.attached = performance.now()
// 绑定 invoker 作为事件处理函数
el.addEventListener(name, invoker)
} else {
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
invoker.value = nextValue
}
} else if (invoker) {
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
el.removeEventListener(name, invoker)
}
} else if (key === 'class') {
// 对 class 进行特殊处理
el.className = nextValue || ''
} else if (shouldSetAsProps(el, key, nextValue)) {
// 获取该 DOM Properties 的类型
const type = typeof el[key]
// 如果是布尔类型,并且 value 是空字符串,则将其矫正为true
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 如果要设置的属性没有对应的 DOM Properties, 则使用 setAttribute函数设置属性
el.setAttribute(key, nextValue)
}
},
createText(text) {
return document.createTextNode(text)
},
setText(el, text) {
el.nodeValue = text
},
})
简单 Diff 算法
// 文本节点的 type 标识
const Text = Symbol()
// 注释节点的 type 标识
const Comment = Symbol()
// 片段节点的 type 标识
const Fragment = Symbol()
// 返回 false 说明不应该作为 DOM Properties 设置
function shouldSetAsProps(el, key, value) {
// 特殊处理 form 为只读属性,只能通过setAttribute设置
if (key === 'form' && el.tagName === 'INPUT') {
return false
}
// 兜底
return key in el
}
// 通过配置创建具体平台的渲染器
function createRenderer(options) {
// 通过 options 得到操作DOM的API
const { createElement, insert, setElementText, patchProps } = options
function patchChildren(n1, n2, container) {
// 判断新子节点的类型是否为文本节点
if (typeof n2.children === 'string') {
// 旧子节点的类型有三种可能性:无子节点, 文本子节点,数组子节点
// 只有当旧子节点为数组子节点时,才需要逐个卸载,其他情况下什么都不做
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 说明新子节点是数组子节点
// 核心diff算法
// 简单做法:卸载旧的,挂载新的
const oldChildren = n1.children
const newChildren = n2.children
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let j = 0
// 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
// 初始值为 false,代表没找到
let find = false
for (j; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
// 一旦找到可复用的节点,则将变量 find 的值设为 true
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 如果当前找到的节点在旧 children 中的索引小于最大索引值 lastIndex
// 说明该节点对应的真实 DOM 需要移动
// 先获取newVNode 的前一个 vnode, 即 prevVNode
const prevVNode = newChildren[i - 1]
// 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,不需要移动它
if (prevVNode) {
// 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,
// 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
const anchor = prevVNode.el.nextSibling
// 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
// 也就是 prevVNode 对应真实 DOM 的后面
insert(newVNode.el, container, anchor)
}
} else {
// 如果当前找到的节点在旧 children 中的索引不小于最大索引值
// 则更新 lastIndex 的值
lastIndex = j
}
break
}
}
// 如果代码运行到这里,find 仍然为 false,
// 说明当前 newVNode 没有在旧的一组子节点中找到可复用的节点
// 也就是说,当前 newVNode 是新增节点,需要挂载
if (!find) {
// 为了将节点挂载到正确位置,我们需要先获取锚点元素
// 首先获取当前 newVNode 的前一个 vnode 节点
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
// 如果有前一个 vnode 节点,则使用它的下一个兄弟节点为锚点元素
anchor = prevVNode.el.nextSibling
} else {
// 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节点
// 这时我们使用容器元素的 firstChild 作为锚点
anchor = container.firstChild
}
// 挂载 newVNode
patch(null, newVNode, container, anchor)
}
}
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
if (!has) {
// 如果没有找到具有相同key值得节点,则说明需要删除该节点
unmount(oldVNode)
}
}
} else {
// 新子节点不存在
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
} else if (typeof n1.children === 'string') {
setElementText(container, '')
}
// 如果旧子节点也不存在,什么都不需要做
}
}
function patchElement(n1, n2) {
const el = (n2.el = n1.el)
const oldProps = n1.props
const newProps = n2.props
// 1) 更新props
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
// 2) 更新 children
patchChildren(n1, n2, el)
}
function mountElement(vnode, container, anchor) {
// 让 vnode.el 引用真实 DOM 元素
const el = (vnode.el = createElement(vnode.type))
// 如果 children 是字符串类型,直接设置为文本内容
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
// 如果 children 为数组类型,分发新增子节点
vnode.children.forEach((child) => {
patch(null, child, el)
})
}
// 如果 vnode.props 存在才处理它
if (vnode.props) {
for (const key in vnode.props) {
// 调用 patchProps 函数即可
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container, anchor)
}
function patch(n1, n2, container, anchor) {
// 如果 n1 存在,则对比 n1 和 n2 的类型
if (n1 && n1.type !== n2.type) {
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// 普通标签元素
if (!n1) {
// 挂载时将锚点元素作为第三个参数传递给 mountElement 函数
mountElement(n2, container, anchor)
} else {
patchElement(n1, n2)
}
} else if (typeof type === 'object') {
// 如果 n2.type 的值的类型是对象,则它描述的是组件
} else if (type === Text) {
// 如果新 vnode 的类型是 Text,说明该 vnode 描述的是文本节点
// 如果没有旧节点,则进行挂载
if (!n1) {
// 使用 createTextNode 创建文本节点
const el = (n2.el = createText(n2.children))
// 将文本节点插入到容器中
insert(el, container)
} else {
// 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
const el = (n2.el = n1.el)
if (n2.children !== n1.children) {
setText(el, n2.children)
}
}
} else if (type === Fragment) {
// 处理 Fragment 类型的 vnode
if (!n1) {
// 旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
n2.children.forEach((c) => patch(null, c, container))
} else {
// 旧 vnode 存在,则只需要更新 Fragment 的 children 即可
patchChildren(n1, n2, children)
}
}
}
function unmount(vnode) {
// 在卸载时,如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
if (vnode.type === Fragment) {
vnode.children.forEach((c) => unmount(c))
return
}
// 获取 el 的父元素
const parent = vnode.el.parentNode
// 调用removeChild 移除元素
if (parent) parent.removeChild(el)
}
function render(vnode, container) {
if (vnode) {
// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
// 无新 vnode, 有旧 vnode, 卸载操作
unmount(container._vnode)
}
}
// 存储 vnode
container._vnode = vnode
}
function hydrate(vnode, container) {}
return {
render,
hydrate,
}
}
const renderer = createRenderer({
// 创建标签节点
createElement(tag) {
return document.createElement(tag)
},
// 设置节点内容为文本
setElementText(el, text) {
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
},
// 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
let invokers = el._vei || (el._vei = {})
let invoker = invokers[key]
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
// vei:vue event invoker
// 将事件处理函数缓存到 el._vei[key] 下,避免覆盖
invoker = el._vei[key] = (e) => {
// e.timestamp 是事件发生的时间
// 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
if (e.timeStamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
// 否则直接作为函数调用
invoker.value(e)
}
}
// 当真正的事件处理函数执行时,会执行真正的事件处理函数
invoker.value = nextValue
// 添加invoker.attached 属性,存储事件处理函数被绑定的时间
invoker.attached = performance.now()
// 绑定 invoker 作为事件处理函数
el.addEventListener(name, invoker)
} else {
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
invoker.value = nextValue
}
} else if (invoker) {
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
el.removeEventListener(name, invoker)
}
} else if (key === 'class') {
// 对 class 进行特殊处理
el.className = nextValue || ''
} else if (shouldSetAsProps(el, key, nextValue)) {
// 获取该 DOM Properties 的类型
const type = typeof el[key]
// 如果是布尔类型,并且 value 是空字符串,则将其矫正为true
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 如果要设置的属性没有对应的 DOM Properties, 则使用 setAttribute函数设置属性
el.setAttribute(key, nextValue)
}
},
// 创建文本节点
createText(text) {
return document.createTextNode(text)
},
// 设置节点内容为文本
setText(el, text) {
el.nodeValue = text
},
})
双端 Diff 算法
// 跟上边简单 Diff 算法 差别体现在以下两个函数
// 核心实现是 patchKeyedChildren函数
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children
const newChildren = n2.children
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 增加两个判断分支,如果旧头/尾部节点为 undefined,则说明该节点已经被处理过了,需要跳过到下一个位置
if (!oldStartVNode) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (!oldEndVNode) {
oldEndVNode = oldChildren[--oldEndIdx]
} else if (oldStartVNode.key === newStartVNode.key) {
patch(oldStartVNode, newStartVNode, container)
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (oldEndVNode.key === newEndVNode.key) {
// 节点在新的顺序中仍然处于尾部,不需要移动,但仍需要打补丁
patch(oldEndVNode, newEndVNode, container)
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldStartVNode.key === newEndVNode.key) {
patch(oldStartVNode, newEndVNode, container)
// 将旧的一组子节点的头部节点对应的真实 DOM 节点 oldStartVNode.el 移动到
// 旧的一组子节点的尾部节点对应的真实 DOM 节点后面
insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
// 更新相关索引到下一个位置
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldEndVNode.key === newStartVNode.key) {
// 仍然需要调用 patch 函数进行打补丁
patch(oldEndVNode, newStartVNode, container)
// 移动 DOM 操作
// oldEndVNode.el 移动到 oldStartVNode.el 前面
insert(oldEndVNode.el, container, oldStartVNode.el)
// 移动 DOM 完成后,更新索引值,并指向下一个位置
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
} else {
// 遍历旧 children, 试图寻找与 newStartVNode 拥有相同 key 值的元素
const idxInOld = oldChildren.findIndex(
(node) => node.key === newStartVNode.key
)
// idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实 DOM 移动到头部
if (idxInOld > 0) {
// idxInOld 位置对应的 vnode 就是需要移动的节点
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove.el, newStartVNode, container)
// 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
insert(vnodeToMove.el, container, oldStartVNode.el)
// 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到别处,因此将其设置为 undefined
oldChildren[idxInOld] = undefined
} else {
// 新增节点
// 将 newStartVNode 作为新节点挂载到头部,使用当前头部节点 oldStartVNode.el 作为锚点
patch(null, newStartVNode, container, oldStartVNode.el)
}
// 最后更新 newStartIdx 到下一个位置
newStartVNode = newChildren[++newStartIdx]
}
}
// 循环结束后检查索引值的情况
if (oldEndIdx <= oldStartIdx && newStartIdx <= newEndIdx) {
// 如果满足条件,则说明有新的节点遗留,需要挂载他们
for (let i = newStartIdx; i <= newEndIdx; i++) {
patch(null, newChildren[i], container, oldStartVNode.el)
}
} else if (newEndIdx <= newStartIdx && oldStartIdx <= oldEndIdx) {
// 移除操作
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
unmount(oldChildren[i])
}
}
}
function patchChildren(n1, n2, container) {
// 判断新子节点的类型是否为文本节点
if (typeof n2.children === 'string') {
// 旧子节点的类型有三种可能性:无子节点, 文本子节点,数组子节点
// 只有当旧子节点为数组子节点时,才需要逐个卸载,其他情况下什么都不做
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 说明新子节点是数组子节点
// 核心diff算法
// 封装 patchKeyedChildren 函数处理两组子节点
patchKeyedChildren(n1, n2, container)
} else {
// 新子节点不存在
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
} else if (typeof n1.children === 'string') {
setElementText(container, '')
}
// 如果旧子节点也不存在,什么都不需要做
}
}
快速 Diff 算法
// 核心实现是 patchKeyedChildren函数
// 计算最长递增子序列函数 lis 自行实现
function patchKeyedChildren(n1, n2, container) {
const newChildren = n2.children
const oldChildren = n1.children
// 更新相同的前置节点
// 索引 j 指向新旧两组子节点的开头
let j = 0
let oldVNode = oldChildren[j]
let newVNode = newChildren[j]
// while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
while (oldVNode.key === newVNode.key) {
// 调用 patch 函数进行更新
patch(oldVNode, newVNode, container)
// j自增
j++
oldVNode = oldChildren[j]
newVNode = newChildren[j]
}
// 更新相同的后置节点
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
// while 循环从后向前遍历,直到遇到拥有不同 key 值得节点为止
while (oldVNode.key === newVNode.key) {
patch(oldVNode, newVNode, container)
oldEnd--
newEnd--
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
}
// 预处理完毕后,处理新增节点与删除节点
if (j > oldEnd && j <= newEnd) {
// 如果满足下列条件则说明从 j --> newEnd 之间的节点应作为新节点插入
// 锚点的索引
const anchorIndex = newEnd + 1
// 锚点元素
const anchor =
anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
// 采用 while 循环,调用 patch 函数逐个挂载新增节点
while (j <= newEnd) {
patch(null, newChildren[j++], container, anchor)
}
} else if (j > newEnd && j <= oldEnd) {
// 删除多余的旧节点
while (j <= oldEnd) {
unmount(oldChildren[j++])
}
} else {
// 处理非理性情况
// 构造 source 数组
// 新的一组子节点中剩余未处理节点的数量
const count = newEnd - j + 1
const source = new Array(count)
source.fill(-1)
// oldStart 和 newStart 分别为起始索引,即 j
const oldStart = (newStart = j)
let moved = false // 是否需要移动
let pos = 0 // 代表遍历旧的一组子节点的过程中遇到的最大索引值 k
// 1)两层for循环,遍历旧的一组子节点 O(n^2)
// for (let i = oldStart; i <= oldEnd; i++) {
// const oldVNode = oldChildren[i]
// // 遍历新的一组子节点
// for (let k = newStart; k <= newEnd; k++) {
// const newVNode = newChildren[k]
// // 找到拥有相同 key 值的可复用节点
// if (oldVNode.key === newVNode.key) {
// patch(oldVNode, newVNode, container)
// // 由于数组 source 的索引是从 0 开始的,而未处理节点的索引未必从 0 开始,
// // 所以在填充数组时需要使用表达式 k - newStart 的值作为数组的索引值。
// // 外层循环的变量 i 就是当前节点在旧的一组子节点中的位置索引,因此直接将变量 i 的值赋给 source[k - newStart] 即可。
// source[k - newStart] = i
// }
// }
// }
// 2)构建索引表 O(n)
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
keyIndex[newChildren[i].key] = i
}
let patched = 0 // 代表更新过的节点数量
for (let i = oldStart; i <= oldEnd; i++) {
oldVNode = oldChildren[i]
if (patched <= count) {
const k = keyIndex[oldVNode.key]
if (typeof k !== 'undefined') {
newVNode = newChildren[k]
patch(oldVNode, newVNode, container)
source[k - newStart] = i
// 判断节点是否需要移动
if (k < pos) {
moved = true
} else {
pos = k
}
} else {
unmount(oldVNode)
}
} else {
// 如果更新过的节点数量大于需要更新的节点数量,卸载多余的节点
unmount(oldVNode)
}
}
if (moved) {
// 计算最长递增子序列 返回的结果为最长递增子序列中的元素在 source 数组中的位置索引
const seq = lis(source)
// s 指向最长递增子序列的最后一个元素
let s = seq.length - 1
// i 指向新的一组子节点的最后一个元素
let i = count - 1
// for 循环使得 i 递减
for (i; i >= 0; i--) {
if (source[i] === -1) {
// 说明索引为 i 的节点是全新的节点,应该将其挂载
const pos = i + newStart // 在新 children 的真实位置索引
const newVNode = newChildren[pos]
const nextPos = pos + 1 // 该节点的下一个节点的位置索引
// 锚点
const anchor =
nextPos < newChildren.length ? newChildren[nextPos].el : null
// 挂载
patch(null, newVNode, container, anchor)
} else if (i !== seq[s]) {
// 如果节点的索引 i 不等于 seq[s] 的值,说明该节点需要移动
const pos = i + newStart
const newVNode = newChildren[pos]
const nextPos = pos + 1
const anchor =
nextPos < newChildren.length ? newChildren[nextPos].el : null
insert(newVNode.el, container, anchor)
} else {
// 当 i === seq[s] 时,说明该位置的节点不需要移动
s--
}
}
}
}
}
组件的实现原理
// resolveProps 函数用于解析组件 props 与 attrs 数据
// 实现中没有包含默认值、类型校验等内容的处理。
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
// 遍历为组件传递的 props 数据
for (const key in propsData) {
// 以字符串 on 开头的 props,无论是否显示地声明,都将其添加到 props 数据中,而不是添加到 attrs 中
if (key in options || key.startsWith('on')) {
// 如果为组件传递的 props 数据在组件自身的 props 选项中有定义,则将其视为合法的 props
props[key] = propsData[key]
} else {
// 否则将其作为 attrs
attrs[key] = propsData[key]
}
}
// 最后返回 props 与 attrs 数据
return [props, attrs]
}
// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null
// 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance) {
currentInstance = instance
}
function createRenderer(options) {
// 其他代码
// 第二种处理方式:composition类型 setup
function mountComponent(vnode, container, anchor) {
function onMounted(fn) {
if (currentInstance) {
currentInstance.mounted.push(fn)
} else {
console.error('onMounted 函数只能在 setup 中调用')
}
}
// 定义 emit 函数,它接收两个参数
// event:事件名称
// payload:传递给事件处理函数的参数
function emit(event, ...payload) {
// 根据约定对事件名称进行处理,例如 change --> onChange
const eventName = `on${event[0].toUppercase() + event.slice(1)}`
// 根据处理后的事件名称去 props 中寻找对应的事件处理函数
const handler = instance.props[eventName]
if (handler) {
// 调用事件处理函数并传递参数
handler(...payload)
} else {
console.error('不存在')
}
}
// 插槽
// 直接使用编译好地 vnode.children 对象作为 slots 对象即可
const slots = vnode.children || []
const componentOptions = vnode.type
let { render, data, setup, beforeCreate, propsOptions } = componentOptions
beforeCreate && beforeCreate()
const state = data ? reactive(data()) : null
const [props, attrs] = resolveProps(propsOptions, vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
// 将插槽添加到组件实例上
slots,
// 用来存储通过 onMounted 函数注册的生命周期钩子函数
mounted: [],
}
// setupContext
const setupContext = { attrs, emit, slots }
// 设置当前组件实例
setCurrentInstance(instance)
// 调用 setup 函数,将只读版本的 props 作为第一个参数传递,避免用户意外的修改 props 的值
// 将 setupContext 作为第二个参数传递
const setupResult = setup(shallowReadonly(instance.props), setupContext)
// setup函数执行完毕后,重置当前组件实例
setCurrentInstance(null)
// setupState 用来存储由 setup 返回的数据
let setupState = null
// 如果 setup 函数的返回值是函数,则将其作为渲染函数
if (typeof setupResult === 'function') {
if (render) {
console.error('setup 函数返回渲染函数,render选项将被忽略')
render = setupResult
} else {
// 如果 setup 返回值不是函数,则作为数据状态赋值给 setupState
setupState = setupResult
}
}
vnode.component = instance
const renderContext = new Proxy(instance, {
get(target, key, receiver) {
const { state, props, slots } = target
if (k === '$slots') {
return slots
}
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else if (setupState && k in setupState) {
return setupState[k]
} else {
console.error('不存在')
}
},
set(target, key, value, receiver) {
const { state, props } = target
if (state && k in state) {
state[k] = value
} else if (k in props) {
console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)
} else if (setupState && k in setupState) {
setupState[k] = value
} else {
console.error('不存在')
}
},
})
created && created.call(renderContext)
// 将组件的 render 函数调用包装到 effect 内
effect(
() => {
// 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
// render的 this 指向响应式数据 state,同时将 state 作为 render 函数的第一个参数传递。
const subTree = render.call(state, state)
if (!instance.isMounted) {
beforeMount && beforeMount.call(state)
// 初次挂载,patch第一个参数为null
patch(null, subTree, container, anchor)
// 将组件实例的 isMounted 设置为true,这样当更新发生时就不会再次进行挂载操作,而是会执行更新
instance.isMounted = true
instance.mounted &&
instance.mounted.forEach((hook) => hook.call(renderContext))
} else {
beforeUpdate && beforeUpdate.call(state)
// 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
// 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
// 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
patch(instance.subTree, subTree, container, anchor)
updated && updated.call(state)
}
// 更新组件实例的子树
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
}
// 第一张处理方式:option 类型
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render
const {
render,
data,
beforeCreate,
created,
beforeMount,
mounted,
beforeUpdate,
updated,
// 从组件选项对象中取出 props 定义,即 propsOption
props: propsOptions,
} = componentOptions
beforeCreate && beforeCreate()
// 调用 data 函数获得原始数据,并调用 reactive 将其包装成响应式数据
const state = reactive(data())
// 调用 resolveProps 函数解析出最终的 props 数据与 attrs 数据
const [props, attrs] = resolveProps(propsOptions, vnode.props)
// 定义组件实例,包含组件有关的状态信息
const instance = {
state,
// 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
props: shallowReactive(props),
isMounted: false,
subTree: null, // 组件渲染的内容,即子树
}
// 将组件实例设置到 vnode 上,用于后续更新
vnode.component = instance
// 创建渲染上下文对象,本质上是组件实例的代理
// 实际上,除了组件自身的数据以及 props 数据之外,
// 完整的组件还包含 methods、computed 等选项中定义的数据和方法,这些内容都应该在渲染上下文对象中处理。
const renderContext = new Proxy(instance, {
get(target, key, receiver) {
// 取得组件自身状态与 props 数据
const { state, props } = target
if (state && key in state) {
return state[k]
} else if (k in props) {
return props[k]
} else {
console.error('不存在')
}
},
set(target, key, value, receiver) {
const { state, key } = target
if (state && key in state) {
state[k] = value
} else if (k in props) {
console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)
} else {
console.error('不存在')
}
},
})
created && created.call(renderContext)
// 将组件的 render 函数调用包装到 effect 内
effect(
() => {
// 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
// render的 this 指向响应式数据 state,同时将 state 作为 render 函数的第一个参数传递。
const subTree = render.call(state, state)
if (!instance.isMounted) {
beforeMount && beforeMount.call(state)
// 初次挂载,patch第一个参数为null
patch(null, subTree, container, anchor)
// 将组件实例的 isMounted 设置为true,这样当更新发生时就不会再次进行挂载操作,而是会执行更新
instance.isMounted = true
mounted && mounted.call(state)
} else {
beforeUpdate && beforeUpdate.call(state)
// 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
// 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
// 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
patch(instance.subTree, subTree, container, anchor)
updated && updated.call(state)
}
// 更新组件实例的子树
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
}
// 其他代码
}
异步组件和函数式组件
// 异步组件
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = {
loader: options,
}
}
const { loader } = options
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 记录重试次数
let retries = 0
// 封装 load 函数用来加载异步组件
function load() {
return loader().catch((err) => {
if (options.onError) {
return new Promise((resolve, reject) => {
const retry = () => {
resolve(load())
retries++
}
const fail = () => reject(err)
options.onError(retry, fail, retires)
})
} else {
throw error
}
})
}
// 返回一个包装组件
return {
name: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 定义 error,当错误发生时,用来存储错误对象
const error = shallowRef(null)
// 一个标志,代表是否正在加载,默认为false
const loading = ref(false)
let loadingTimer = null
// 存在 delay,当延迟到时后将 loading.value 设置为 true
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
// 配置项不存在delay,则直接标记为加载中
loading.value = true
}
// 调用 load 函数加载组件
load()
.then((c) => {
InnerComp = c
loaded.value = true
})
.catch((err) => (error.value = err))
.finally(() => {
loading.value = false
// 加载完毕后,需要清除加载延时定时器
clearTimeout(loadingTimer)
})
let timer = null
if (options.timeout) {
// 如果指定了超时时长,则开启一个定时器计时
timer = setTimeout(() => {
const err = new Error(
`Async component timed out after ${options.timeout}ms.`
)
err.value = err
}, options.timeout)
}
// 包装组件被卸载时清除定时器
onUnmounted(() => clearTimeout(timer))
// 占位内容
const placeholder = { type: Text, children: '' }
return () => {
// 如果异步组件加载成功,则渲染该组件,否则渲染一个占位内容
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && options.errorComponent) {
// 只有当错误存在且用户配置了 errorComponent 时才展示 Error组件
// 同时将 error 作为 props 传递
return {
type: options.errorComponent,
props: { error: error.value },
}
} else if (loading.value && options.loadingComponent) {
// 如果异步组件正在加载,且用户指定了 Loading 组件,则渲染 Loading 组件
return { type: options.loadingComponent }
} else {
return placeholder
}
}
},
}
}
// 重试机制模拟实现
// 模拟网络请求失败
function fetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('err')
}, 1000)
})
}
// 重试机制 onError(retry: fn, fail: fn)
function load(onError) {
const p = fetch()
return p.catch((err) => {
// 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调,
// 同时将 retry 函数作为 onError 回调的参数
return new Promise((resolve, reject) => {
// retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
const retry = () => resolve(load(onError))
const fail = () => reject(err)
onError(retry, fail)
})
})
}
// load实用的例子
load((retry) => retry()).then((res) => console.log(res))
// 针对函数式组件的调整
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
// 省略其他代码
}
const { type } = n2
if (typeof type === 'string') {
// 省略其他代码
} else if (typeof type === 'object') {
// 省略其他代码
} else if (type === Text) {
// 省略其他代码
} else if (type === Fragment) {
// 省略其他代码
} else if (typeof type === 'object' || typeof type === 'function') {
// 状态组件 以及 函数组件
if (!n1) {
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
function mountComponent(vnode, container, anchor) {
// 省略其他代码
// 检查是否为函数式组件
const isFunctional = typeof vnode.type === 'function'
// 通过 vnode 获取组件的选项对象,即 vnode.type
let componentOptions = vnode.type
// 如果是函数式组件,则将 vnode.type 作为渲染函数,将 vnode.type.props 作为 props 选项定义即可
if (isFunctional) {
componentOptions = {
render: vnode.type,
props: vnode.type.props,
}
}
// 省略其他代码
}
内建组件和模块
// KeepAlive组件
// 最基本的 KeepAlive 组件实现
const KeepAlive = {
// KeepAlive 组件独有的属性,用作标识
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp,
},
setup(props, { emits }) {
// 创建一个缓存对象
// key:vnode.type
// value:vnode
const cache = new Map()
// 当前 KeepAlive 组件的实例
const instance = currentInstance
// 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
// 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
const { move, createElement } = instance.keepAliveCtx
// 创建隐藏容器
const storageContainer = createElement('div')
// KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate 和 _activate
// 这两个函数会在渲染器中被调用
instance._deActivate = (vnode) => {
move(vnode, storageContainer) // 移动到隐藏容器
}
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor) // 移动到页面原先的容器中
}
return () => {
// KeepAlive 的默认插槽就是要被 KeepAlive 的组件
let rawVNode = slots.default()
if (typeof rawVNode.type !== 'object') {
// 如果不是组件,直接渲染即可,因为非组件的虚拟节点无法被 KeepAlive
return rawVNode
}
// 获取“内部组件”的 name
const name = rawVNode.type.name
// 对 name 进行匹配
if (
name &&
// 如果 name 无法被 include 匹配
((props.include && !props.include.test(name)) ||
// 或者被 exclude
(props.exclude && props.exclude.test(name)))
) {
// 则直接渲染“内部组件”,不对其进行后续的缓存操作
return rawVNode
}
// 在挂载时先获取缓存的组件 vnode
const cachedVNode = cache.get(rawVNode.type)
if (cachedVNode) {
// 如果有缓存的内容,则说明不应该执行挂载,而应该执行激活
// 继承组件实例
rawVNode.component = cachedVNode.component
rawVNode.keptAlive = true
} else {
// 如果没有缓存,则将其添加到缓存中,这样下次激活组件时就不会执行新的挂载动作了
cache.set(rawVNode.type, rawVNode)
}
// 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
rawVNode.shouldKeepAlive = true
// 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问
rawVNode.keepAliveInstance = instance
// 渲染组件 vnode
return rawVNode
}
},
}
function unmount(vnode) {
if (vnode.type === 'Fragment') {
vnode.children.forEach((c) => unmount(c))
return
} else if (typeof vnode.type === 'object') {
// vnode.shouldKeepAlive 是一个布尔值,用来标识该组件是否应该被 KeepAlive
if (vnode.shouldKeepAlive) {
// 对于需要被 KeepAlive 的组件,我们不应该真的卸载它,而应调用该组件的父组件,
// 即 KeepAlive 组件的 _deActivate 函数使其失活
vnode.keepAliveInstance._deActivate(vnode)
} else {
unmount(vnode.component.subTree)
}
return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// DOM节点,省略部分代码
} else if (typeof type === Text) {
// 文本节点,省略部分代码
} else if (typeof type === Fragment) {
// 片段节点,省略部分代码
} else if (typeof type === 'object' || typeof type === 'function') {
// 组件
if (!n1) {
// 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用 _activate来激活它
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor)
} else {
mountComponent(n2, container, anchor)
}
} else {
patchComponent(n1, n2, anchor)
}
}
}
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: [],
// 只有 KeepAlive组件的实例下才会有 keepAliveCtx 属性
keepAliveCtx: null,
}
// 检查当前要挂载的组件是否是 KeepAlive 组件
const isKeepAlive = vnode.type.__isKeepAlive
if (isKeepAlive) {
// 在 KeepAlive 组件实例上添加 keepAliveCtx 对象
instance.keepAliveCtx = {
// move 函数用来移动一段 vnode
move(vnode, container, anchor) {
// 本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
insert(vnode.component.subTree.el, container, anchor)
},
createElement,
}
}
// 省略部分代码
}
// 一个基本的缓存实例实现
const _cache = new Map()
// TypeScript
const cache: KeepAliveCache = {
get(key) {
_cache.get(key)
},
set(key, value) {
_cache.set(key, value)
},
delete(key) {
_cache.delete(key)
},
forEach(fn) {
_cache.forEach(fn)
},
}
// Teleport组件
// Teleport组件定义
const Teleport = {
__isTeleport: true,
process(n1, n2, container, anchor, internals) {
// 在这里处理渲染逻辑
// 通过 internals 参数取得渲染器的内部方法
const { patch, patchChildren, move } = internals
// 如果旧 VNode n1 不存在,则是全新的挂载,否则执行更新
if (!n1) {
// 挂载
// 获取容器,即挂载点
const target =
typeof n2.props.to === 'string'
? document.querySelector(n2.props.to)
: n2.props.to
// 将 n2.children 渲染到指定挂载点即可
n2.children.forEach((c) => patch(null, c, target, anchor))
} else {
// 更新
patchChildren(n1, n2, container)
// 如果新旧 to 参数的值不同,则需要对内容进行移动
if (n2.props.to !== n1.props.to) {
// 获取新的容器
const newTarget =
typeof n2.props.to === 'string'
? document.querySelector(n2.props.to)
: n2.props.to
n2.children.forEach((c) => move(c, newTarget))
}
}
},
}
// 对于 Teleport 组件来说,直接将其子节点编译成一个数组即可
function render() {
return {
type: Teleport,
// 以普通 children 形式代表被 Teleport 的内容
children: [
// xxx
],
}
}
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// DOM节点,省略部分代码
} else if (typeof type === Text) {
// 文本节点,省略部分代码
} else if (typeof type === Fragment) {
// 片段节点,省略部分代码
} else if (typeof type === 'object' && type.__isTeleport) {
// 组件选项中如果存在 __isTeleport 标识,则它是 Teleport 组件,
// 调用 Teleport 组件选项中的 process 函数将控制权交接出去
// 传递给 process 函数的第五个参数是渲染器的一些内部方法
type.process(n1, n2, container, anchor, {
patch,
patchChildren,
unmount,
move(vnode, container, anchor) {
insert(
vnode.component ? vnode.component.subTree.el : vnode.el, // 移动一个组件或普通元素
container,
anchor
)
},
})
} else if (typeof type === 'object' || typeof type === 'function') {
// 组件,省略部分代码
}
}
// 原生DOM的过渡
const el = document.createElement('div')
el.classList.add('box')
// 创建DOM元素完成之后,到把 DOM 添加到 body 前,可以视作 beforeEnter 阶段
el.classList.add('enter-from')
el.classList.add('enter-active')
document.body.appendChild(el)
// 之所以需要调用两次requestAnimationFrame是由于浏览器Bug
// 使用 requestAnimationFrame 函数注册回调会在当前帧执行,除非其他代码已经调用了一次requestAnimationFrame 函数。
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// DOM 添加到 body 之后,则可以视为 enter 阶段
el.classList.remove('enter-from')
el.classList.add('enter-to')
// 进场动效结束,通过监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to')
el.classList.remove('enter-active')
})
})
})
// 卸载元素
el.addEventListener('click', () => {
// 将卸载动作封装到 performRemove 函数中
const performRemove = () => el.parentNode.removeChild(el)
// 设置初始状态:添加 leave-from 和 leave-active 类
el.classList.add('leave-from')
el.classList.add('leave-active')
// 强制 reflow:使初始状态生效
document.body.offsetHeight
// 在下一帧切换状态
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 切换到结束状态
el.classList.remove('leave-from')
el.classList.add('leave-to')
// 监听 transitionend 事件做收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('leave-to')
el.classList.remove('leave-active')
performRemove()
})
})
})
})
// Transition组件
const Transition = {
name: 'Transition',
setup(props, { slots }) {
return () => {
// 通过默认插槽获取需要过渡的元素
const innerVNode = slots.default()
// 在过渡元素的 VNode 对象上添加 transition 相应的钩子函数
innerVNode.transition = {
beforeEnter(el) {
// 设置初始状态
el.classList.add('enter-from')
el.classList.add('enter-active')
},
enter(el) {
// 在下一帧切换到结束状态
nextFrame(() => {
// 移除 enter-from 类,添加 enter-to 类
el.classList.remove('enter-from')
el.classList.add('enter-to')
})
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to')
el.classList.remove('enter-active')
})
},
leave(el, performRemove) {
// 设置离场过渡的初始状态:添加 leave-from 和 leave-active 类
el.classList.add('leave-from')
el.classList.add('leave-active')
// 强制reflow,使初始状态生效
document.body.offsetHeight
// 在下一帧修改状态
nextFrame(() => {
el.classList.remove('leave-from')
el.classList.add('leave-to')
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('leave-to')
el.classList.remove('leave-active')
// 完成 DOM 元素的卸载
performRemove()
})
})
},
}
return innerVNode
}
},
}
function mountComponent(vnode, container, anchor) {
const el = (vnode.el = createElement(vnode.type))
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.sArray(vnode.children)) {
vnode.children.forEach((child) => {
patch(null, child, el)
})
}
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
// 判断一个 vnode 是否需要过渡
const needTransition = vnode.transition
if (needTransition) {
// 调用 transition.beforeEnter 钩子,并将 DOM 元素 作为参数传递
vnode.transition.beforeEnter(el)
}
insert(el, container, anchor)
if (needTransition) {
// 调用 transition.enter 钩子,并将 DOM 元素 作为参数传递
vnode.transition.enter(el)
}
}
function unmount(vnode) {
const needTransition = vnode.transition
if (vnode.type === Fragment) {
vnode.children.forEach((c) => unmount(c))
return
} else if (typeof vnode.type === 'object') {
if (vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActive(vnode)
} else {
unmount(vnode.component.subTree)
}
return
}
const parent = vnode.el.parentNode
if (parent) {
// 将卸载动作封装到 performRemove 函数中
const performRemove = () => parent.removeChild(vnode.el)
}
if (needTransition) {
// 如果需要过渡处理,则调用 transition.leave 钩子
// 同时将 DOM 元素和 performRemove 函数作为参数传递
vnode.transition.leave(vnode.el, performRemove)
} else {
// 如果不需要过渡处理,则直接执行卸载操作
performRemove()
}
}
编译器
// 编译器实现
// 定义状态机的状态
const State = {
initial: 1,
tagOpen: 2,
tagName: 3,
text: 4,
tagEnd: 5,
tagEndName: 6,
}
// 一个辅助函数,用于判断是否是字母
function isAlpha(char) {
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
}
// 接收模板字符串作为参数,并将模板切割为 Token 返回
function tokenize(str) {
// 状态机的当前状态:初始状态
let currentState = State.initial
// 用于缓存字符
const chars = []
// 生成的 Token 会存储到 tokens 数组中,并作为函数的返回值返回
const tokens = []
// 使用 while 循环开启自动机,只要模板字符串没有被消费尽,自动机就会一直运行
while (str) {
// 查看第一个字符
const char = str[0]
// switch 语句匹配当前状态
switch (currentState) {
// 状态机当前处于初始状态
case State.initial:
if (char === '<') {
currentState = State.tagOpen
str = str.slice(1)
} else if (isAlpha(char)) {
currentState = State.text
chars.push(char)
str = str.slice(1)
}
break
// 状态机当前处于标签开始状态
case State.tagOpen:
if (isAlpha(char)) {
currentState = State.tagName
chars.push(char)
str = str.slice(1)
} else if (char === '/') {
currentState = State.tagEnd
str = str.slice(1)
}
break
// 状态机当前处于标签名称状态
case State.tagName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tag',
name: chars.join(''),
})
chars.length = 0
str = str.slice(1)
}
break
// 状态机当前处于文本状态
case State.text:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '<') {
currentState = State.tagOpen
tokens.push({
type: 'text',
content: chars.join(''),
})
chars.length = 0
str = str.slice(1)
}
break
// 状态机当前处于标签结束状态
case State.tagEnd:
if (isAlpha(char)) {
currentState = State.tagEndName
chars.push(char)
str = str.slice(1)
}
break
// 状态机当前处于结束标签名称状态
case State.tagEndName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tagEnd',
name: chars.join(''),
})
chars.length = 0
str = str.slice(1)
}
break
}
}
return tokens
}
function parse(str) {
const tokens = tokenize(str)
const root = {
type: 'Root',
children: [],
}
// 创建 elementStack 栈,起初只有 Root 根节点
const elementStack = [root]
// 开启一个 while 循环扫描 tokens,直到所有 Token 都被扫描完毕为止
while (tokens.length) {
const parent = elementStack[elementStack.length - 1]
// 当前扫描的 Token
const t = tokens[0]
switch (t.type) {
case 'tag':
const elementNode = {
type: 'Element',
tag: t.name,
children: [],
}
// 将其添加到父级节点的 children 中
parent.children.push(elementNode)
// 将当前节点压入栈
elementStack.push(elementNode)
break
case 'text':
// 如果当前 Token 是文本,则创建 Text 类型的 AST 节点
const textNode = {
type: 'Text',
content: t.content,
}
parent.children.push(textNode)
break
case 'tagEnd':
// 遇到结束标签,将栈顶节点弹出
elementStack.pop()
break
}
// 消费已经扫描过的 token
tokens.shift()
}
// 最后返回 AST
return root
}
// 打印当前 AST 中节点的信息
function dump(node, indent = 0) {
// 节点的类型
const type = node.type
// 节点的描述,如果是根节点,则没有描述
// 如果是 Element 类型的节点,则使用 node.tag 作为节点的描述
// 如果是 Text 类型的节点,则使用 node.content 作为节点的描述
const desc =
node.type === 'Root'
? ''
: node.type === 'Element'
? node.tag
: node.content
// 打印节点的类型和描述信息
console.log(`${'-'.repeat(indent)}${type}:${desc}`)
// 递归地打印子节点
if (node.children) {
node.children.forEach((n) => dump(n, indent + 2))
}
}
// 深度优先遍历实现对 AST 中的节点的访问
function traverseNode(ast, context) {
// 当前节点,ast 本身就是 Root 节点
context.currentNode = ast
// 1. 增加退出阶段的回调函数数组
const exitFns = []
// context.nodeTransforms 是一个数组,其中每一个元素都是一个函数
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
// 2. 转换函数可以返回另外一个函数,该函数即作为退出阶段的回调函数
const onExit = transforms[i](context.currentNode, context)
if (onExit) {
exitFns.push(onExit)
}
// 由于任何转换函数都可能移除当前节点,因此每个转换函数执行完毕后,
// 都应该检查当前节点是否已经被移除,如果被移除了,直接返回即可
if (!context.currentNode) return
}
const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
// 递归地调用 traverseNode 转换子节点之前,将当前节点设置为父节点
context.parent = context.currentNode
// 设置位置索引
context.childIndex = i
// 如果有子节点,则递归地调用 traverseNode 函数进行遍历
traverseNode(children[i], context)
}
}
// 在节点处理的最后阶段执行缓存到 exitFns 中的回调函数
// 注意,这里我们要反序执行
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
// 封装 transform 函数,用来对 AST 进行转换
function transform(ast) {
function transformElement(node, context) {
// 进入节点
// 返回一个会在退出节点时执行的回调函数
return () => {
// 将转换代码编写在退出阶段的回调函数中,
// 这样可以保证该标签节点的子节点全部被处理完毕
if (node.type !== 'Element') {
// 如果被转换的节点不是元素节点,则什么都不做
return
}
// 1. 创建 h 函数调用语句,
// h 函数调用的第一个参数是标签名称,因此我们以 node.tag 来创建一个字符串字面量节点
// 作为第一个参数
const callExp = createCallExpression('h', [createStringLiteral(node.tag)])
// 2. 处理 h 函数调用的参数
node.children.length === 1
? // 如果当前标签节点只有一个子节点,则直接使用子节点的 jsNode 作为参数
callExp.arguments.push(node.children[0].jsNode)
: // 如果当前标签节点有多个子节点,则创建一个 ArrayExpression 节点作为参数
callExp.arguments.push(
// 数组的每个元素都是子节点的 jsNode
createArrayExpression(node.children.map((c) => c.jsNode))
)
// 3. 将当前标签节点对应的 JavaScript AST 添加到 jsNode 属性下
node.jsNode = callExp
}
}
function transformText(node, context) {
if (node.type !== 'Text') {
return
}
// 文本节点对应的 JavaScript AST 节点其实就是一个字符串字面量,
// 因此只需要使用 node.content 创建一个 StringLiteral 类型的节点即可
// 最后将文本节点对应的 JavaScript AST 节点添加到 node.jsNode 属性下
node.jsNode = createStringLiteral(node.content)
}
// 转换 Root 根节点
function transformRoot(node) {
return () => {
if (node.type !== 'Root') {
return
}
const vnodeJSAST = node.children[0].jsNode
node.jsNode = {
type: 'FunctionDecl',
id: {
type: 'Identifier',
name: 'render',
},
params: [],
body: [
{
type: 'ReturnStatement',
return: vnodeJSAST,
},
],
}
}
}
// 在 transform 函数内创建 context 对象
const context = {
// 增加 currentNode 用来存储当前正在转换的节点
currentNode: null,
// 增加 childIndex 用来存储当前节点在父节点的 children 中的位置索引
childIndex: 0,
// 增加 parent 用来存储当前转换节点的父节点
parent: null,
// 用于替换节点的函数,接收新节点作为参数
replaceNode(node) {
// 为了替换节点,我们需要修改 AST
// 找到当前节点在父节点的 children 中的位置:context.childIndex
// 然后使用新节点替换即可
context.parent.children[context.childIndex] = node
// 由于当前节点已经被新节点替换掉了,因此我们需要将 currentNode 更新为新节点
context.currentNode = node
},
// 用于删除当前节点
removeNode() {
if (context.parent) {
// 调用数组的 splice 方法,根据当前节点的索引删除当前节点
context.parent.children.splice(context.childIndex, 1)
// 将 context.currentNode 置空
context.currentNode = null
}
},
nodeTransforms: [transformElement, transformText, transformRoot],
}
traverseNode(ast, context)
dump(ast)
}
// 创建 StringLiteral 节点
function createStringLiteral(value) {
return {
type: 'StringLiteral',
value,
}
}
// 创建 Identifier 节点
function createIdentifier(name) {
return {
type: 'Identifier',
name,
}
}
// 创建 ArrayExpression 节点
function createArrayExpression(elements) {
return {
type: 'ArrayExpression',
elements,
}
}
// 创建 CallExpression 节点
function createCallExpression(callee, arguments) {
return {
type: 'CallExpression',
callee: createIdentifier(callee),
arguments,
}
}
function compile(template) {
const ast = parse(template)
transform(ast)
const code = generate(ast.jsNode)
return code
}
function generate(node) {
const context = {
code: '',
push(code) {
context.code += code
},
// 当前缩进的级别,初始值为 0,即没有缩进
currentIndent: 0,
// 该函数用来换行,即在代码字符串的后面追加 \n 字符,
// 另外,换行时应该保留缩进,所以我们还要追加 currentIndent * 2 个空格字符
newline() {
context.code += `\n` + ` `.repeat(context.currentIndent)
},
// 用来缩进,即让 currentIndent 自增后,调用换行函数
indent() {
context.currentIndent++
context.newline()
},
// 取消缩进,即让 currentIndent 自减后,调用换行函数
deIndent() {
context.currentIndent--
context.newline()
},
}
genNode(node, context)
return context.code
}
function genNode(node, context) {
switch (node.type) {
case 'FunctionDecl':
genFunctionDecl(node, context)
break
case 'ReturnStatement':
genReturnStatement(node, context)
break
case 'CallExpression':
genCallExpression(node, context)
break
case 'StringLiteral':
genStringLiteral(node, context)
break
case 'ArrayExpression':
genArrayExpression(node, context)
break
}
}
function genFunctionDecl(node, context) {
// 从 context 对象中取出工具函数
const { push, indent, deIndent } = context
// node.id 是一个标识符,用来描述函数的名称,即 node.id.name
push(`function ${node.id.name}`)
push(`(`)
// 调用 genNodeList 为函数的参数生成代码
genNodeList(node.params, context)
push(`)`)
push(`{}`)
// 缩进
indent()
// 为函数体生成代码,这里递归地调用了 genNode 函数
node.body.forEach((n) => genNode(n, context))
// 取消缩进
deIndent()
push(`}`)
}
function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
genNode(node, context)
if (i < nodes.length - 1) {
push(', ')
}
}
}
function genArrayExpression(node, context) {
const { push } = context
push('[')
genNodeList(node.elements, context)
push(']')
}
function genReturnStatement(node, context) {
const { push } = context
push(`return `)
genNode(node.return, context)
}
function genStringLiteral(node, context) {
const { push } = context
push(`'${node.value}'`)
}
function genCallExpression(node, context) {
const { push } = context
// 取得被调用函数名称和参数列表
const { callee, arguments: args } = node
// 生成函数调用代码
push(`${callee.name}(`)
// 调用 genNodeList 生成参数代码
genNodeList(args, context)
// 补全括号
push(`)`)
}
compile(`<div><p>Vue</p><p>Template</p></div>`)
解析器
// 定义文本模式,作为一个状态表
const TextModes = {
DATA: 'DATA',
RCDATA: 'RCDATA',
RAWTEXT: 'RAWTEXT',
CDATA: 'CDATA',
}
// 解析器函数,接收模板作为参数
function parse(str) {
// 定义上下文对象
const context = {
// source 是模板内容,用于在解析过程中进行消费
source: str,
// 解析器当前处于文本模式,初始模式为 DATA
mode: TextModes.DATA,
// advanceBy 函数用来消费指定数量的字符,它接收一个数字作为参数
advanceBy(num) {
// 根据给定字符数 num,截取位置 num 后的模板内容,并替换当前模板内容
context.source = context.source.slice(num)
},
// 无论是开始标签还是结束标签,都可能存在无用的空白字符,例如 <div >
advanceSpaces() {
// 匹配空白字符
const match = /^[\t\r\n\f ]+/.exec(context.source)
if (match) {
// 调用 advanceBy 函数消费空白字符
context.advanceBy(match[0].length)
}
},
}
// 调用 parseChildren 函数开始进行解析,它返回解析后得到的子节点
// parseChildren 函数接收两个参数:
// 第一个参数是上下文对象 context
// 第二个参数是由父代节点构成的节点栈,初始时栈为空
const nodes = parseChildren(context, [])
// 解析器返回 Root 根节点
return {
type: 'Root',
// 使用 nodes 作为根节点的 children
children: nodes,
}
}
function parseChildren(context, ancestors) {
// 定义 nodes 数组存储子节点,它将作为最终的返回值
let nodes = []
// 从上下文对象中取得当前状态,包括模式 mode 和模板内容 source
const { mode, source } = context
// 开启 while 循环,只要满足条件就会一直对字符串进行解析
// 关于 isEnd() 后文会详细讲解
while (!isEnd(context, ancestors)) {
let node
// 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (mode === TextModes.DATA && source[0] === '<') {
if (source[1] === '!') {
// 注释
node = parseComment(context)
} else if (source.startsWith('<!CDATA[')) {
// CDATA
node = parseCDATA(context, ancestors)
} else if (source[1] === '/') {
// 状态机遭遇了闭合标签,此时应该抛出错误,因为它缺少与之对应的开始标签
console.error('无效的结束标签')
continue
} else if (/[a-z]/i.test(source[1])) {
// 标签
node = parseElement(context, ancestors)
}
} else if (source.startsWith(`{{`)) {
// 解析插值
node = parseInterpolation(context)
}
}
// node 不存在,说明处于其他模式,即非 DATA 模式且非 RCDATA 模式
// 这时一切内容都作为文本处理
if (!node) {
// 解析文本节点
node = parseText(context)
}
// 将节点添加到 nodes 数组中
nodes.push(node)
}
// 当 while 循环停止后,说明子节点解析完毕,返回子节点
return nodes
}
function parseElement(context, ancestors) {
// 调用 parseTag 函数解析开始标签
const element = parseTag(context)
if (element.isSelfClosing) return element
// 切换到正确的文本模式
if (element.tag === 'textarea' || element.tag === 'title') {
// 如果由 parseTag 解析得到的标签是 <textarea> 或 <title>,则切换到 RCDATA 模式
context.mode = TextModes.RCDATA
} else if (/style|xmp|iframe|noembed|noframes|noscript/.test(element.tag)) {
// 如果由 parseTag 解析得到的标签是:
// <style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript>
// 则切换到 RAWTEXT 模式
context.mode = TextModes.RAWTEXT
} else {
// 否则切换到 DATA 模式
context.mode = TextModes.DATA
}
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
if (context.source.startsWith(`</${element.tag}>`)) {
// 再次调用 parseTag 函数解析结束标签,传递了第二个参数:'end'
parseTag(context, 'end')
} else {
// 缺少闭合标签
console.error(`${element.tag} 标签缺少闭合标签`)
}
return element
}
function isEnd(context, ancestors) {
// 当模板内容解析完毕后,停止
if (!context.source) return true
// 与父级节点栈内所有节点做比较
for (let i = ancestors.length - 1; i >= 0; --i) {
// 只要栈中存在与当前结束标签同名的节点,就停止状态机
if (context.source.startsWith(`</${ancestors[i].tag}>`)) {
return true
}
}
}
function parseTag(context, type = 'start') {
// 从上下文对象中拿到 advanceBy 函数
const { advanceBy, advanceSpaces } = context
// 处理开始标签和结束标签的正则表达式不同
const match =
type === 'start'
? // 匹配开始标签
/^<([a-z][^\t\r\n\f />]*)/i.exec(context.source)
: // 匹配结束标签
/^<\/([a-z][^\t\r\n\f />]*)/i.exec(context.source)
// 匹配成功后,正则表达式的第一个捕获组的值就是标签名称
const tag = match[1]
// 消费正则表达式匹配的全部内容,例如 '<div' 这段内容
advanceBy(match[0].length)
// 消费标签中无用的空白字符
advanceSpaces()
// 调用 parseAttributes 函数完成属性与指令的解析,并得到 props 数组,
// props 数组是由指令节点与属性节点共同组成的数组
const props = parseAttributes(context)
// 在消费匹配的内容后,如果字符串以 '/>' 开头,则说明这是一个自闭合标签
const isSelfClosing = context.source.startsWith('/>')
// 如果是自闭合标签,则消费 '/>', 否则消费 '>'
advanceBy(isSelfClosing ? 2 : 1)
// 返回标签节点
return {
type: 'Element',
// 标签名称
tag,
// 将 props 数组添加到标签节点上
props,
// 子节点留空
children: [],
// 是否自闭合
isSelfClosing,
}
}
function parseAttributes(context) {
const { advanceBy, advanceSpaces } = context
// 用来存储解析过程中产生的属性节点和指令节点
const props = []
// 开启 while 循环,不断地消费模板内容,直至遇到标签的“结束部分”为止
while (!context.source.startsWith('>') && !context.source.startsWith('/>')) {
// 解析属性或指令
// 该正则用于匹配属性名称
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
// 得到属性名称
const name = match[0]
// 消费属性名称
advanceBy(name.length)
// 消费属性名称与等于号之间的空白字符
advanceSpaces()
// 消费等于号
advanceBy(1)
// 消费等于号与属性值之间的空白字符
advanceSpaces()
// 属性值
let value = ''
// 获取当前模板内容的第一个字符
const quote = context.source[0]
// 判断属性值是否被引号引用
const isQuoted = quote === '"' || quote === "'"
if (isQuoted) {
// 属性值被引号引用,消费引号
advanceBy(1)
// 获取下一个引号的索引
const endQuoteIndex = context.source.indexOf(quote)
if (endQuoteIndex > -1) {
// 获取下一个引号之前的内容作为属性值
value = context.source.slice(0, endQuoteIndex)
// 消费属性值
advanceBy(value.length)
// 消费引号
advanceBy(1)
} else {
// 缺少引号错误
console.error('缺少引号')
}
} else {
// 代码运行到这里,说明属性值没有被引号引用
// 下一个空白字符之前的内容全部作为属性值
const match = /^[^\t\r\n\f >]+/.exec(context.source)
// 获取属性值
value = match[0]
// 消费属性值
advanceBy(value.length)
}
// 消费属性值后面的空白字符
advanceSpaces()
// 使用属性名称 + 属性值创建一个属性节点,添加到 props 数组中
props.push({
type: 'Attribute',
name,
value,
})
}
// 将解析结果返回
return props
}
function parseText(context) {
// endIndex 为文本内容的结尾索引,默认将整个模板剩余内容都作为文本内容
let endIndex = context.source.length
// 寻找字符 < 的位置索引
const ltIndex = context.source.indexOf('<')
// 寻找定界符 {{ 的位置索引
const delimiterIndex = context.source.indexOf('{{')
// 取 ltIndex 和当前 endIndex 中较小的一个作为新的结尾索引
if (ltIndex > -1 && ltIndex < endIndex) {
endIndex = ltIndex
}
// 取 delimiterIndex 和当前 endIndex 中较小的一个作为新的结尾索引
if (delimiterIndex > -1 && delimiterIndex < endIndex) {
endIndex = delimiterIndex
}
// 此时 endIndex 是最终的文本内容的结尾索引,调用 slice 函数截取文本内容
const content = context.source.slice(0, endIndex)
// 消耗文本内容
context.advanceBy(content.length)
// 返回文本节点
return {
// 节点类型
type: 'Text',
// 文本内容
content: decodeHtml(content),
}
}
const namedCharacterReferences = {
gt: '>',
'gt;': '>',
lt: '<',
'lt;': '<',
'ltcc;': '⪦',
}
const CCR_REPLACEMENTS = {
0x80: 0x20ac,
0x82: 0x201a,
0x83: 0x0192,
0x84: 0x201e,
0x85: 0x2026,
0x86: 0x2020,
0x87: 0x2021,
0x88: 0x02c6,
0x89: 0x2030,
0x8a: 0x0160,
0x8b: 0x2039,
0x8c: 0x0152,
0x8e: 0x017d,
0x91: 0x2018,
0x92: 0x2019,
0x93: 0x201c,
0x94: 0x201d,
0x95: 0x2022,
0x96: 0x2013,
0x97: 0x2014,
0x98: 0x02dc,
0x99: 0x2122,
0x9a: 0x0161,
0x9b: 0x203a,
0x9c: 0x0153,
0x9e: 0x017e,
0x9f: 0x0178,
}
// 第一个参数为要被解码的文本内容
// 第二个参数是一个布尔值,代表文本内容是否作为属性值
function decodeHtml(rawText, asAttr = false) {
let offset = 0
const end = rawText.length
// 经过解码后的文本将作为返回值被返回
let decodedText = ''
// 引用表中实体名称的最大长度
let maxCRNameLength = 0
// advance 函数用于消费指定长度的文本
function advance(length) {
offset += length
rawText = rawText.slice(length)
}
// 消费字符串,直到处理完毕为止
while (offset < end) {
// 用于匹配字符引用的开始部分,如果匹配成功,那么 head[0] 的值将有三种可能:
// 1. head[0] === '&',这说明该字符引用是命名字符引用
// 2. head[0] === '&#',这说明该字符引用是用十进制表示的数字字符引用
// 3. head[0] === '&#x',这说明该字符引用是用十六进制表示的数字字符引用
const head = /&(?:#x?)?/i.exec(rawText)
// 如果没有匹配,说明已经没有需要解码的内容了
if (!head) {
// 计算剩余内容的长度
const remaining = end - offset
// 将剩余内容加到 decodedText 上
decodedText += rawText.slice(0, remaining)
// 消费剩余内容
advance(remaining)
break
}
// head.index 为匹配的字符 & 在 rawText 中的位置索引
// 截取字符 & 之前的内容加到 decodedText 上
decodedText += rawText.slice(0, head.index)
// 消费字符 & 之前的内容
advance(head.index)
// 如果满足条件,则说明是命名字符引用,否则为数字字符引用
if (head[0] === '&') {
let name = ''
let value
// 字符 & 的下一个字符必须是 ASCII 字母或数字,这样才是合法的命名字符引用
if (/[0-9a-z]/i.test(rawText[1])) {
if (!maxCRNameLength) {
maxCRNameLength = Object.keys(namedCharacterReferences).reduce(
(max, name) => Math.max(max, name.length),
0
)
}
// 从最大长度开始对文本进行截取,并试图去引用表中找到对应的项
for (let length = maxCRNameLength; !value && length > 0; --length) {
// 截取字符 & 到最大长度之间的字符作为实体名称
name = rawText.substr(1, length)
// 使用实体名称去索引表中查找对应项的值
value = namedCharacterReferences[name]
}
// 如果找到了对应项的值,说明解码成功
if (value) {
// 检查实体名称的最后一个匹配字符是否是分号
const semi = name.endsWith(';')
// 如果解码的文本作为属性值,最后一个匹配的字符不是分号,
// 并且最后一个匹配字符的下一个字符是等于号(=)、ASCII 字母或数字,
// 由于历史原因,将字符 & 和实体名称 name 作为普通文本
if (
asAttr &&
!semi &&
/[=a-z0-9]/i.test(rawText[name.length + 1] || '')
) {
decodedText += '&' + name
advance(1 + name.length)
} else {
// 其他情况下,正常使用解码后的内容拼接到 decodedText 上
decodedText += value
advance(1 + name.length)
}
} else {
// 如果没有找到对应的值,说明解码失败
decodedText += '&' + name
advance(1 + name.length)
}
} else {
// 如果字符 & 的下一个字符不是 ASCII 字母或数字,则将字符 & 作为普通文本
decodedText += '&'
advance(1)
}
} else {
// 判断是十进制表示还是十六进制表示
const hex = head[0] === '&#x'
// 根据不同进制表示法,选用不同的正则
const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
// 最终,body[1] 的值就是 Unicode 码点
const body = pattern.exec(rawText)
if (body) {
// 根据对应的进制,将码点字符串转换为数字
const cp = Number.parseInt(body[1], hex ? 16 : 10)
// 码点的合法性检查
if (cp === 0) {
// 如果码点值为 0x00,替换为 0xfffd
cp = 0xfffd
} else if (cp > 0x10ffff) {
// 如果码点值超过 Unicode 的最大值,替换为 0xfffd
cp = 0xfffd
} else if (cp >= 0xd800 && cp <= 0xdfff) {
// 如果码点值处于 surrogate pair 范围内,替换为 0xfffd
cp = 0xfffd
} else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
// 如果码点值处于 noncharacter 范围内,则什么都不做,交给平台处理
// noop
} else if (
// 控制字符集的范围是:[0x01, 0x1f] 加上 [0x7f, 0x9f]
// 去掉 ASICC 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF)
// 0x0D(CR) 虽然也是 ASICC 空白符,但需要包含
(cp >= 0x01 && cp <= 0x08) ||
cp === 0x0b ||
(cp >= 0x0d && cp <= 0x1f) ||
(cp >= 0x7f && cp <= 0x9f)
) {
// 在 CCR_REPLACEMENTS 表中查找替换码点,如果找不到,则使用原码点
cp = CCR_REPLACEMENTS[cp] || cp
}
// 解码后追加到 decodedText 上
decodedText += String.fromCodePoint(cp)
// 消费整个数字字符引用的内容
advance(body[0].length)
} else {
// 如果没有匹配,则不进行解码操作,只是把 head[0] 追加到 decodedText 上并消费
decodedText += head[0]
advance(head[0].length)
}
}
}
return decodedText
}
function parseInterpolation(context) {
// 消费开始定界符
context.advanceBy('{{'.length)
// 找到结束定界符的位置索引
closeIndex = context.source.indexOf('}}')
if (closeIndex < 0) {
console.error('插值缺少结束定界符')
}
// 截取开始定界符与结束定界符之间的内容作为插值表达式
const content = context.source.slice(0, closeIndex)
// 消费表达式的内容
context.advanceBy(content.length)
// 消费结束定界符
context.advanceBy('}}'.length)
// 返回类型为 Interpolation 的节点,代表插值节点
return {
type: 'Interpolation',
// 插值节点的 content 是一个类型为 Expression 的表达式节点
content: {
type: 'Expression',
// 表达式节点的内容则是经过 HTML 解码后的插值表达式
content: decodeHtml(content),
},
}
}
function parseComment(context) {
context.advanceBy('<!--'.length)
closeIndex = context.source.indexOf('-->')
const content = context.source.slice(0, closeIndex)
context.advanceBy(content.length)
context.advanceBy('-->'.length)
return {
type: 'Comment',
content,
}
}
编译优化
const PatchFlags = {
TEXT: 1, // 代表节点有动态的 textContent
CLASS: 2, // 代表元素有动态的 class 绑定
STYLE: 3, // 代表元素有动态的 style 绑定
// 其他......
}
// 动态节点栈
const dynamicChildrenStack = []
// 当前动态节点集合
let currentDynamicChildren = null
// openBlock 用来创建一个新的动态节点集合,并将该集合压入栈中
function openBlock() {
dynamicChildrenStack.push((currentDynamicChildren = []))
}
// closeBlock 用来将通过 openBlock 创建的动态节点集合从栈中弹出
function closeBlock() {
currentDynamicChildren = dynamicChildrenStack.pop()
}
function createVNode(tag, props, children, flags) {
const key = props && props.key
props && delete props.key
const vnode = {
tag,
props,
key,
patchFlags: flags,
}
if (typeof flags !== 'undefined' && currentDynamicChildren) {
// 动态节点,将其添加到当前动态节点集合中
currentDynamicChildren.push(vnode)
}
return vnode
}
render() {
// 1. 使用 createBlock 代替 createVNode 来创建 block
// 2. 每当调用 createBlock 之前,先调用 openBlock
return (openBlock(), createBlock('div', null, [
createVNode('p', { class: 'foo' }, null, 1),
createVNode('p', { class: 'bar' }, null )
]))
}
function createBlock(tag, props, children) {
// block 本质上也是一个 vnode
const block = createVNode(tag, props, children)
// 将当前动态节点集合作为 block.dynamicChildren
block.dynamicChildren = currentDynamicChildren
// 关闭 block
closeBlock()
// 返回
return block
}
function patchElement(n1, n2) {
const el = n2.el = n1.el
const oldProps = n1.props
const newProps = n2.props
// 省略部分代码
if (n2.patchFlags) {
if (n2.patchFlags === 1) {
// textContent
} else if (n2.patchFlags === 2) {
// class
} else if (n2.patchFlags === 3) {
// style
} else {
// 全量更新
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if(!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
}
patchChildren(n1, n2, el)
}
if (n2.dynamicChildren) {
// 只会更新动态节点
patchBlockChildren(n1, n2)
} else {
patchChildren(n1, n2, el)
}
}
function patchBlockChildren(n1, n2) {
for (let i = 0; i < n2.dynamicChildren.length; i++) {
patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
}
}
// Block树
// v-if 在 Diff 过程中,渲染器能够根据 Block 的 key 值区分出更新前后的两个 Block 是不同的,并使用新的 Block 替换旧的Block。
const block = {
tag: 'div',
dynamicChildren: [
/* Block(Section v-if) 或者 Block(Section v-else) */
{
tag: 'section',
{
key: 0 /* key 值会根据不同的 Block 而发生变化 */
},
dynamicChildren: [...] },
]
}
// v-for 让带有 v-for 指令的标签也作为 Block 角色即可。
// 前后v-for的节点数量或顺序不同时,
// 只能放弃根据dynamicChildren 数组中的动态节点进行靶向更新的思路,
// 并回退到传统虚拟DOM 的 Diff 手段,即直接使用 Fragment 的 children 而非 dynamicChildren 来进行 Diff 操作。
const block = {
tag: 'div',
dynamicChildren: [
// 这是一个 Block,它有 dynamicChildren
{ tag: Fragment, dynamicChildren: [/* v-for 的节点 */] }
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
// 静态提升
// 把纯静态的节点提升到渲染函数之外后,在渲染函数内只会持有对静态节点的引用。
const hoist1 = createVNode('p', null, 'text')
function render() {
return (openBlock(), createBlock('div', null, [
hoist1, // 静态节点引用
createVNode('p', null, ctx.title, 1 /* TEXT */)
]))
}
// 静态提升的 props 对象
const hoistProp = { foo: 'bar', a: 'b' }
function render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', hoistProp, ctx.text)
]))
}
// 包含大量连续纯静态的标签节点:预字符串化
const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20 个...<p></p>')
render() {
return (openBlock(), createBlock('div', null, [
hoistStatic
]))
}
// 对内联事件处理函数进行缓存
// Vue.js 3 不仅会缓存内联事件处理函数,配合 v-once 还可实现对虚拟 DOM 的缓存。
function render(ctx, cache) {
return h(Comp, {
// 将内联事件处理函数缓存到 cache 数组中
onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))
})
}
// v-once
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
cache[1] || (
setBlockTracking(-1), // 阻止这段 VNode 被 Block 收集
cache[1] = h("div", null, ctx.foo, 1 /* TEXT */),
setBlockTracking(1), // 恢复
cache[1] // 整个表达式的值
)
]))
}