聊聊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: '<div>{{ hi }}</div>'
- })
- // 不需要
- 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ì)對一些模板語法進(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 < nodeTransforms.length; 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
- // 檢測節(jié)點(diǎn)屬性
- if (props.length > 0) {
- // 檢測節(jié)點(diǎn)屬性的動(dòng)態(tài)部分
- const propsBuildResult = buildProps(node, context)
- vnodeProps = propsBuildResult.props
- patchFlag = propsBuildResult.patchFlag
- }
- // 檢測子節(jié)點(diǎn)
- if (node.children.length > 0) {
- if (node.children.length === 1) {
- const child = node.children[0]
- // 檢測子節(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ì)對節(jié)點(diǎn)的屬性進(jìn)行一次遍歷,由于內(nèi)部源碼涉及很多其他的細(xì)節(jié),這里的代碼是經(jīng)過簡化之后的,只保留了 patchFlag 相關(guān)的邏輯。
- export function buildProps(
- node: ElementNode,
- context: TransformContext,
- props: ElementNode['props'] = node.props
- ) {
- let patchFlag = 0
- for (let i = 0; i < props.length; 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,每次對 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)。
- <p :style="{ color: color }">name: {{name}}</p>
我們將上面的代碼放到 Vue3 中運(yùn)行:
- const app = Vue.createApp({
- data() {
- return {
- color: 'red',
- name: 'shenfq'
- }
- },
- template: `<div>
- <p :style="{ color: color }">name: {{name}}</p>
- </div>`
- })
- app.mount('#app')
最后生成的 render 方法如下,和我們之前的描述基本一致。
function render() {}
render 優(yōu)化
Vue3 在虛擬 DOM Diff 時(shí),會(huì)取出 patchFlag 和需要進(jìn)行的 diff 類型進(jìn)行 &(與)操作,如果結(jié)果為 true 才進(jìn)入對應(yīng)的 diff。
patchFlag 判斷
還是拿之前的模板舉例:
- <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
- patchFlag = 5
- patchFlag & PatchFlags.TEXT
- // 或運(yùn)算:只有對應(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ì)對該節(jié)點(diǎn)的 class 屬性進(jìn)行 diff,以此來優(yōu)化性能。
patchFlag
- patchFlag = 5
- patchFlag & PatchFlags.CLASS
- // 或運(yùn)算:只有對應(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)化邏輯,總的來說還是有所收獲。
本文轉(zhuǎn)載自微信公眾號(hào)「更了不起的前端」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系更了不起的前端公眾號(hào)。