您好,登錄后才能下訂單哦!
小編給大家分享一下Vue3模板編譯優化的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
編譯入口
了解過 Vue3 的同學肯定知道 Vue3 引入了新的組合 Api,在組件 mount 階段會調用 setup 方法,之后會判斷 render 方法是否存在,如果不存在會調用 compile 方法將 template 轉化為 render。
// packages/runtime-core/src/renderer.ts const mountComponent = (initialVNode, container) => { const instance = ( initialVNode.component = createComponentInstance( // ...params ) ) // 調用 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) { // ...調用 setup } if (compile && Component.template && !Component.render) { // 如果沒有 render 方法 // 調用 compile 將 template 轉為 render 方法 Component.render = compile(Component.template, {...}) } }
這部分都是 runtime-core 中的代碼,之前的文章有講過 Vue 分為完整版和 runtime 版本。如果使用 vue-loader 處理 .vue 文件,一般都會將 .vue 文件中的 template 直接處理成 render 方法。
// 需要編譯器 Vue.createApp({ template: '<div>{{ hi }}</div>' }) // 不需要 Vue.createApp({ render() { return Vue.h('div', {}, this.hi) } })
完整版與 runtime 版的差異就是,完整版會引入 compile 方法,如果是 vue-cli 生成的項目就會抹去這部分代碼,將 compile 過程都放到打包的階段,以此優化性能。runtime-dom 中提供了 registerRuntimeCompiler 方法用于注入 compile 方法。
主流程
在完整版的 index.js 中,調用了 registerRuntimeCompiler 將 compile 進行注入,接下來我們看看注入的 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 不是字符串 // 則認為是一個 DOM 節點,獲取 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 : '' } // 調用 compile 獲取 render code const { code } = compile( template, options ) // 將 render code 轉化為 function const render = new Function(code)(); // 返回 render 方法的同時,將其放入緩存 return (compileCache[key] = render) } // 注入 compile registerRuntimeCompiler(compileToFunction)
在講 Vue2 模板編譯的時候已經講過,compile 方法主要分為三步,Vue3 的邏輯類似:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
模板編譯,將模板代碼轉化為 AST;
優化 AST,方便后續虛擬 DOM 更新;
生成代碼,將 AST 轉化為可執行的代碼;
// 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,轉化為 ast const ast = baseParse(template, options) // 優化 ast,標記靜態節點 transform(ast, { ...options, nodeTransforms: [ transformIf, transformFor, transformText, transformElement, // ... 省略了部分 transform ], directiveTransforms: { on: transformOn, bind: transformBind, model: transformModel } }) // 將 ast 轉化為可執行代碼 return generate(ast, options) }
計算 PatchFlag
這里大致的邏輯與之前的并沒有多大的差異,主要是 optimize 方法變成了 transform 方法,而且默認會對一些模板語法進行 transform。這些 transform 就是后續虛擬 DOM 優化的關鍵,我們先看看 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 會返回一個退出函數,在處理完所有的子節點后再執行 const onExit = nodeTransforms[i](node, context) if (onExit) { if (isArray(onExit)) { exitFns.push(...onExit) } else { exitFns.push(onExit) } } } traverseChildren(node, context) context.currentNode = node // 執行所以 Transform 的退出函數 let i = exitFns.length while (i--) { exitFns[i]() } }
我們重點看一下 transformElement 的邏輯:
// packages/compiler-core/src/transforms/transformElement.ts export const transformElement: NodeTransform = (node, context) => { // transformElement 沒有執行任何邏輯,而是直接返回了一個退出函數 // 說明 transformElement 需要等所有的子節點處理完后才執行 return function postTransformElement() { const { tag, props } = node let vnodeProps let vnodePatchFlag const vnodeTag = node.tagType === ElementTypes.COMPONENT ? resolveComponentType(node, context) : `"${tag}"` let patchFlag = 0 // 檢測節點屬性 if (props.length > 0) { // 檢測節點屬性的動態部分 const propsBuildResult = buildProps(node, context) vnodeProps = propsBuildResult.props patchFlag = propsBuildResult.patchFlag } // 檢測子節點 if (node.children.length > 0) { if (node.children.length === 1) { const child = node.children[0] // 檢測子節點是否為動態文本 if (!getStaticType(child)) { patchFlag |= PatchFlags.TEXT } } } // 格式化 patchFlag if (patchFlag !== 0) { vnodePatchFlag = String(patchFlag) } node.codegenNode = createVNodeCall( context, vnodeTag, vnodeProps, vnodeChildren, vnodePatchFlag ) } }
buildProps 會對節點的屬性進行一次遍歷,由于內部源碼涉及很多其他的細節,這里的代碼是經過簡化之后的,只保留了 patchFlag 相關的邏輯。
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 的類型:
節點只有一個文本子節點,且該文本包含動態的數據(TEXT = 1)
<p>name: {{name}}</p>
節點包含可變的 class 屬性(CLASS = 1 << 1)
<div :class="{ active: isActive }"></div>
節點包含可變的 style 屬性(STYLE = 1 << 2)
<div :style="{ color: color }"></div>
可以看到 PatchFlags 都是數字 1 經過 左移操作符 計算得到的。
export const enum PatchFlags { TEXT = 1, // 1, 二進制 0000 0001 CLASS = 1 << 1, // 2, 二進制 0000 0010 STYLE = 1 << 2, // 4, 二進制 0000 0100 PROPS = 1 << 3, // 8, 二進制 0000 1000 ... }
從上面的代碼能看出來,patchFlag 的初始值為 0,每次對 patchFlag 都是執行 | (或)操作。如果當前節點是一個只有動態文本子節點且同時具有動態 style 屬性,最后得到的 patchFlag 為 5(二進制:0000 0101)。
<p :style="{ color: color }">name: {{name}}</p>
我們將上面的代碼放到 Vue3 中運行:
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 優化
Vue3 在虛擬 DOM Diff 時,會取出 patchFlag 和需要進行的 diff 類型進行 &(與)操作,如果結果為 true 才進入對應的 diff。
patchFlag 判斷
還是拿之前的模板舉例:
<p :style="{ color: color }">name: {{name}}</p>
如果此時的 name 發生了修改,p 節點進入了 diff 階段,此時會將判斷 patchFlag & PatchFlags.TEXT ,這個時候結果為真,表明 p 節點存在文本修改的情況。
patchFlag
patchFlag = 5 patchFlag & PatchFlags.TEXT // 或運算:只有對應的兩個二進位都為1時,結果位才為1。 // 0000 0101 // 0000 0001 // ------------ // 0000 0001 => 十進制 1
if (patchFlag & PatchFlags.TEXT) { if (oldNode.children !== newNode.children) { // 修改文本 hostSetElementText(el, newNode.children) } }
但是進行 patchFlag & PatchFlags.CLASS 判斷時,由于節點并沒有動態 Class,返回值為 0,所以就不會對該節點的 class 屬性進行 diff,以此來優化性能。
patchFlag
patchFlag = 5 patchFlag & PatchFlags.CLASS // 或運算:只有對應的兩個二進位都為1時,結果位才為1。 // 0000 0101 // 0000 0010 // ------------ // 0000 0000 => 十進制 0
以上是“Vue3模板編譯優化的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。