NG Incremental DOM

Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.

虚拟 DOM 的工作方式

虚拟 DOM 的主要概念是在内存中保留 UI 的虚拟表示,并使用协调(reconciliation)过程将其与真实 DOM 同步。

该过程包括三个主要步骤

  • 当用户 UI 发生变化时,将整个用户 UI 渲染到虚拟 DOM 中

  • 计算之前虚拟 DOM 和当前虚拟 DOM 表示形式之间的差异

  • 根据变化差异更新真实 DOM

增量 DOM 的工作方式

增量 DOM 通过使用真实 DOM 来定位代码更改,带来了一种比虚拟 DOM 更简单的方法。

因此,内存中不会有任何真实 DOM 的虚拟表示来计算差异,真实 DOM 仅用于与新 DOM 树进行差异比较。

增量 DOM 概念背后的主要思想是

将每个组件编译成一组指令。然后,这些指令用于创建 DOM 树并对其进行更改

  • 示例

    @Component({
      selector: 'todos-cmp',
      template: `
        <div *ngFor="let t of todos|async">
            {
         
         {t.description}}
        </div>
      `
    })
    class TodosComponent {
      todos: Observable<Todo[]> = this.store.pipe(select('todos'));
      constructor(private store: Store<AppState>) {}
    }
    
    var TodosComponent = /** @class */ (function () {
      function TodosComponent(store) {
        this.store = store;
        this.todos = this.store.pipe(select('todos'));
      }
    
      TodosComponent.ngComponentDef = defineComponent({
        type: TodosComponent,
        selectors: [["todos-cmp"]],
        factory: function TodosComponent_Factory(t) {
          return new (t || TodosComponent)(directiveInject(Store));
        },
        consts: 2,
        vars: 3,
        template: function TodosComponent_Template(rf, ctx) {
          if (rf & 1) { // create dom
            pipe(1, "async");
            template(0, TodosComponent_div_Template_0, 2, 1, null, _c0);
          } if (rf & 2) { // update dom
            elementProperty(0, "ngForOf", bind(pipeBind1(1, 1, ctx.todos)));
          }
        },
        encapsulation: 2
      });
    
      return TodosComponent;
    }());
    
  • 示例

    <span>My name is {
         
         {name}}</span>
    
    // 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));
    }
    

Tree Shaking 特性

Tree Shaking 指在编译目标代码时移除上下文中未引用代码的过程

增量 DOM 充分利用了这一点,因为它使用了基于指令的方法。

如前所述,增量 DOM 在编译之前将每个组件编译成一组指令,这有助于识别未使用的指令。因此,它们可以在编译时进行删除操作

虚拟 DOM 不能够 Tree Shaking,因为它使用解释器,并且没有办法在编译时识别未使用的代码

减少内存使用

与虚拟 DOM 不同,增量 DOM 在重新呈现应用程序 UI 时不会生成真实 DOM 的副本。

此外,如果应用程序 UI 没有变化,增量 DOM 就不会分配任何内存。

大多数情况下,我们都是在没有任何重大修改的情况下重新呈现应用程序 UI。因此,按照这种方法可以极大地节省设备的内存使用;

Incremental DOM,在视图未改变时,是不需要任何内存的。我们只有在添加或删除DOM时需要分配内存。并且分配的内存大小与改变的DOM的大小成正比

增量 DOM 似乎有一个减少虚拟 DOM 内存占用的解决方案。但是为什么其他框架不使用它呢。

这里存在一个权衡,虽然增量 DOM 通过按照更有效的方法来计算差异,从而减少了内存使用,但是该方法比虚拟 DOM 更耗时,

因此,在选择使用增量 DOM 和虚拟 DOM 时,会对运行速度和内存使用之间进行权衡。

Ivy 中的增量 DOM

Ivy 引擎基于增量 DOM 的概念,它与虚拟 DOM 方法的不同之处在于,diff 操作是针对 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 树并在数据更改时就地更新它们。

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模板里的相关指令。最终生成的这些指令,会体现在编译后的组件中。

  • 示例

    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 可树摇的原因。

虚拟 DOM 的优缺点

  • 高效的 diff 算法
  • 简单且有助于提升性能
  • 没有 React 也能使用
  • 轻量
  • 允许构建应用程序且不考虑状态转换

虽然虚拟 DOM 快速高效,但有一个缺点

这个区分过程(diffing process)确实减少了真实 DOM 的工作量。但它需要将当前的虚拟 DOM 状态与之前的状态进行比较,以识别变化。为了更好地理解这一点,让我们看一个小的 React 代码示例

function WelcomeMessage (props) {
  return (
    <div className="welcome">
      Welcome {props.name}
    </div>
  );
}

假设 props.name 的初始值是 Chameera ,后来改成了 Reader。

整个代码中唯一的变化就是 props,不需要改变 DOM 节点或者比较 <div> 标签内部的属性。

然而,使用 diff 算法,有必要检查所有步骤来识别变化。

我们在开发过程中可以看到大量这样的微小变化,比较用户 UI 中的每个元素无疑是一种开销。

虚拟 DOM 的设计中存在一个无法避免的问题:

每个渲染操作分配一个新的虚拟 DOM 树,该树至少大到足以容纳发生变化的节点,并且通常更大一些,这样的设计会导致更多的一些内存占用。

当大型虚拟 DOM 树需要大量更新时,尤其是在内存受限的移动设备上,性能可能会受到影响。

这可以被认为是虚拟 DOM 的主要缺点之一,然而,增量 DOM 为这个大量内存使用问题提供了一个解决方案

React 中分别对 tree diff、component diff 以及 element diff 进行了算法优化,同时引入了任务调度来控制状态更新的计算和渲染。

在 Vue 3.0 中,则将虚拟 DOM 的更新从以前的整体作用域调整为树状作用域,树状的结构会带来算法的简化以及性能的提升。

增量 DOM 的优缺点

正如前面提到的,增量 DOM 通过使用真实 DOM 跟踪变化,提供了一个减少虚拟 DOM 内存消耗的解决方案。

这种方法大大降低了计算开销,也优化了应用程序的内存使用

所以,这是使用增量 DOM 相对于虚拟 DOM 的主要优势,可以列出增量 DOM 的其他优点

  • 易于与许多其他框架结合使用
  • 简单的 API 使其成为强大的目标模板引擎
  • 适合基于移动设备的应用程序,
  • 优化了内存占用

在大多数情况下,增量 DOM 不如虚拟 DOM 运行快

虽然增量 DOM 带来了减少内存使用的解决方案,但是该解决方案影响了增量 DOM 的速度,因为增量 DOM 的差异计算比虚拟 DOM 方法耗费更多时间。

因此,我们可以认为这是使用增量 DOM 的主要缺点。

Angular Ivy 编译器中增量DOM怎么应用-群英

Understanding Angular Ivy: Incremental DOM and Virtual DOM

增量DOM与虚拟DOM

了解Angular Ivy: Incremental DOM 和 Virtual DOM

猜你喜欢

转载自blog.csdn.net/SeriousLose/article/details/128130309
DOM