Angular 学習: Ivy コンパイラーのインクリメンタル DOM の簡単な分析

青灯夜游
リリース: 2022-02-23 11:07:13
転載
2892 人が閲覧しました

この記事は、Angular フレームワークについて学習し、Ivy コンパイラーのインクリメンタル DOM について知っていただくことを目的としています。皆様のお役に立てれば幸いです。

Angular 学習: Ivy コンパイラーのインクリメンタル DOM の簡単な分析

「大規模なフロントエンド プロジェクト向け」に設計されたフロントエンド フレームワークとして、Angular には実際に参考にして学ぶ価値のある設計が数多くあります。このシリーズは主に次の用途に使用されます。これらのデザインと機能を研究し、実現原理を研究します。この記事では、Angular のコア機能である Ivy コンパイラーに焦点を当て、そのインクリメンタル DOM 設計を紹介します。 [関連チュートリアルの推奨事項: "angular チュートリアル"]

フロントエンド フレームワークを導入するときに、テンプレート エンジンを導入することがよくあります。テンプレート エンジンのレンダリング プロセスには、Vue/React などのフレームワークで仮想 DOM などの設計が使用されます。

Angular Ivy コンパイラでは、仮想 DOM は使用されませんが、インクリメンタル DOM が使用されます。

増分 DOM

Ivy コンパイラでは、テンプレート コンパイル済み製品は View エンジンとは異なり、個別のコンパイルや増分コンパイルなどの機能をサポートします。

例: <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 と仮想 DOM

仮想 DOM は誰もが理解しているはずです。その中心的な計算プロセスには次のものが含まれます。

  • JavaScript オブジェクトを使用する DOM をシミュレートするツリーを作成し、仮想 DOM ツリーを取得します。

  • ページ データが変更されると、新しい仮想 DOM ツリーが生成され、新旧の仮想 DOM ツリーの差分が比較されます。

  • 差異を実際の DOM ツリーに適用します。

仮想 DOM は、頻繁なページの更新とレンダリングによって引き起こされるパフォーマンスの問題を解決しますが、従来の仮想 DOM には依然として次のようなパフォーマンスのボトルネックがあります。コンポーネントの仮想 DOM ツリー全体をコンポーネント内で走査する必要があります。

    一部のコンポーネントのテンプレート全体に動的ノードが数個しかない場合、これらの走査はパフォーマンスの無駄です
  • 再帰的走査UI レンダリングがブロックされ、ユーザー エクスペリエンスが低下する原因になりやすいです。
  • これらの状況を考慮して、React や Vue などのフレームワークもさらに最適化されています。それぞれツリー diff、コンポーネント diff、要素 diff のアルゴリズムを最適化し、状態更新の計算とレンダリングを制御するタスク スケジューリングを導入します。 Vue 3.0では仮想DOMの更新が従来の全体スコープからツリースコープに変更され、ツリー構造によるアルゴリズムの簡素化とパフォーマンスの向上が図られています。
いずれの場合でも、仮想 DOM の設計には避けられない問題があります。各レンダリング操作では、少なくとも変更されたノードを収容できる十分な大きさの新しい仮想 DOM ツリーが割り当てられ、通常はそれよりも大きくなります。このような設計では、メモリ使用量が増加します。大規模な仮想 DOM ツリーで大量の更新が必要な場合、特にメモリに制約のあるモバイル デバイスでは、パフォーマンスが低下する可能性があります。

インクリメンタル DOM 設計の中心的な考え方は次のとおりです。

新しい (仮想) DOM ツリーを作成するときは、既存のツリーに沿って歩き、変更を見つけます。
  • 変更がない場合、メモリは割り当てられません。
  • #変更がある場合は、既存のツリーを変更します (絶対に必要な場合にのみメモリを割り当てます) )、その差分を物理 DOM に適用します。
  • (仮想) が括弧内に置かれているのは、事前計算されたメタ情報を既存の DOM ノードに混合するときに、仮想 DOM ツリーに依存する代わりに物理 DOM ツリーが使用されるためです。十分。
インクリメンタル DOM には、仮想 DOM ベースのアプローチと比較して 2 つの主な利点があります。

インクリメンタル機能により、レンダリング プロセス中のメモリ割り当てが大幅に削減され、パフォーマンスがより予測可能になります

    テンプレートベースのメソッドに簡単にマッピングできます。制御ステートメントとループは、要素および属性の宣言と自由に組み合わせることができます。
  • インクリメンタル DOM の設計は Google によって提案され、オープン ソース ライブラリも提供しています
  • google/incremental-dom
, DOMツリーの更新を表現・適用するためのライブラリです。 JavaScript を使用すると、データを抽出、反復処理し、HTMLElement および Text ノードを生成する呼び出しに変換できます。

しかし、新しい Ivy エンジンはそれを直接使用せず、独自のバージョンを実装します。

Ivy のインクリメンタル DOM

Ivy エンジンはインクリメンタル DOM の概念に基づいています。仮想 DOM メソッドとの違いは、DOM に対して diff 操作がインクリメンタルに実行されることです (つまり、ノードが 1 回だけ)。 ) 仮想 DOM ツリー上で実行する代わりに。この設計に基づいて、Angular のインクリメンタル DOM とダーティ チェック メカニズムは実際にうまく連携して機能します。

增量 DOM 元素创建

增量 DOM 的 API 的一个独特功能是它分离了标签的打开(elementStart)和关闭(elementEnd),因此它适合作为模板语言的编译目标,这些语言允许(暂时)模板中的 HTML 不平衡(比如在单独的模板中,打开和关闭的标签)和任意创建 HTML 属性的逻辑。

在 Ivy 中,使用elementStartelementEnd创建一个空的 Element 实现如下(在 Ivy 中,elementStartelementEnd的具体实现便是ɵɵ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 元素的过程中,主要依赖于LViewTViewTNode

在 Angular Ivy 中,使用了LViewTView.data来管理和跟踪渲染模板所需要的内部数据。对于TNode,在 Angular 中则是用于在特定类型的所有模板之间共享的特定节点的绑定数据(享元)。

ɵɵelementEnd()则用于标记元素的结尾:

export function ɵɵelementEnd(): void {}
ログイン後にコピー

对于ɵɵelementEnd()的详细实现不过多介绍,基本上主要包括一些对 Class 和样式中@input等指令的处理,循环遍历提供的tNode上的指令、并将要运行的钩子排入队列,元素层次的处理等等。

组件创建与增量 DOM 指令

在增量 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);
}
ログイン後にコピー

可见,在组件编译时,会被编译成一系列的指令,包括constvarsdirectivespipesstyleschangeDetection等等,当然也包括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 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:juejin.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート