溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Ivy編譯器中增量DOM的示例分析

發(fā)布時(shí)間:2022-02-23 11:50:03 來(lái)源:億速云 閱讀:164 作者:小新 欄目:web開(kāi)發(fā)

這篇文章主要為大家展示了“Ivy編譯器中增量DOM的示例分析”,內(nèi)容簡(jiǎn)而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“Ivy編譯器中增量DOM的示例分析”這篇文章吧。

作為“為大型前端項(xiàng)目”而設(shè)計(jì)的前端框架,Angular 其實(shí)有許多值得參考和學(xué)習(xí)的設(shè)計(jì),本系列主要用于研究這些設(shè)計(jì)和功能的實(shí)現(xiàn)原理。本文圍繞 Angular 的核心功能 Ivy 編譯器,介紹其中的增量 DOM 設(shè)計(jì)。

在介紹前端框架的時(shí)候,我常常會(huì)介紹到模板引擎。對(duì)于模板引擎的渲染過(guò)程,像 Vue/React 這樣的框架里,使用了虛擬 DOM 這樣的設(shè)計(jì)。

在 Angular Ivy 編譯器中,并沒(méi)有使用虛擬 DOM,而且使用了增量 DOM。

增量 DOM

在 Ivy 編譯器里,模板編譯后的產(chǎn)物與 View Engine 不一樣了,這是為了支持單獨(dú)編譯、增量編譯等能力。

比如,<span>My name is {{name}}</span>這句模板代碼,在 Ivy 編譯器中編譯后的代碼大概長(zhǎng)這個(gè)樣子:

// 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 的設(shè)計(jì)。

增量 DOM vs 虛擬 DOM

虛擬 DOM 想必大家都已經(jīng)有所了解,它的核心計(jì)算過(guò)程包括:

  • 用 JavaScript 對(duì)象模擬 DOM 樹(shù),得到一棵虛擬 DOM 樹(shù)。

  • 當(dāng)頁(yè)面數(shù)據(jù)變更時(shí),生成新的虛擬 DOM 樹(shù),比較新舊兩棵虛擬 DOM 樹(shù)的差異。

  • 把差異應(yīng)用到真正的 DOM 樹(shù)上。

雖然虛擬 DOM 解決了頁(yè)面被頻繁更新和渲染帶來(lái)的性能問(wèn)題,但傳統(tǒng)虛擬 DOM 依然有以下性能瓶頸:

  • 在單個(gè)組件內(nèi)部依然需要遍歷該組件的整個(gè)虛擬 DOM 樹(shù)

  • 在一些組件整個(gè)模版內(nèi)只有少量動(dòng)態(tài)節(jié)點(diǎn)的情況下,這些遍歷都是性能的浪費(fèi)

  • 遞歸遍歷和更新邏輯容易導(dǎo)致 UI 渲染被阻塞,用戶(hù)體驗(yàn)下降

針對(duì)這些情況,React 和 Vue 等框架也有更多的優(yōu)化,比如 React 中分別對(duì) tree diff、component diff 以及 element diff 進(jìn)行了算法優(yōu)化,同時(shí)引入了任務(wù)調(diào)度來(lái)控制狀態(tài)更新的計(jì)算和渲染。在 Vue 3.0 中,則將虛擬 DOM 的更新從以前的整體作用域調(diào)整為樹(shù)狀作用域,樹(shù)狀的結(jié)構(gòu)會(huì)帶來(lái)算法的簡(jiǎn)化以及性能的提升。

而不管怎樣,虛擬 DOM 的設(shè)計(jì)中存在一個(gè)無(wú)法避免的問(wèn)題:每個(gè)渲染操作分配一個(gè)新的虛擬 DOM 樹(shù),該樹(shù)至少大到足以容納發(fā)生變化的節(jié)點(diǎn),并且通常更大一些,這樣的設(shè)計(jì)會(huì)導(dǎo)致更多的一些內(nèi)存占用。當(dāng)大型虛擬 DOM 樹(shù)需要大量更新時(shí),尤其是在內(nèi)存受限的移動(dòng)設(shè)備上,性能可能會(huì)受到影響。

增量 DOM 的設(shè)計(jì)核心思想是:

  • 在創(chuàng)建新的(虛擬)DOM 樹(shù)時(shí),沿著現(xiàn)有的樹(shù)走,并在進(jìn)行時(shí)找出更改。

  • 如果沒(méi)有變化,則不分配內(nèi)存;

  • 如果有,改變現(xiàn)有樹(shù)(僅在絕對(duì)必要時(shí)分配內(nèi)存)并將差異應(yīng)用到物理 DOM。

