這篇文章進行來學習Angular框架,帶大家了解一下Ivy編譯器中的增量DOM,希望對大家有幫助!
作為「為大型前端專案」而設計的前端框架,Angular 其實有許多值得參考和學習的設計,本系列主要用於研究這些設計和功能的實現原理。本文圍繞著 Angular 的核心功能 Ivy 編譯器,介紹其中的增量 DOM 設計。 【相關教學推薦:《angular教學》】
在介紹前端框架的時候,我常常會介紹到模板引擎。對於模板引擎的渲染過程,像 Vue/React 這樣的框架裡,使用了虛擬 DOM 這樣的設計。
在 Angular Ivy 編譯器中,並沒有使用虛擬 DOM,而且使用了增量 DOM。
在 Ivy 編譯器裡,模板編譯後的產物與 View Engine 不一樣了,這是為了支援單獨編譯、增量編譯等能力。
例如,<span>My name is {{name}}</span>
這句模板程式碼,在Ivy 編譯器中編譯後的程式碼大概長這個樣子:
// create mode if (rf & RenderFlags.Create) { elementStart(0, "span"); text(1); elementEnd(); } // update mode if (rf & RenderFlags.Update) { textBinding(1, interpolation1("My name is", ctx.name)); }
可以看到,相較於View Engine 中的elementDef(0,null,null,1,'span',...),
,elementStart()
、elementEnd()
這些API 顯得更加清爽,它們使用的便是增量DOM 的設計。
虛擬DOM 想必大家都已經有所了解,它的核心計算過程包括:
用JavaScript 對象模擬DOM 樹,得到一棵虛擬DOM 樹。
當頁面資料變更時,產生新的虛擬 DOM 樹,比較新舊兩棵虛擬 DOM 樹的差異。
把差異應用到真正的 DOM 樹上。
雖然虛擬DOM 解決了頁面被頻繁更新和渲染帶來的效能問題,但傳統虛擬DOM 依然有以下效能瓶頸:
針對這些情況,React 和Vue 等框架也有更多的最佳化,例如React 中分別對tree diff、component diff 以及element diff 進行了演算法優化,同時引入了任務調度來控制狀態更新的計算和渲染。在 Vue 3.0 中,則將虛擬 DOM 的更新從先前的整體作用域調整為樹狀作用域,樹狀的結構會帶來演算法的簡化以及效能的提升。
而不管怎樣,虛擬DOM 的設計中存在一個無法避免的問題:每個渲染操作分配一個新的虛擬DOM 樹,該樹至少大到足以容納發生變化的節點,並且通常更大一些,這樣的設計會導致更多的一些記憶體佔用。當大型虛擬 DOM 樹需要大量更新時,尤其是在記憶體受限的行動裝置上,效能可能會受到影響。
增量DOM 的設計核心思想是:
在創建新的(虛擬)DOM 樹時,沿著現有的樹走,並在進行時找出更改。
如果沒有變化,則不分配記憶體;
#如果有,改變現有樹(僅在絕對必要時分配記憶體)並將差異應用到物理DOM。
這裡將(虛擬)放在括號中是因為,當將預先計算的元資訊混合到現有DOM 節點中時,使用實體DOM 樹而不是依賴虛擬DOM 樹實際上已經夠快了。
與基於虛擬DOM 的方法相比,增量DOM 有兩個主要優勢:
增量DOM 的設計由Google 提出,同時他們也提供了一個開源庫google/incremental-dom#,它是一個用於表達和應用DOM 樹更新的函式庫。 JavaScript 可用於擷取、迭代資料並將其轉換為產生 HTMLElements 和 Text 節點的呼叫。
但新的 Ivy 引擎沒有直接使用它,而是實作了自己的版本。
Ivy 引擎是基於增量DOM 的概念,它與虛擬DOM 方法的不同之處在於,diff 操作是針對DOM 增量執行的(即一次一個節點),而不是在虛擬DOM 樹上執行。基於這樣的設計,增量 DOM 與 Angular 中的髒檢查機制其實也能很好地搭配。
增量 DOM 的 API 的一个独特功能是它分离了标签的打开(elementStart
)和关闭(elementEnd
),因此它适合作为模板语言的编译目标,这些语言允许(暂时)模板中的 HTML 不平衡(比如在单独的模板中,打开和关闭的标签)和任意创建 HTML 属性的逻辑。
在 Ivy 中,使用elementStart
和elementEnd
创建一个空的 Element 实现如下(在 Ivy 中,elementStart
和elementEnd
的具体实现便是ɵɵelementStart
和ɵɵelementEnd
):
export function ɵɵelement( index: number, name: string, attrsIndex?: number | null, localRefsIndex?: number ): void { ɵɵelementStart(index, name, attrsIndex, localRefsIndex); ɵɵelementEnd(); }
其中,ɵɵelementStart
用于创建 DOM 元素,该指令后面必须跟有ɵɵelementEnd()
调用。
export function ɵɵelementStart( index: number, name: string, attrsIndex?: number | null, localRefsIndex?: number ): void { const lView = getLView(); const tView = getTView(); const adjustedIndex = HEADER_OFFSET + index; const renderer = lView[RENDERER]; // 此处创建 DOM 元素 const native = (lView[adjustedIndex] = createElementNode( renderer, name, getNamespace() )); // 获取 TNode // 在第一次模板传递中需要收集匹配 const tNode = tView.firstCreatePass ? elementStartFirstCreatePass( adjustedIndex, tView, lView, native, name, attrsIndex, localRefsIndex) : tView.data[adjustedIndex] as TElementNode; setCurrentTNode(tNode, true); const mergedAttrs = tNode.mergedAttrs; // 通过推断的渲染器,将所有属性值分配给提供的元素 if (mergedAttrs !== null) { setUpAttributes(renderer, native, mergedAttrs); } // 将 className 写入 RElement const classes = tNode.classes; if (classes !== null) { writeDirectClass(renderer, native, classes); } // 将 cssText 写入 RElement const styles = tNode.styles; if (styles !== null) { writeDirectStyle(renderer, native, styles); } if ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) { // 添加子元素 appendChild(tView, lView, native, tNode); } // 组件或模板容器的任何直接子级,必须预先使用组件视图数据进行猴子修补 // 以便稍后可以使用任何元素发现实用程序方法检查元素 if (getElementDepthCount() === 0) { attachPatchData(native, lView); } increaseElementDepthCount(); // 对指令 Host 的处理 if (isDirectiveHost(tNode)) { createDirectivesInstances(tView, lView, tNode); executeContentQueries(tView, tNode, lView); } // 获取本地名称和索引的列表,并将解析的本地变量值按加载到模板中的相同顺序推送到 LView if (localRefsIndex !== null) { saveResolvedLocalsInData(lView, tNode); } }
可以看到,在ɵɵelementStart
创建 DOM 元素的过程中,主要依赖于LView
、TView
和TNode
。
在 Angular Ivy 中,使用了LView
和TView.data
来管理和跟踪渲染模板所需要的内部数据。对于TNode
,在 Angular 中则是用于在特定类型的所有模板之间共享的特定节点的绑定数据(享元)。
ɵɵelementEnd()
则用于标记元素的结尾:
export function ɵɵelementEnd(): void {}
对于ɵɵelementEnd()
的详细实现不过多介绍,基本上主要包括一些对 Class 和样式中@input
等指令的处理,循环遍历提供的tNode
上的指令、并将要运行的钩子排入队列,元素层次的处理等等。
在增量 DOM 中,每个组件都被编译成一系列指令。这些指令创建 DOM 树并在数据更改时就地更新它们。
Ivy 在运行时编译一个组件的过程中,会创建模板解析相关指令:
export function compileComponentFromMetadata( meta: R3ComponentMetadata, constantPool: ConstantPool, bindingParser: BindingParser ): R3ComponentDef { // 其他暂时省略 // 创建一个 TemplateDefinitionBuilder,用于创建模板相关的处理 const templateBuilder = new TemplateDefinitionBuilder( constantPool, BindingScope.createRootScope(), 0, templateTypeName, null, null, templateName, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML, meta.relativeContextFilePath, meta.i18nUseExternalIds); // 创建模板解析相关指令,包括: // 第一轮:创建模式,包括所有创建模式指令(例如解析侦听器中的绑定) // 第二轮:绑定和刷新模式,包括所有更新模式指令(例如解析属性或文本绑定) const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []); // 提供这个以便动态生成的组件在实例化时,知道哪些投影内容块要传递给组件 const ngContentSelectors = templateBuilder.getNgContentSelectors(); if (ngContentSelectors) { definitionMap.set("ngContentSelectors", ngContentSelectors); } // 生成 ComponentDef 的 consts 部分 const { constExpressions, prepareStatements } = templateBuilder.getConsts(); if (constExpressions.length > 0) { let constsExpr: o.LiteralArrayExpr|o.FunctionExpr = o.literalArr(constExpressions); // 将 consts 转换为函数 if (prepareStatements.length > 0) { constsExpr = o.fn([], [...prepareStatements, new o.ReturnStatement(constsExpr)]); } definitionMap.set("consts", constsExpr); } // 生成 ComponentDef 的 template 部分 definitionMap.set("template", templateFunctionExpression); }
可见,在组件编译时,会被编译成一系列的指令,包括const
、vars
、directives
、pipes
、styles
、changeDetection
等等,当然也包括template
模板里的相关指令。最终生成的这些指令,会体现在编译后的组件中,比如之前文章中提到的这样一个Component
文件:
import { Component, Input } from "@angular/core"; @Component({ selector: "greet", template: "<div> Hello, {{name}}! </div>", }) export class GreetComponent { @Input() name: string; }
经ngtsc
编译后,产物包括该组件的.js
文件:
const i0 = require("@angular/core"); class GreetComponent {} GreetComponent.ɵcmp = i0.ɵɵdefineComponent({ type: GreetComponent, tag: "greet", factory: () => new GreetComponent(), template: function (rf, ctx) { if (rf & RenderFlags.Create) { i0.ɵɵelementStart(0, "div"); i0.ɵɵtext(1); i0.ɵɵelementEnd(); } if (rf & RenderFlags.Update) { i0.ɵɵadvance(1); i0.ɵɵtextInterpolate1("Hello ", ctx.name, "!"); } }, });
其中,elementStart()
、text()
、elementEnd()
、advance()
、textInterpolate1()
这些都是增量 DOM 相关的指令。在实际创建组件的时候,其template
模板函数也会被执行,相关的指令也会被执行。
正因为在 Ivy 中,是由组件来引用着相关的模板指令。如果组件不引用某个指令,则我们的 Angular 中永远不会使用到它。因为组件编译的过程发生在编译过程中,因此我们可以根据引用到指令,来排除未引用的指令,从而可以在 Tree-shaking 过程中,将未使用的指令从包中移除,这便是增量 DOM 可树摇的原因。
现在,我们已经知道在 Ivy 中,是通过编译器将模板编译为template
渲染函数,其中会将对模板的解析编译成增量 DOM 相关的指令。其中,在elementStart()
执行时,我们可以看到会通过createElementNode()
方法来创建 DOM。实际上,增量 DOM 的设计远不止只是创建 DOM,还包括变化检测等各种能力,关于具体的渲染过程,我们会在下一讲中进行介绍。
更多编程相关知识,请访问:编程教学!!
以上是Angular學習之淺析Ivy編譯器中的增量DOM的詳細內容。更多資訊請關注PHP中文網其他相關文章!