Vue3 的模板編譯優(yōu)化
Vue3 正式發(fā)布已經(jīng)有一段時(shí)間了,前段時(shí)間寫了一篇文章(《Vue 模板編譯原理》)分析 Vue 的模板編譯原理。今天的文章打算學(xué)習(xí)下 Vue3 下的模板編譯與 Vue2 下的差異,以及 VDOM 下 Diff 算法的優(yōu)化。
編譯入口
了解過 Vue3 的同學(xué)肯定知道 Vue3 引入了新的組合 Api,在組件 mount 階段會(huì)調(diào)用 setup 方法,之后會(huì)判斷 render 方法是否存在,如果不存在會(huì)調(diào)用 compile 方法將 template 轉(zhuǎn)化為 render。
//?packages/runtime-core/src/renderer.ts
const?mountComponent?=?(initialVNode,?container)?=>?{
??const?instance?=?(
????initialVNode.component?=?createComponentInstance(
??????//?...params
????)
??)
??//?調(diào)用?setup
??setupComponent(instance)
}
//?packages/runtime-core/src/component.ts
let?compile
export?function?registerRuntimeCompiler(_compile)?{
??compile?=?_compile
}
export?function?setupComponent(instance)?{
??const?Component?=?instance.type
??const?{?setup?}?=?Component
??if?(setup)?{
????//?...調(diào)用?setup
??}
??if?(compile?&&?Component.template?&&?!Component.render)?{
???//?如果沒有?render?方法
????//?調(diào)用?compile?將?template?轉(zhuǎn)為?render?方法
????Component.render?=?compile(Component.template,?{...})
??}
}
這部分都是 runtime-core 中的代碼,之前的文章有講過 Vue 分為完整版和 runtime 版本。如果使用 vue-loader 處理 .vue 文件,一般都會(huì)將 .vue 文件中的 template 直接處理成 render 方法。
//??需要編譯器
Vue.createApp({
??template:?'{{?hi?}}'
})
//?不需要
Vue.createApp({
??render()?{
????return?Vue.h('div',?{},?this.hi)
??}
})
完整版與 runtime 版的差異就是,完整版會(huì)引入 compile 方法,如果是 vue-cli 生成的項(xiàng)目就會(huì)抹去這部分代碼,將 compile 過程都放到打包的階段,以此優(yōu)化性能。runtime-dom 中提供了 registerRuntimeCompiler 方法用于注入 compile 方法。