這里將(虛擬)放在括號(hào)中是因?yàn)椋?dāng)將預(yù)先計(jì)算的元信息混合到現(xiàn)有 DOM 節(jié)點(diǎn)中時(shí),使用物理 DOM 樹(shù)而不是依賴(lài)虛擬 DOM 樹(shù)實(shí)際上已經(jīng)足夠快了。

與基于虛擬 DOM 的方法相比,增量 DOM 有兩個(gè)主要優(yōu)勢(shì):

  • 增量特性允許在渲染過(guò)程中顯著減少內(nèi)存分配,從而實(shí)現(xiàn)更可預(yù)測(cè)的性能

  • 它很容易映射到基于模板的方法??刂普Z(yǔ)句和循環(huán)可以與元素和屬性聲明自由混合

增量 DOM 的設(shè)計(jì)由 Google 提出,同時(shí)他們也提供了一個(gè)開(kāi)源庫(kù) google/incremental-dom,它是一個(gè)用于表達(dá)和應(yīng)用 DOM 樹(shù)更新的庫(kù)。JavaScript 可用于提取、迭代數(shù)據(jù)并將其轉(zhuǎn)換為生成 HTMLElements 和 Text 節(jié)點(diǎn)的調(diào)用。

但新的 Ivy 引擎沒(méi)有直接使用它,而是實(shí)現(xiàn)了自己的版本。

Ivy 中的增量 DOM

Ivy 引擎基于增量 DOM 的概念,它與虛擬 DOM 方法的不同之處在于,diff 操作是針對(duì) DOM 增量執(zhí)行的(即一次一個(gè)節(jié)點(diǎn)),而不是在虛擬 DOM 樹(shù)上執(zhí)行?;谶@樣的設(shè)計(jì),增量 DOM 與 Angular 中的臟檢查機(jī)制其實(shí)能很好地搭配。

增量 DOM 元素創(chuàng)建

增量 DOM 的 API 的一個(gè)獨(dú)特功能是它分離了標(biāo)簽的打開(kāi)(elementStart)和關(guān)閉(elementEnd),因此它適合作為模板語(yǔ)言的編譯目標(biāo),這些語(yǔ)言允許(暫時(shí))模板中的 HTML 不平衡(比如在單獨(dú)的模板中,打開(kāi)和關(guān)閉的標(biāo)簽)和任意創(chuàng)建 HTML 屬性的邏輯。

在 Ivy 中,使用elementStartelementEnd創(chuàng)建一個(gè)空的 Element 實(shí)現(xiàn)如下(在 Ivy 中,elementStartelementEnd的具體實(shí)現(xiàn)便是??elementStart??elementEnd):

export function ??element(
  index: number,
  name: string,
  attrsIndex?: number | null,
  localRefsIndex?: number
): void {
  ??elementStart(index, name, attrsIndex, localRefsIndex);
  ??elementEnd();
}

其中,??elementStart用于創(chuàng)建 DOM 元素,該指令后面必須跟有??elementEnd()調(diào)用。

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];
  // 此處創(chuàng)建 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;
  // 通過(guò)推斷的渲染器,將所有屬性值分配給提供的元素
  if (mergedAttrs !== null) {
    setUpAttributes(renderer, native, mergedAttrs);
  }
  // 將 className 寫(xiě)入 RElement
  const classes = tNode.classes;
  if (classes !== null) {
    writeDirectClass(renderer, native, classes);
  }
  // 將 cssText 寫(xiě)入 RElement
  const styles = tNode.styles;
  if (styles !== null) {
    writeDirectStyle(renderer, native, styles);
  }

  if ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) {
    // 添加子元素
    appendChild(tView, lView, native, tNode);
  }

  // 組件或模板容器的任何直接子級(jí),必須預(yù)先使用組件視圖數(shù)據(jù)進(jìn)行猴子修補(bǔ)
  // 以便稍后可以使用任何元素發(fā)現(xiàn)實(shí)用程序方法檢查元素
  if (getElementDepthCount() === 0) {
    attachPatchData(native, lView);
  }
  increaseElementDepthCount();

  // 對(duì)指令 Host 的處理
  if (isDirectiveHost(tNode)) {
    createDirectivesInstances(tView, lView, tNode);
    executeContentQueries(tView, tNode, lView);
  }
  // 獲取本地名稱(chēng)和索引的列表,并將解析的本地變量值按加載到模板中的相同順序推送到 LView
  if (localRefsIndex !== null) {
    saveResolvedLocalsInData(lView, tNode);
  }
}

