In-depth v-if
how it works
<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>
Human flesh single-step debugging:
- Call
createApp
Generate the global scope according to the input parametersrootScope
, and create the root contextrootCtx
; - Call
mount
for<div v-scope="App"></div>
to build the root block objectrootBlock
and use it as a template to perform parsing processing; -
v-scope
attribute is recognized during parsing, and the global scoperootScope
is used as the basis to obtain the local scopescope
, and the root contextrootCtx
Build a new context for the blueprintctx
for parsing and rendering of child nodes; - Get
$template
attribute value and generate HTML element; - Depth-first traversal of parsing child nodes (call
walkChildren
); - Parse
<span v-if="status === 'offline'"> OFFLINE </span>
Parse <span v-if="status === 'offline'"> OFFLINE </span>
The book is connected to the previous one, and we continue to debug the human flesh step by step:
- Identify the element with the
v-if
attribute, and call the_if
original instruction to parse the element and its sibling elements; - Convert the elements with ---13f5d14671e4073ecf90d8375121ed33
v-if
and the accompanyingv-else-if
andv-else
into logical branch records; - Loop through the branches, create a block object for the branch whose logical operation result is
true
and destroy the block object of the original branch (the first rendering of the block object without the original branch), and submit the rendering task to the asynchronous queue.
// 文件 ./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
}
Let's take a look at the constructor and insert
and remove
methods of the sub-block object below.
// 文件 ./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())
}
}
In-depth v-for
how it works
<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">
I'm the one of options
</option>
</select>
`,
}
values: [1,2,3]
}).mount('[v-scope]')
</script>
Human flesh single-step debugging:
- Call
createApp
Generate the global scope according to the input parametersrootScope
, and create the root contextrootCtx
; - Call
mount
<div v-scope="App"></div>
build a root block objectrootBlock
and use it as a template to perform parsing processing; - Parsing the identified
v-scope
property to the global scoperootScope
give local scope based computingscope
, root and contextrootCtx
Build a new context for the blueprintctx
for parsing and rendering of child nodes; - Get
$template
attribute value and generate HTML element; - Depth-first traversal of parsing child nodes (call
walkChildren
); - Parse
<option v-for="val in values" v-key="val">I'm the one of options</option>
Parse <option v-for="val in values" v-key="val">I'm the one of options</option>
The book is connected to the previous one, and we continue to debug the human flesh step by step:
- Identify the element with the
v-for
attribute, and call_for
the original instruction to parse the element; - Extract the expression strings of sets and set elements in ---553e0ec5f212778caab15007a493566b
v-for
through regular expressions, and the expression strings ofkey
; - Create separate scopes based on each collection element, and create separate block object render elements.
// 文件 ./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
}
Summarize
We see that block objects will be generated in the parsing process of v-if
and v-for
, and each branch of v-if
corresponds to a block object, while v-for
means that each child element corresponds to a block object. In fact, the block object is not only a unit that controls DOM manipulation, but it is used to represent the unstable part of the tree structure. For example, the addition and deletion of nodes will lead to the instability of the tree structure. Pack these unstable parts into independent block objects, and encapsulate operations such as resource recovery when building and deleting respectively, which not only improves the readability of the code but also Improve the efficiency of the program.
v-if
uses the same logic for the first rendering and re-rendering, but v-for
will be used when re-rendering key
elements to improve efficiency, and can be re-rendered The algorithm will be replicated quite a bit. In the next article we will dive into how v-for
works when re-rendering, so stay tuned :)
"Anatomy of Petite-Vue Source Code" booklet
"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also makes a detailed analysis of the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。