《Petite-Vue源码剖析》
结合示例从在线渲染、响应式系统和沙箱模型分别对源码逐行解读,其中还对响应式系统中利用JS引擎的SMI优化依赖清理算法作详细分析。绝对是入门Vue3源码前绝佳的踏脚石喜欢的话记得转发、赞赏哦
深入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