可以看到,在??elementStart創(chuàng)建 DOM 元素的過(guò)程中,主要依賴(lài)于LView、TViewTNode。

在 Angular Ivy 中,使用了LViewTView.data來(lái)管理和跟蹤渲染模板所需要的內(nèi)部數(shù)據(jù)。對(duì)于TNode,在 Angular 中則是用于在特定類(lèi)型的所有模板之間共享的特定節(jié)點(diǎn)的綁定數(shù)據(jù)(享元)。

??elementEnd()則用于標(biāo)記元素的結(jié)尾:

export function ??elementEnd(): void {}

對(duì)于??elementEnd()的詳細(xì)實(shí)現(xiàn)不過(guò)多介紹,基本上主要包括一些對(duì) Class 和樣式中@input等指令的處理,循環(huán)遍歷提供的tNode上的指令、并將要運(yùn)行的鉤子排入隊(duì)列,元素層次的處理等等。

組件創(chuàng)建與增量 DOM 指令

在增量 DOM 中,每個(gè)組件都被編譯成一系列指令。這些指令創(chuàng)建 DOM 樹(shù)并在數(shù)據(jù)更改時(shí)就地更新它們。

Ivy 在運(yùn)行時(shí)編譯一個(gè)組件的過(guò)程中,會(huì)創(chuàng)建模板解析相關(guān)指令:

export function compileComponentFromMetadata(
  meta: R3ComponentMetadata,
  constantPool: ConstantPool,
  bindingParser: BindingParser
): R3ComponentDef {
  // 其他暫時(shí)省略

  // 創(chuàng)建一個(gè) TemplateDefinitionBuilder,用于創(chuàng)建模板相關(guān)的處理
  const templateBuilder = new TemplateDefinitionBuilder(
      constantPool, BindingScope.createRootScope(), 0, templateTypeName, null, null, templateName,
      directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
      meta.relativeContextFilePath, meta.i18nUseExternalIds);

  // 創(chuàng)建模板解析相關(guān)指令,包括:
  // 第一輪:創(chuàng)建模式,包括所有創(chuàng)建模式指令(例如解析偵聽(tīng)器中的綁定)
  // 第二輪:綁定和刷新模式,包括所有更新模式指令(例如解析屬性或文本綁定)
  const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []);

  // 提供這個(gè)以便動(dòng)態(tài)生成的組件在實(shí)例化時(shí),知道哪些投影內(nèi)容塊要傳遞給組件
  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 轉(zhuǎn)換為函數(shù)
    if (prepareStatements.length > 0) {
      constsExpr = o.fn([], [...prepareStatements, new o.ReturnStatement(constsExpr)]);
    }
    definitionMap.set("consts", constsExpr);
  }

  // 生成 ComponentDef 的 template 部分
  definitionMap.set("template", templateFunctionExpression);
}

可見(jiàn),在組件編譯時(shí),會(huì)被編譯成一系列的指令,包括const、vars、directives、pipes、styles、changeDetection等等,當(dāng)然也包括template模板里的相關(guān)指令。最終生成的這些指令,會(huì)體現(xiàn)在編譯后的組件中,比如之前文章中提到的這樣一個(gè)Component文件:

import { Component, Input } from "@angular/core";

@Component({
  selector: "greet",
  template: "<div> Hello, {{name}}! </div>",
})
export class GreetComponent {
  @Input() name: string;
}

經(jīng)ngtsc編譯后,產(chǎn)物包括該組件的.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 相關(guān)的指令。在實(shí)際創(chuàng)建組件的時(shí)候,其template模板函數(shù)也會(huì)被執(zhí)行,相關(guān)的指令也會(huì)被執(zhí)行。

正因?yàn)樵?Ivy 中,是由組件來(lái)引用著相關(guān)的模板指令。如果組件不引用某個(gè)指令,則我們的 Angular 中永遠(yuǎn)不會(huì)使用到它。因?yàn)榻M件編譯的過(guò)程發(fā)生在編譯過(guò)程中,因此我們可以根據(jù)引用到指令,來(lái)排除未引用的指令,從而可以在 Tree-shaking 過(guò)程中,將未使用的指令從包中移除,這便是增量 DOM 可樹(shù)搖的原因。

以上是“Ivy編譯器中增量DOM的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

dom
AI