> 웹 프론트엔드 > JS 튜토리얼 > 각도 학습: Ivy 컴파일러의 증분 DOM에 대한 간략한 분석

각도 학습: Ivy 컴파일러의 증분 DOM에 대한 간략한 분석

青灯夜游
풀어 주다: 2022-02-23 11:07:13
앞으로
2944명이 탐색했습니다.

이 기사는 Angular 프레임워크를 배우고 Ivy 컴파일러의 증분 DOM을 안내하는 내용입니다. 도움이 되기를 바랍니다.

각도 학습: Ivy 컴파일러의 증분 DOM에 대한 간략한 분석

"대규모 프런트엔드 프로젝트용"으로 설계된 프런트엔드 프레임워크로서 Angular에는 실제로 참고하고 학습할 만한 디자인이 많이 있습니다. 이 시리즈는 주로 이러한 디자인과 기능의 구현 원리를 연구하는 데 사용됩니다. 이 글에서는 Angular의 핵심 기능인 Ivy 컴파일러에 초점을 맞추고 증분 DOM 설계를 소개합니다. [관련 추천 튜토리얼: "angular Tutorial"]

저는 프론트엔드 프레임워크를 소개할 때 템플릿 엔진을 소개하는 경우가 많습니다. 템플릿 엔진의 렌더링 프로세스를 위해 Vue/React와 같은 프레임워크는 가상 DOM과 같은 디자인을 사용합니다.

Angular Ivy 컴파일러에서는 가상 DOM을 사용하지 않고 증분 DOM을 사용합니다.

Incremental DOM

아이비 컴파일러에서는 템플릿 컴파일 제품이 View Engine과 다릅니다. 이는 개별 컴파일, 증분 컴파일 등의 기능을 지원하기 위한 것입니다.

예를 들어 템플릿 코드 <span>My name is {{name}}</span>인 경우 Ivy 컴파일러에서 컴파일된 코드는 다음과 같습니다. <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,&#39;span&#39;,...),elementStart()elementEnd()

export function ɵɵelement(
  index: number,
  name: string,
  attrsIndex?: number | null,
  localRefsIndex?: number
): void {
  ɵɵelementStart(index, name, attrsIndex, localRefsIndex);
  ɵɵelementEnd();
}
로그인 후 복사
로그인 후 복사

View Engine의 elementDef(0,null,null,1,'span',...),, elementStart(), < code>와 비교하여 볼 수 있습니다. elementEnd()이러한 API는 더 깔끔하게 보이며 증분 DOM 디자인을 사용합니다.

증분 DOM과 가상 DOM

가상 DOM의 핵심 계산 프로세스는 다음과 같습니다.
  • JavaScript 개체를 사용하여 DOM 트리를 시뮬레이션하고 가상 DOM 트리를 얻습니다.
  • 페이지 데이터가 변경되면 새로운 가상 DOM 트리가 생성되고 이전 가상 DOM 트리와 새 가상 DOM 트리의 차이점을 비교합니다.
  • 차이점을 실제 DOM 트리에 적용합니다.

Virtual DOM은 빈번한 페이지 업데이트 및 렌더링으로 인한 성능 문제를 해결하지만 기존 Virtual DOM에는 여전히 다음과 같은 성능 병목 현상이 있습니다.
  • 단일 구성 요소에서는 구성 요소의 전체 가상 DOM 트리를 여전히 순회해야 합니다.
  • 일부 구성 요소가 전체 템플릿에서 적은 수의 동적 노드만 포함하는 경우 이러한 순회는 성능 낭비입니다.
  • 재귀 순회 및 업데이트 논리로 인해 UI 렌더링이 쉽게 차단되고 사용자 경험이 저하될 수 있습니다

In 이러한 상황에 대한 대응으로 React 및 Vue와 같은 프레임워크에는 트리 차이, 구성 요소 차이 및 요소 차이에 대한 알고리즘 최적화와 같은 더 많은 최적화가 있으며 상태 업데이트의 계산 및 렌더링을 제어하기 위해 작업 스케줄링이 도입되었습니다. Vue 3.0에서는 가상 DOM의 업데이트가 기존 전체 범위에서 트리 범위로 조정되어 알고리즘이 단순화되고 성능이 향상됩니다.

어쨌든 가상 DOM 설계에는 피할 수 없는 문제가 있습니다. 각 렌더링 작업은 최소한 변경된 노드를 수용할 수 있을 만큼 크며 일반적으로 다음과 같이 더 큰 새로운 가상 DOM 트리를 할당합니다. 더 큰 메모리 공간에서. 대규모 가상 DOM 트리에 대규모 업데이트가 필요한 경우, 특히 메모리가 제한된 모바일 장치에서 성능이 저하될 수 있습니다.

증분 DOM의 핵심 디자인 아이디어는 다음과 같습니다.
  • 새 (가상) DOM 트리를 만들 때 기존 트리를 따라 이동하면서 변경 사항을 식별합니다.
  • 변경 사항이 없으면 메모리가 할당되지 않습니다.
  • 있는 경우 기존 트리를 변경하고(꼭 필요한 경우에만 메모리 할당) 차이점을 물리적 DOM에 적용합니다.

미리 계산된 메타 정보를 기존 DOM 노드에 혼합할 때 가상 DOM 트리에 의존하는 대신 물리적 DOM 트리를 사용하는 것이 실제로 충분히 빠르기 때문에 여기에 (virtual)을 괄호 안에 넣었습니다.

증분 DOM은 가상 DOM 기반 접근 방식에 비해 두 가지 주요 장점이 있습니다.
  • 증분 기능을 사용하면 렌더링 중에 메모리 할당이 크게 줄어들어 결과적으로 더 예측 가능한 성능을 얻을 수 있습니다.
  • 템플릿 기반 접근 방식에 쉽게 매핑할 수 있습니다. 제어문 및 루프는 요소 및 속성 선언과 자유롭게 혼합될 수 있습니다.

증분형 DOM의 설계는 Google에서 제안했으며 표현 및 적용을 위한 라이브러리인 오픈 소스 라이브러리 google/incremental-dom

도 제공합니다. DOM 트리 업데이트된 라이브러리. JavaScript를 사용하면 데이터를 추출, 반복 및 HTMLElements 및 Text 노드를 생성하는 호출로 변환할 수 있습니다.

하지만 새로운 Ivy 엔진은 이를 직접적으로 사용하지 않고 자체 버전을 구현합니다.

Ivy 엔진의 Incremental DOM은 Incremental DOM 개념을 기반으로 합니다. 가상 DOM 방식과의 차이점은 diff 작업이 DOM에서 점진적으로 수행된다는 것입니다(즉, 한 번에 하나의 노드). 가상 DOM. DOM 트리에서 실행됩니다. 이 디자인을 기반으로 증분 DOM과 Angular의 더티 검사 메커니즘이 실제로 잘 작동합니다.

增量 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,还包括变化检测等各种能力,关于具体的渲染过程,我们会在下一讲中进行介绍。

更多编程相关知识,请访问:编程教学!!

위 내용은 각도 학습: Ivy 컴파일러의 증분 DOM에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