主流程
在完整版的 index.js 中,調(diào)用了 ?registerRuntimeCompiler 將 compile 進(jìn)行注入,接下來我們看看注入的 compile 方法主要做了什么。
//?packages/vue/src/index.ts
import?{?compile?}?from?'@vue/compiler-dom'
//?編譯緩存
const?compileCache?=?Object.create(null)
//?注入?compile?方法
function?compileToFunction(
?//?模板
??template:?string?|?HTMLElement,
??//?編譯配置
??options?:?CompilerOptions
):?RenderFunction?{
??if?(!isString(template))?{
????//?如果?template?不是字符串
????//?則認(rèn)為是一個(gè)?DOM?節(jié)點(diǎn),獲取?innerHTML
????if?(template.nodeType)?{
??????template?=?template.innerHTML
????}?else?{
??????return?NOOP
????}
??}
??//?如果緩存中存在,直接從緩存中獲取
??const?key?=?template
??const?cached?=?compileCache[key]
??if?(cached)?{
????return?cached
??}
??//?如果是?ID?選擇器,這獲取?DOM?元素后,取?innerHTML
??if?(template[0]?===?'#')?{
????const?el?=?document.querySelector(template)
????template?=?el???el.innerHTML?:?''
??}
??//?調(diào)用?compile?獲取?render?code
??const?{?code?}?=?compile(
????template,
????options
??)
??//?將?render?code?轉(zhuǎn)化為?function
??const?render?=?new?Function(code)();
?//?返回?render?方法的同時(shí),將其放入緩存
??return?(compileCache[key]?=?render)
}
//?注入?compile
registerRuntimeCompiler(compileToFunction)
在講 Vue2 模板編譯的時(shí)候已經(jīng)講過,compile 方法主要分為三步,Vue3 的邏輯類似:
模板編譯,將模板代碼轉(zhuǎn)化為 AST; 優(yōu)化 AST,方便后續(xù)虛擬 DOM 更新; 生成代碼,將 AST 轉(zhuǎn)化為可執(zhí)行的代碼;
//?packages/compiler-dom/src/index.ts
import?{?baseCompile,?baseParse?}?from?'@vue/compiler-core'
export?function?compile(template,?options)?{
??return?baseCompile(template,?options)
}
//?packages/compiler-core/src/compile.ts
import?{?baseParse?}?from?'./parse'
import?{?transform?}?from?'./transform'
import?{?transformIf?}?from?'./transforms/vIf'
import?{?transformFor?}?from?'./transforms/vFor'
import?{?transformText?}?from?'./transforms/transformText'
import?{?transformElement?}?from?'./transforms/transformElement'
import?{?transformOn?}?from?'./transforms/vOn'
import?{?transformBind?}?from?'./transforms/vBind'
import?{?transformModel?}?from?'./transforms/vModel'
export?function?baseCompile(template,?options)?{
??//?解析?html,轉(zhuǎn)化為?ast
??const?ast?=?baseParse(template,?options)
??//?優(yōu)化?ast,標(biāo)記靜態(tài)節(jié)點(diǎn)
??transform(ast,?{
????...options,
????nodeTransforms:?[
??????transformIf,
??????transformFor,
??????transformText,
??????transformElement,
??????//?...?省略了部分?transform
????],
????directiveTransforms:?{
??????on:?transformOn,
??????bind:?transformBind,
??????model:?transformModel
????}
??})
??//?將?ast?轉(zhuǎn)化為可執(zhí)行代碼
??return?generate(ast,?options)
}
計(jì)算 PatchFlag
這里大致的邏輯與之前的并沒有多大的差異,主要是 optimize 方法變成了 ?transform 方法,而且默認(rèn)會(huì)對(duì)一些模板語法進(jìn)行 transform。這些 transform 就是后續(xù)虛擬 DOM 優(yōu)化的關(guān)鍵,我們先看看 transform 的代碼 。
//?packages/compiler-core/src/transform.ts
export?function?transform(root,?options)?{
??const?context?=?createTransformContext(root,?options)
??traverseNode(root,?context)
}
export?function?traverseNode(node,?context)?{
??context.currentNode?=?node
??const?{?nodeTransforms?}?=?context
??const?exitFns?=?[]
??for?(let?i?=?0;?i?????//?Transform?會(huì)返回一個(gè)退出函數(shù),在處理完所有的子節(jié)點(diǎn)后再執(zhí)行
????const?onExit?=?nodeTransforms[i](node,?context)
????if?(onExit)?{
??????if?(isArray(onExit))?{
????????exitFns.push(...onExit)
??????}?else?{
????????exitFns.push(onExit)
??????}
????}
??}
??traverseChildren(node,?context)
??context.currentNode?=?node
??//?執(zhí)行所以?Transform?的退出函數(shù)
??let?i?=?exitFns.length
??while?(i--)?{
????exitFns[i]()
??}
}
我們重點(diǎn)看一下 transformElement 的邏輯:
//?packages/compiler-core/src/transforms/transformElement.ts
export?const?transformElement:?NodeTransform?=?(node,?context)?=>?{
??//?transformElement?沒有執(zhí)行任何邏輯,而是直接返回了一個(gè)退出函數(shù)
??//?說明?transformElement?需要等所有的子節(jié)點(diǎn)處理完后才執(zhí)行
??return?function?postTransformElement()?{
????const?{?tag,?props?}?=?node
????let?vnodeProps
????let?vnodePatchFlag
????const?vnodeTag?=?node.tagType?===?ElementTypes.COMPONENT
????????resolveComponentType(node,?context)
??????:?`"${tag}"`
????
????let?patchFlag?=?0
????//?檢測(cè)節(jié)點(diǎn)屬性
????if?(props.length?>?0)?{
??????//?檢測(cè)節(jié)點(diǎn)屬性的動(dòng)態(tài)部分
??????const?propsBuildResult?=?buildProps(node,?context)
??????vnodeProps?=?propsBuildResult.props
??????patchFlag?=?propsBuildResult.patchFlag
????}
????//?檢測(cè)子節(jié)點(diǎn)
????if?(node.children.length?>?0)?{
??????if?(node.children.length?===?1)?{
????????const?child?=?node.children[0]
????????//?檢測(cè)子節(jié)點(diǎn)是否為動(dòng)態(tài)文本
????????if?(!getStaticType(child))?{
??????????patchFlag?|=?PatchFlags.TEXT
????????}
??????}
????}
????//?格式化?patchFlag
????if?(patchFlag?!==?0)?{
????????vnodePatchFlag?=?String(patchFlag)
????}
????node.codegenNode?=?createVNodeCall(
??????context,
??????vnodeTag,
??????vnodeProps,
??????vnodeChildren,
??????vnodePatchFlag
????)
??}
}
buildProps 會(huì)對(duì)節(jié)點(diǎn)的屬性進(jìn)行一次遍歷,由于內(nèi)部源碼涉及很多其他的細(xì)節(jié),這里的代碼是經(jīng)過簡(jiǎn)化之后的,只保留了 patchFlag 相關(guān)的邏輯。
export?function?buildProps(
??node:?ElementNode,
??context:?TransformContext,
??props:?ElementNode['props']?=?node.props
)?{
??let?patchFlag?=?0
??for?(let?i?=?0;?i?????const?prop?=?props[i]
????const?[key,?name]?=?prop.name.split(':')
????if?(key?===?'v-bind'?||?key?===?'')?{
??????if?(name?===?'class')?{
???????//?如果包含?:class?屬性,patchFlag?|?CLASS
????????patchFlag?|=?PatchFlags.CLASS
??????}?else?if?(name?===?'style')?{
???????//?如果包含?:style?屬性,patchFlag?|?STYLE
????????patchFlag?|=?PatchFlags.STYLE
??????}
????}
??}
??return?{
????patchFlag
??}
}
上面的代碼只展示了三種 patchFlag 的類型:
節(jié)點(diǎn)只有一個(gè)文本子節(jié)點(diǎn),且該文本包含動(dòng)態(tài)的數(shù)據(jù)( TEXT = 1)
<p>name:?{{name}}p>
節(jié)點(diǎn)包含可變的 class 屬性( CLASS = 1 << 1)
<div?:class="{?active:?isActive?}">div>
節(jié)點(diǎn)包含可變的 style 屬性( STYLE = 1 << 2)
<div?:style="{?color:?color?}">div>
可以看到 PatchFlags 都是數(shù)字 1 經(jīng)過 左移操作符 計(jì)算得到的。
export?const?enum?PatchFlags?{
??TEXT?=?1,?????????????//?1,?二進(jìn)制?0000?0001
??CLASS?=?1?<1,???????//?2,?二進(jìn)制?0000?0010
??STYLE?=?1?<2,???????//?4,?二進(jìn)制?0000?0100
??PROPS?=?1?<3,???????//?8,?二進(jìn)制?0000?1000
??...
}
從上面的代碼能看出來,patchFlag 的初始值為 0,每次對(duì) patchFlag 都是執(zhí)行 | (或)操作。如果當(dāng)前節(jié)點(diǎn)是一個(gè)只有動(dòng)態(tài)文本子節(jié)點(diǎn)且同時(shí)具有動(dòng)態(tài) style 屬性,最后得到的 patchFlag 為 5(二進(jìn)制:0000 0101)。
"{?color:?color?}">name:?{{name}}</p>
patchFlag?=?0
patchFlag?|=?PatchFlags.STYLE
patchFlag?|=?PatchFlags.TEXT
//?或運(yùn)算:兩個(gè)對(duì)應(yīng)的二進(jìn)制位中只要一個(gè)是1,結(jié)果對(duì)應(yīng)位就是1。
//?0000?0001
//?0000?0100
//?------------
//?0000?0101??=>??十進(jìn)制?5

