深入v-if的工作原理
<div v
–scope
=“App”></div
>
<script type
=“module”>
import { createApp
} from https://unpkg.com/petite-vue?module
createApp({
App: {
$template: `
<span v-if=”status === offline”> OFFLINE </span>
<span v-else-if=”status === UNKOWN”> UNKOWN </span>
<span v-else> ONLINE </span>
`,
}
status: online
}).mount([v-scope])
</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>解析<span v-if=”status === offline”> OFFLINE </span>
书接上一回,我们继续人肉单步调试:
识别元素带上v-if属性,调用_if原指令对元素及兄弟元素进行解析;将附带v-if和跟紧其后的附带v-else-if和v-else的元素转化为逻辑分支记录;循环遍历分支,并为逻辑运算结果为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
)) {
if (i
!== activeBranchIndex
) {
removeActiveBlock()
block
= new Block(el
, ctx
)
block
.insert(parent
, anchor
)
parent
.removeChild(anchor
)
activeBranchIndex
= i
}
return
}
}
activeBranchIndex
= –1
removeActiveBlock()
})
return nextNode
}
下面我们看看子块对象的构造函数和insert、remove方法
// 文件 ./src/block.ts
export class Block {
constuctor(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
) {
// TODO: function `remove` is located at @vue/shared
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-for的工作原理
<div v
–scope
=“App”></div
>
<script type
=“module”>
import { createApp
} from https://unpkg.com/petite-vue?module
createApp({
App: {
$template: `
<select>
<option v-for=”val of values” v-key=”val”>
Im the one of options
</option>
</select>
`,
}
values: [1,2,3]
}).mount([v-scope])
</script
>
人肉单步调试:
调用createApp根据入参生成全局作用域rootScope,创建根上下文rootCtx;调用mount为<div v-scope=”App”></div>构建根块对象rootBlock,并将其作为模板执行解析处理;解析时识别到v-scope属性,以全局作用域rootScope为基础运算得到局部作用域scope,并以根上下文rootCtx为蓝本一同构建新的上下文ctx,用于子节点的解析和渲染;获取$template属性值并生成HTML元素;深度优先遍历解析子节点(调用walkChildren);解析<option v-for=”val in values” v-key=”val”>Im the one of options</option>解析<option v-for=”val in values” v-key=”val”>Im the one of options</option>
书接上一回,我们继续人肉单步调试:
识别元素带上v-for属性,调用_for原指令对该元素解析;通过正则表达式提取v-for中集合和集合元素的表达式字符串,和key的表达式字符串;基于每个集合元素创建独立作用域,并创建独立的块对象渲染元素。
// 文件 ./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-for))) {
return _for(el
, exp
, ctx
) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点
}
}
}
// 文件 ./src/directives/for.ts
/* [\s\S]*表示识别空格字符和非空格字符若干个,默认为贪婪模式,即 `(item, index) in value` 就会匹配整个字符串。
* 修改为[\s\S]*?则为懒惰模式,即`(item, index) in value`只会匹配`(item, index)`
*/
const forAliasRE
= /([\s\S]*?)\s+(?:in)\s+([\s\S]*?)/
// 用于移除`(item, index)`中的`(`和`)`
const stripParentRE
= /^\(|\)$/g
// 用于匹配`item, index`中的`, index`,那么就可以抽取出value和index来独立处理
const forIteratorRE
= /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
type KeyToIndexMap
= Map
<any
, number
>
// 为便于理解,我们假设只接受`v-for=”val in values”`的形式,并且所有入参都是有效的,对入参有效性、解构等代码进行了删减
export const _for = (el: Element, exp: string, ctx: Context) => {
// 通过正则表达式抽取表达式字符串中`in`两侧的子表达式字符串
const inMatch
= exp
.match(forAliasRE
)
// 保存下一轮遍历解析的模板节点
const nextNode
= el
.nextSibling
// 插入锚点,并将带`v-for`的元素从DOM树移除
const parent
= el
.parentElement
!
const anchor
= new Text()
parent
.insertBefore(anchor
, el
)
parent
.removeChild(el
)
const sourceExp
= inMatch
[2].trim() // 获取`(item, index) in value`中`value`
let valueExp
= inMatch
[1].trim().replace(stripParentRE
, ).trim() // 获取`(item, index) in value`中`item, index`
let indexExp: string
| undefined
let keyAttr
= key
let keyExp
=
el
.getAttribute(keyAttr
) ||
el
.getAttribute(keyAttr
= :key) ||
el
.getAttribute(keyAttr
= v-bind:key)
if (keyExp
) {
el
.removeAttribute(keyExp
)
// 将表达式序列化,如`value`序列化为`”value”`,这样就不会参与后面的表达式运算
if (keyAttr
=== key) keyExp
= JSON.stringify(keyExp
)
}
let match
if (match
= valueExp
.match(forIteratorRE
)) {
valueExp
= valueExp
.replace(forIteratorRE
, ).trim() // 获取`item, index`中的item
indexExp
= match
[1].trim() // 获取`item, index`中的index
}
let mounted
= false // false表示首次渲染,true表示重新渲染
let blocks: Block
[]
let childCtxs: Context
[]
let keyToIndexMap: KeyToIndexMap
// 用于记录key和索引的关系,当发生重新渲染时则复用元素
const createChildContexts
= (source
: unknown
): [Context
[], KeyToIndexMap
] => {
const map: KeyToIndexMap
= new Map()
const ctxs: Context
[] = []
if (isArray(source
)) {
for (let i
= 0; i
< source
.length
; i
++) {
ctxs
.push(createChildContext(map
, source
[i
], i
))
}
}
return [ctxs
, map
]
}
// 以集合元素为基础创建独立的作用域
const createChildContext
= (
map: KeyToIndexMap
,
value: any
, // the item of collection
index: number
// the index of item of collection
): Context => {
const data: any
= {}
data
[valueExp
] = value
indexExp
&& (data
[indexExp
] = index
)
// 为每个子元素创建独立的作用域
const childCtx
= createScopedContext(ctx
, data
)
// key表达式在对应子元素的作用域下运算
const key
= keyExp
? evaluate(childCtx
.scope
, keyExp
) : index
map
.set(key
, index
)
childCtx
.key
= key
return childCtx
}
// 为每个子元素创建块对象
const mountBlock = (ctx: Conext, ref: Node) => {
const block
= new Block(el
, ctx
)
block
.key
= ctx
.key
block
.insert(parent
, ref
)
return block
}
ctx
.effect(() => {
const source
= evaluate(ctx
.scope
, sourceExp
) // 运算出`(item, index) in items`中items的真实值
const prevKeyToIndexMap
= keyToIndexMap
// 生成新的作用域,并计算`key`,`:key`或`v-bind:key`
;[childCtxs
, keyToIndexMap
] = createChildContexts(source
)
if (!mounted
) {
// 为每个子元素创建块对象,解析子元素的子孙元素后插入DOM树
blocks
= childCtxs
.map(s => mountBlock(s
, anchor
))
mounted
= true
}
// 由于我们示例只研究静态视图,因此重新渲染的代码,我们后面再深入了解吧
})
return nextNode
}
总结
我们看到在v-if和v-for的解析过程中都会生成块对象,而且是v-if的每个分支都对应一个块对象,而v-for则是每个子元素都对应一个块对象。其实块对象不单单是管控DOM操作的单元,而且它是用于表示树结构不稳定的部分。如节点的增加和删除,将导致树结构的不稳定,把这些不稳定的部分打包成独立的块对象,并封装各自构建和删除时执行资源回收等操作,这样不仅提高代码的可读性也提高程序的运行效率。
v-if的首次渲染和重新渲染采用同一套逻辑,但v-for在重新渲染时会采用key复用元素从而提高效率,可以重新渲染时的算法会复制不少。下一篇我们将深入了解v-for在重新渲染时的工作原理,敬请期待:)
尊重原创,转载请注明来自:https://cloud.tencent.com/developer/article/1997335 肥仔John