<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
靜態提升是Vue3編譯優化中的其中一個優化點。所謂的靜態提升,就是指在編譯器編譯的過程中,將一些靜態的節點或屬性提升到渲染函數之外。下面,我們通過一個例子來深入理解什麼是靜態提升。
假設我們有如下模板:
<div> <p>static text</p> <p>{{ title }}</p> </div>
在沒有靜態提升的情況下,它對應的渲染函數是:
function render() { return (openClock(), createClock('div', null, [ createVNode('p', null, 'static text'), createVNode('p', null, ctx.title, 1 /* TEXT */) ])) }
從上面的程式碼中可以看到,在這段虛擬DOM的描述中存在兩個 p 標籤,一個是純靜態的,而另一個擁有動態文字。當響應式資料 title 的值發生變化時,整個渲染函數會重新執行,併產生新的虛擬DOM樹。在這個過程中存在效能開銷的問題。原因是純靜態的虛擬節點在更新時也會被重新建立一次,其實這是沒有必要的。因此我們需要使用靜態提升來解決這個問題。
所謂的 “靜態提升”,就是將一些靜態的節點或屬性提升到渲染函數之外。如下面的程式碼所示:
// 把靜態節點提升到渲染函數之外 const hoist1 = createVNode('p', null, 'text') function render() { return (openBlock(), createBlock('div', null, [ hoist1, // 靜態節點參照 createVNode('p', null, ctx.title, 1 /* TEXT */) ])) }
可以看到,經過靜態提升後,在渲染函數內只會持有對靜態節點的參照。當響應式資料發生變化,並使得渲染函數重新執行時,並不會重新建立靜態的虛擬節點,從而避免了額外的效能開銷。
在模板編譯器將模板編譯為渲染函數的過程中,transform 函數扮演著十分重要的角色。它用來將模板AST轉換為 JavaScript AST。下面,我們來看看 transform 函數做了什麼。
// packages/compiler-core/src/transform.ts // 將 模板AST 轉換為 JavaScript AST export function transform(root: RootNode, options: TransformOptions) { // 建立轉換上下文 const context = createTransformContext(root, options) // 遍歷所有節點,執行轉換 traverseNode(root, context) // 如果編譯選項中開啟了 hoistStatic 選項,則進行靜態提升 if (options.hoistStatic) { hoistStatic(root, context) } // 建立 Block,收集所有動態子節點 if (!options.ssr) { createRootCodegen(root, context) } // finalize meta information // 確定最終的元資訊 root.helpers = [...context.helpers.keys()] root.components = [...context.components] root.directives = [...context.directives] root.imports = context.imports root.hoists = context.hoists root.temps = context.temps root.cached = context.cached if (__COMPAT__) { root.filters = [...context.filters!] } }
從上面的原始碼中可以看到,transform 函數的實現並不複雜。
由於本文主要是介紹靜態提升,因此我們圍繞靜態提升的程式碼繼續往下探索,其餘部分程式碼將在其它文章中介紹。
hoistStatic 函數的原始碼如下所示:
// packages/compiler-core/src/transforms/hoistStatic.ts export function hoistStatic(root: RootNode, context: TransformContext) { walk( root, context, // Root node is unfortunately non-hoistable due to potential parent // fallthrough attributes. // 根節點作為 Block 角色是不能被提升的 isSingleElementRoot(root, root.children[0]) ) }
可以看到,hoistStatic 函數接收兩個引數,第一個引數是根節點,第二個引數是轉換上下文。在該函數中,僅僅只是呼叫了 walk 函數來實現靜態提升。
並且在 walk 函數中呼叫 isSingleElementRoot 函數來告知 walk 函數根節點是不能提升的,原因是根節點作為 Block 角色是不可被提升的。
我們接下來繼續探究 walk 函數的原始碼。
靜態提升的真正實現邏輯在 walk 函數內,其原始碼如下所示:
function walk( node: ParentNode, context: TransformContext, // 轉換上下文物件 doNotHoistNode: boolean = false // 節點是否可以被提升 ) { const { children } = node // 子節點的數量 const originalCount = children.length // 可提升節點的數量 let hoistedCount = 0 for (let i = 0; i < children.length; i++) { const child = children[i] // only plain elements & text calls are eligible for hoisting. // 只有普通元素和文字才能被提升 if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT ) { // 如果節點不能被提升,則將 constantType 賦值為 NOT_CONSTANT 不可被提升的標記 // 否則呼叫 getConstantType 獲取子節點的靜態型別:ConstantTypes 定義了子節點的靜態型別 const constantType = doNotHoistNode ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) // 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT if (constantType > ConstantTypes.NOT_CONSTANT) { // 如果節點可以被提升 if (constantType >= ConstantTypes.CAN_HOIST) { // 則將子節點的 codegenNode 屬性的 patchFlag 標記為 HOISTED ,即可提升 ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) // 提升節點,將節點儲存到 轉換上下文context 的 hoist 陣列中 child.codegenNode = context.hoist(child.codegenNode!) // 提升節點數量自增1 hoistedCount++ continue } } else { // node may contain dynamic children, but its props may be eligible for // hoisting. // 包含動態繫結的節點本身不會被提升,該動態節點上可能存在純靜態的屬性,靜態的屬性可以被提升 const codegenNode = child.codegenNode! if (codegenNode.type === NodeTypes.VNODE_CALL) { // 獲取 patchFlag 修補程式標誌 const flag = getPatchFlag(codegenNode) // 如果不存在 patchFlag 修補程式標誌 或者 patchFlag 是文字型別 // 並且該節點的 props 是可以被提升的 if ( (!flag || flag === PatchFlags.NEED_PATCH || flag === PatchFlags.TEXT) && getGeneratedPropsConstantType(child, context) >= ConstantTypes.CAN_HOIST ) { // 獲取節點的 props,並在轉換上下文物件中執行提升操作, // 將被提升的 props 新增到轉換上下文context 的 hoist 陣列中 const props = getNodeProps(child) if (props) { codegenNode.props = context.hoist(props) } } // 將節點的動態 props 新增到轉換上下文物件中 if (codegenNode.dynamicProps) { codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps) } } } } else if ( // 如果是節點型別是 TEXT_CALL,並且節點可以被提升 child.type === NodeTypes.TEXT_CALL && getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST ) { // 提升節點 child.codegenNode = context.hoist(child.codegenNode) hoistedCount++ } // walk further if (child.type === NodeTypes.ELEMENT) { // 如果子節點的 tagType 是元件,則繼續遍歷子節點 // 以判斷插槽中的情況 const isComponent = child.tagType === ElementTypes.COMPONENT if (isComponent) { context.scopes.vSlot++ } // 執行 walk函數,繼續判斷插槽中的節點及節點屬性是否可以被提升 walk(child, context) if (isComponent) { context.scopes.vSlot-- } } else if (child.type === NodeTypes.FOR) { // Do not hoist v-for single child because it has to be a block // 帶有 v-for 指令的節點是一個 Block // 如果 v-for 的節點中只有一個子節點,則不能被提升 walk(child, context, child.children.length === 1) } else if (child.type === NodeTypes.IF) { // 帶有 v-if 指令的節點是一個 Block for (let i = 0; i < child.branches.length; i++) { // Do not hoist v-if single child because it has to be a block // 如果只有一個分支條件,則不進行提升 walk( child.branches[i], context, child.branches[i].children.length === 1 ) } } } // 將被提升的節點序列化,即轉換成字串 if (hoistedCount && context.transformHoist) { context.transformHoist(children, context, node) } // all children were hoisted - the entire children array is hoistable. if ( hoistedCount && hoistedCount === originalCount && node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT && node.codegenNode && node.codegenNode.type === NodeTypes.VNODE_CALL && isArray(node.codegenNode.children) ) { node.codegenNode.children = context.hoist( createArrayExpression(node.codegenNode.children) ) } }
可以看到,walk 函數的程式碼比較常,下面,我們將分步對其進行解析。
1、首先,我們來看 walk 函數的函數簽名,程式碼如下:
function walk( node: ParentNode, context: TransformContext, // 轉換上下文物件 doNotHoistNode: boolean = false // 節點是否可以被提升 )
從函數簽名中可以知道,walk 函數接收三個引數,第一個引數是一個 node 節點,第二個引數是轉換器的轉換上下文物件 context,第三個引數 doNotHoistNode 是一個布林值,用來判斷傳入節點的子節點是否可以被提升。
2、初始化兩個變數,originalCount:子節點的數量;hoistedCount:可提升節點的數量,該變數將會用於判斷被提升的節點是否可序列化。
const { children } = node // 子節點的數量 const originalCount = children.length // 可提升節點的數量 let hoistedCount = 0
3、我們來看 for 迴圈語句裡的第一個 if 語句分支,這裡處理的是普通元素和靜態文字被提升的情況。
// only plain elements & text calls are eligible for hoisting. // 只有普通元素和文字才能被提升 if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT ) { // 如果節點不能被提升,則將 constantType 賦值為 NOT_CONSTANT 不可被提升的標記 // 否則呼叫 getConstantType 獲取子節點的靜態型別:ConstantTypes 定義了子節點的靜態型別 const constantType = doNotHoistNode ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) // 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT if (constantType > ConstantTypes.NOT_CONSTANT) { // 如果節點可以被提升 if (constantType >= ConstantTypes.CAN_HOIST) { // 則將子節點的 codegenNode 屬性的 patchFlag 標記為 HOISTED ,即可提升 ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) // 提升節點,將節點儲存到 轉換上下文context 的 hoist 陣列中 child.codegenNode = context.hoist(child.codegenNode!) // 提升節點數量自增1 hoistedCount++ continue } } else { // node may contain dynamic children, but its props may be eligible for // hoisting. // 包含動態繫結的節點本身不會被提升,該動態節點上可能存在純靜態的屬性,靜態的屬性可以被提升 const codegenNode = child.codegenNode! if (codegenNode.type === NodeTypes.VNODE_CALL) { // 獲取 patchFlag 修補程式標誌 const flag = getPatchFlag(codegenNode) // 如果不存在 patchFlag 修補程式標誌 或者 patchFlag 是文字型別 // 並且該節點的 props 是可以被提升的 if ( (!flag || flag === PatchFlags.NEED_PATCH || flag === PatchFlags.TEXT) && getGeneratedPropsConstantType(child, context) >= ConstantTypes.CAN_HOIST ) { // 獲取節點的 props,並在轉換上下文物件中執行提升操作, // 將被提升的 props 新增到轉換上下文context 的 hoist 陣列中 const props = getNodeProps(child) if (props) { codegenNode.props = context.hoist(props) } } // 將節點的動態 props 新增到轉換上下文物件中 if (codegenNode.dynamicProps) { codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps) } } }
首先通過外部傳入的 doNotHoistNode 引數來獲取子節點的靜態型別。如果 doNotHoistNode 為 true,則將 constantType 的值設定為 ConstantType 列舉值中的 NOT_CONSTANT,即不可被提升。否則通過 getConstantType 函數獲取子節點的靜態型別。如下面的程式碼所示:
// 如果節點不能被提升,則將 constantType 賦值為 NOT_CONSTANT 不可被提升的標記 // 否則呼叫 getConstantType 獲取子節點的靜態型別:ConstantTypes 定義了子節點的靜態型別 const constantType = doNotHoistNode ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context)
接下來通過判斷 constantType 的列舉值來處理是需要提升靜態節點還是提升動態節點的靜態屬性。
然後執行轉換器上下文中的 hoist 方法,將該節點儲存到轉換上下文context 的 hoist 陣列中,該陣列中儲存的是可被提升的節點。如下面的程式碼所示:
// 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT if (constantType > ConstantTypes.NOT_CONSTANT) { // 如果節點可以被提升 if (constantType >= ConstantTypes.CAN_HOIST) { // 則將子節點的 codegenNode 屬性的 patchFlag 標記為 HOISTED ,即可提升 ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) // 提升節點,將節點儲存到 轉換上下文context 的 hoist 陣列中 child.codegenNode = context.hoist(child.codegenNode!) // 提升節點數量自增1 hoistedCount++ continue } }
如果獲取到的 constantType 列舉值不大於 NOT_CONSTANT,說明該節點包含動態繫結,包含動態繫結的節點上如果存在純靜態的 props,那麼這些靜態的 props 是可以被提升的。
從下面的程式碼中我們可以看到,在提升靜態的 props 時,同樣是執行轉換器上下文中的 hoist 方法,將靜態的props儲存到轉換上下文context 的 hoist 陣列中。
如下面的程式碼所示:
else { // node may contain dynamic children, but its props may be eligible for // hoisting. // 包含動態繫結的節點本身不會被提升,該動態節點上可能存在純靜態的屬性,靜態的屬性可以被提升 const codegenNode = child.codegenNode! if (codegenNode.type === NodeTypes.VNODE_CALL) { // 獲取 patchFlag 修補程式標誌 const flag = getPatchFlag(codegenNode) // 如果不存在 patchFlag 修補程式標誌 或者 patchFlag 是文字型別 // 並且該節點的 props 是可以被提升的 if ( (!flag || flag === PatchFlags.NEED_PATCH || flag === PatchFlags.TEXT) && getGeneratedPropsConstantType(child, context) >= ConstantTypes.CAN_HOIST ) { // 獲取節點的 props,並在轉換上下文物件中執行提升操作, // 將被提升的 props 新增到轉換上下文context 的 hoist 陣列中 const props = getNodeProps(child) if (props) { codegenNode.props = context.hoist(props) } } // 將節點的動態 props 新增到轉換上下文物件中 if (codegenNode.dynamicProps) { codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps) } } }
4、如果節點型別時是 TEXT_CALL 型別並且該節點可以被提升,則同樣執行轉換器上下文中的 hoist 方法,將節點儲存到轉換上下文context 的 hoist 陣列中,如下面的程式碼所示:
else if ( // 如果是節點型別是 TEXT_CALL,並且節點可以被提升 child.type === NodeTypes.TEXT_CALL && getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST ) { // 提升節點 child.codegenNode = context.hoist(child.codegenNode) hoistedCount++ }
5、如果子節點是元件,則遞迴呼叫 walk 函數,繼續遍歷子節點,以判斷插槽中的節點及節點屬性是否可以被提升。如下面的程式碼所示:
if (child.type === NodeTypes.ELEMENT) { // 如果子節點的 tagType 是元件,則繼續遍歷子節點 // 以判斷插槽中的情況 const isComponent = child.tagType === ElementTypes.COMPONENT if (isComponent) { context.scopes.vSlot++ } // 執行 walk函數,繼續判斷插槽中的節點及節點屬性是否可以被提升 walk(child, context) if (isComponent) { context.scopes.vSlot-- } }
6、如果節點上帶有 v-for 指令或 v-if 指令,則遞迴呼叫 walk 函數,繼續判斷子節點是否可以被提升。如果 v-for 指令的節點只有一個子節點,v-if 指令的節點只有一個分支條件,則不進行提升。如下面的程式碼所示:
else if (child.type === NodeTypes.FOR) { // Do not hoist v-for single child because it has to be a block // 帶有 v-for 指令的節點是一個 Block // 如果 v-for 的節點中只有一個子節點,則不能被提升 walk(child, context, child.children.length === 1) } else if (child.type === NodeTypes.IF) { // 帶有 v-if 指令的節點是一個 Block for (let i = 0; i < child.branches.length; i++) { // Do not hoist v-if single child because it has to be a block // 如果只有一個分支條件,則不進行提升 walk( child.branches[i], context, child.branches[i].children.length === 1 ) } }
靜態提升,就是指在編譯器編譯的過程中,將一些靜態的節點或屬性提升到渲染函數之外。它能夠減少更新時建立虛擬 DOM 帶來的效能開銷和記憶體佔用。
對於純靜態的節點和動態節點上的純靜態屬性,則直接執行轉換器上下文中的 hoist 方法,將節點或屬性進行提升。如果節點是元件、節點上帶有 v-for 指令或v-if 指令,則遞迴呼叫 walk 函數判斷子節點是否可以被提升。
以上就是Vue3 原始碼解讀靜態提升詳解的詳細內容,更多關於Vue3 靜態提升的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45