我們將上面的代碼放到 Vue3 中運(yùn)行:
const?app?=?Vue.createApp({
??data()?{
????return?{
??????color:?'red',
??????name:?'shenfq'
????}
??},
??template:?`
???name:?{{name}}
??`
})
app.mount('#app')
最后生成的 render 方法如下,和我們之前的描述基本一致。

render 優(yōu)化
Vue3 在虛擬 DOM Diff 時(shí),會(huì)取出 patchFlag 和需要進(jìn)行的 diff 類型進(jìn)行 &(與)操作,如果結(jié)果為 true 才進(jìn)入對(duì)應(yīng)的 diff。

還是拿之前的模板舉例:
<p?:style="{?color:?color?}">name:?{{name}}p>
如果此時(shí)的 name 發(fā)生了修改,p 節(jié)點(diǎn)進(jìn)入了 diff 階段,此時(shí)會(huì)將判斷 patchFlag & PatchFlags.TEXT ,這個(gè)時(shí)候結(jié)果為真,表明 p 節(jié)點(diǎn)存在文本修改的情況。

patchFlag?=?5
patchFlag?&?PatchFlags.TEXT
//?或運(yùn)算:只有對(duì)應(yīng)的兩個(gè)二進(jìn)位都為1時(shí),結(jié)果位才為1。
//?0000?0101
//?0000?0001
//?------------
//?0000?0001??=>??十進(jìn)制?1
if?(patchFlag?&?PatchFlags.TEXT)?{
??if?(oldNode.children?!==?newNode.children)?{
????//?修改文本
????hostSetElementText(el,?newNode.children)
??}
}
但是進(jìn)行 ?patchFlag & PatchFlags.CLASS 判斷時(shí),由于節(jié)點(diǎn)并沒有動(dòng)態(tài) Class,返回值為 0,所以就不會(huì)對(duì)該節(jié)點(diǎn)的 class 屬性進(jìn)行 diff,以此來優(yōu)化性能。

patchFlag?=?5
patchFlag?&?PatchFlags.CLASS
//?或運(yùn)算:只有對(duì)應(yīng)的兩個(gè)二進(jìn)位都為1時(shí),結(jié)果位才為1。
//?0000?0101
//?0000?0010
//?------------
//?0000?0000??=>??十進(jìn)制?0
總結(jié)
其實(shí) Vue3 相關(guān)的性能優(yōu)化有很多,這里只單獨(dú)將 patchFlag 的十分之一的內(nèi)容拿出來講了,Vue3 還沒正式發(fā)布的時(shí)候就有看到說 Diff 過程會(huì)通過 patchFlag 來進(jìn)行性能優(yōu)化,所以打算看看他的優(yōu)化邏輯,總的來說還是有所收獲。
推薦閱讀
