1. 条件v-if的首次渲染和更新渲染
1.1. v-if的首次渲染
下面我们在静态视图的基础上加入v-if
指令,并通过人肉单步调试的方式看看v-if
到底是如何渲染的吧!
<div v-scope="App"></div>
<script type="module"></script>
人肉单步调试开始:
- 调用
createApp
根据入参生成全局作用域rootScope
,创建根上下文rootCtx
; - 调用
mount
为<div v-scope="App"></div>
构建根块对象rootBlock
,并将其作为模板执行解析处理; - 解析时识别到
v-scope
属性,以全局作用域rootScope
为基础运算得到局部作用域scope
,并以根上下文rootCtx
为蓝本一同构建新的上下文ctx
,用于子节点的解析和渲染; - 获取
$template
属性值并生成HTML元素; - 深度优先遍历解析子节点(调用
walkChildren
); - 解析
<span v-if="status === 'offline'"> OFFLINE </span>
1.1.1. 解析<span v-if="status === 'offline'"> OFFLINE </span>
终于要和v-if
碰头了,那么我们继续人肉单步调试:
- 识别元素带上
v-if
属性,调用_if
原指令对元素及兄弟元素进行解析; _if
元指令会将附带v-if
和跟紧其后的附带v-else-if
和v-else
的元素转化为逻辑分支记录(Branch
);- 循环遍历分支,并为逻辑运算结果为
true
的分支创建块对象并销毁原有分支的块对象(首次渲染没有原分支的块对象),并提交渲染任务到异步队列。
// 文件 ./src/walk.ts
// 为便于理解,我对代码进行了精简
export const walk = (node: Node, ctx: Context): ChildNode | null | void {
const type = node.nodeType
if (type == 1) {
// node为Element类型
const el = node as Element
let exp: string | null
if ((exp = checkAttr(el, 'v-if'))) {
return _if(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点
}
}
}
// 文件 ./src/directives/if.ts
interface Branch {
exp?: string | null // 该分支逻辑运算表达式
el: Element // 该分支对应的模板元素,每次渲染时会以该元素为模板通过cloneNode复制一个实例插入到DOM树中
}
export const _if = (el: Element, exp: string, ctx: Context) => {
const parent = el.parentElement!
/* 锚点元素,由于v-if、v-else-if和v-else标识的元素可能在某个状态下都不位于DOM树上,
* 因此通过锚点元素标记插入点的位置信息,当状态发生变化时则可以将目标元素插入正确的位置。
*/
const anchor = new Comment('v-if')
parent.insertBefore(anchor, el)
// 逻辑分支,并将v-if标识的元素作为第一个分支
const branches: Branch[] = [
{
exp,
el
}
]
/* 定位v-else-if和v-else元素,并推入逻辑分支中
* 这里没有控制v-else-if和v-else的出现顺序,因此我们可以写成
* <span v-if="status=0"></span><span v-else></span><span v-else-if="status === 1"></span>
* 但效果为变成<span v-if="status=0"></span><span v-else></span>,最后的分支永远没有机会匹配。
*/
let elseEl: Element | null
let elseExp: string | null
while ((elseEl = el.nextElementSibling)) {
elseExp = null
if (
checkAttr(elseEl, 'v-else') === '' ||
(elseExp = checkAttr(elseEl, 'v-else-if'))
) {
// 从在线模板移除分支节点
parent.removeChild(elseEl)
branches.push({ exp: elseExp, el: elseEl })
}
else {
break
}
}
// 保存最近一个不带`v-else`和`v-else-if`节点作为下一轮遍历解析的模板节点
const nextNode = el.nextSibling
// 从在线模板移除带`v-if`节点
parent.removeChild(el)
let block: Block | undefined // 当前逻辑运算结构为true的分支对应块对象
let activeBranchIndex: number = -1 // 当前逻辑运算结构为true的分支索引
// 若状态发生变化导致逻辑运算结构为true的分支索引发生变化,则需要销毁原有分支对应块对象(包含中止旗下的副作用函数监控状态变化,执行指令的清理函数和递归触发子块对象的清理操作)
const removeActiveBlock = () => {
if (block) {
// 重新插入锚点元素来定位插入点
parent.insertBefore(anchor, block.el)
block.remove()
// 解除对已销毁的块对象的引用,让GC回收对应的JavaScript对象和detached元素
block = undefined
}
}
// 向异步任务对立压入渲染任务,在本轮Event Loop的Micro Queue执行阶段会执行一次
ctx.effect(() => {
// 此方法体的代码是异步执行的
for (let i = 0; i < branches.length; i++) {
const { exp, el } = branches[i]
if (!exp || evaluate(ctx.scope, exp)) {
// 若当前运算结果为true的分支,不是活跃分支,那么需要移除活跃分支,并创建新分支插入到锚点元素前面
if (i !== activeBranchIndex) {
removeActiveBlock()
block = new Block(el, ctx)
block.insert(parent, anchor)
parent.removeChild(anchor)
activeBranchIndex = i
}
return
}
}
// 所有分支运算结果都是false,则需要移除活跃分支
activeBranchIndex = -1
removeActiveBlock()
})
// 返回最近一个不附带`v-if`、`v-if-else`或`v-else`的兄弟节点
return nextNode
}
那么在电脑足够慢的情况下,我们可以看到页面显示效果如下:
- 空白一片
显示所有元素
<span v-if="status === 'offline'"> OFFLINE </span> <span v-else-if="status === 'UNKNOWN'"> UNKNOWN </span> <span v-else> ONLINE </span>
添加锚点元素
<!-- v-if --> <span v-if="status === 'offline'"> OFFLINE </span> <span v-else-if="status === 'UNKNOWN'"> UNKNOWN </span> <span v-else> ONLINE </span>
移除
v-else-if
<!-- v-if --> <span v-if="status === 'offline'"> OFFLINE </span> <span v-else> ONLINE </span>
移除
v-else
<!-- v-if --> <span v-if="status === 'offline'"> OFFLINE </span>
移除
v-if
,只剩下锚点元素<!-- v-if -->
显示
v-else
<span v-else> ONLINE </span> <!-- v-if -->
1.1.2. 解读Block的insert和remove方法
在_if
元指令中我们看到块对象block
的身影,其实块对象不单单是管控DOM操作的单元,而且它是用于表示树结构不稳定的部分。如节点的增加和删除,将导致树结构的不稳定,把这些不稳定的部分打包成独立的块对象,并封装各自构建和删除时执行资源回收等操作,这样不仅提高代码的可读性也提高程序的运行效率。
接下来我们看看子块对象的构造函数和insert
、remove
方法:
// 文件 ./src/block.ts
export class Block {
constructor(template: Element, parentCtx: Context, isRoot = false) {
if (isRoot) {
// ...
}
else {
// 以v-if、v-else-if和v-else分支的元素作为模板创建元素实例
this.template = template.cloneNode(true) as Element
}
if (isRoot) {
// ...
}
else {
this.parentCtx = parentCtx
parentCtx.blocks.push(this)
this.ctx = createContext(parentCtx)
}
}
// 由于当前示例没有用到<template>元素,因此我对代码进行了删减
insert(parent: Element, anchor: Node | null = null) {
parent.insertBefore(this.template, anchor)
}
// 由于当前示例没有用到<template>元素,因此我对代码进行了删减
remove() {
if (this.parentCtx) {
remove(this.parentCtx.blocks, this)
}
// 移除当前块对象的根节点,其子孙节点都一并被移除
this.template.parentNode!.removeChild(this.template)
this.teardown()
}
teardown() {
// 先递归调用子块对象的清理方法
this.ctx.blocks.forEach(child => {
child.teardown()
})
// 包含中止副作用函数监控状态变化
this.ctx.effects.forEach(stop)
// 执行指令的清理函数
this.ctx.cleanups.forEach(fn => fn())
}
}
由v-if
、v-else-if
或v-else
标识的每个分支都会为其创建独立的块对象,而当分支被销毁时同样会以块对象为单元执行销毁操作。
1.2. 小结
由于v-if
的渲染十分简单直接,所以首次渲染和更新渲染可以共享同一套逻辑处理,但它会引起树结构的不稳定,因此必须通过块对象(Block
)对每个分支进行管理。