dojo dragon main logo

渲染部件

Dojo 是一个响应式框架,负责处理数据变更的传播和相关的后台更新渲染。Dojo 采用虚拟 DOM(VDOM) 的概念来描述输出的元素,VDOM 中的节点是简单的 JavaScript 对象,旨在提高开发人员效率,而不用与实际的 DOM 元素交互。

应用程序只需要关心,将它们的期望的输出结构声明为有层级的虚拟 DOM 节点即可,通常是作为部件的渲染函数的返回值来完成的。然后,框架的 Renderer 组件会将期望的输出同步为 DOM 中的具体元素。也可以通过给虚拟 DOM 节点传入属性,从而配置部件和元素,以及为部件和元素提供状态。

Dojo 支持树的部分子节点渲染,这意味着当状态发生变化时,框架能够定位到受变化影响的 VDOM 节点的对应子集。然后,只更新 DOM 树中受影响的子树,从而响应变化、提高渲染性能并改善用户的交互体验。

注意: 部件渲染函数中返回的虚拟节点,是唯一影响应用程序渲染的因素。尝试使用任何其他实践,在 Dojo 应用程序开发中是被视为反模式的,应当避免。

支持 TSX

Dojo 支持使用 jsx 语法扩展,在 TypeScript 中被称为 tsx。此语法能更方便的描述 VDOM 的输出,并且更接近于构建的应用程序中的 HTML。

允许使用 TSX 的应用程序

可以通过 dojo create app --tsx CLI 命令 轻松搭建出允许使用 TSX 的项目。

对于不是通过这种方式搭建的 Dojo 项目,可以通过在项目的 TypeScript 配置中添加以下内容来启用 TSX:

./tsconfig.json

{
    "compilerOptions": {
        "jsx": "react",
        "jsxFactory": "tsx"
    },
    "include": ["./src/**/*.ts", "./src/**/*.tsx", "./tests/**/*.ts", "./tests/**/*.tsx"]
}

TSX 部件示例

具有 .tsx 文件扩展名的部件,要在渲染函数中输出 TSX,只需要导入 @dojo/framework/core/vdom 模块中的 tsx 函数:

src/widgets/MyTsxWidget.tsx

基于函数的部件:

import { create, tsx } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyTsxWidget() {
    return <div>Hello from a TSX widget!</div>;
});

基于类的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';

export default class MyTsxWidget extends WidgetBase {
    protected render() {
        return <div>Hello from a TSX widget!</div>;
    }
}

若部件需要返回多个顶级 TSX 节点,则可以将它们包裹在 <virtual> 容器元素中。这比返回节点数组更清晰明了,因为这样支持更自然的自动格式化 TSX 代码块。如下:

src/widgets/MyTsxWidget.tsx

基于函数的部件:

import { create, tsx } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyTsxWidget() {
    return (
        <virtual>
            <div>First top-level widget element</div>
            <div>Second top-level widget element</div>
        </virtual>
    );
});

使用 VDOM

VDOM 节点类型

Dojo 会在 VDOM 中识别出两类节点:

  • VNode,或称为 Virtual Nodes,是具体 DOM 元素的虚拟表示,作为所有 Dojo 应用程序最底层的渲染输出。
  • WNode,或称为 Widget Nodes,将 Dojo 部件关联到 VDOM 的层级结构上。

Dojo 的虚拟节点中,VNodeWNode 都可看作 DNode 的子类型,但应用程序通常不处理抽象层面的 DNode。推荐使用 TSX 语法,因为它能以统一的语法渲染两类虚拟节点。

实例化 VDOM 节点

如果不想使用 TSX,在部件中可以导入 @dojo/framework/core/vdom 模块中的 v()w() 函数。它们分别创建 VNodeWNode,并可作为部件渲染函数返回值的一部分。它们的签名,抽象地说,如下:

  • v(tagName | VNode, properties?, children?):
  • w(Widget | constructor, properties, children?)
参数 可选 描述
tagName | VNode 通常,会以字符串的形式传入 tagName,该字符串对应 VNode 将要渲染的相应 DOM 元素的标签名。如果传入的是 VNode,新创建的 VNode 将是原始 VNode 的副本。如果传入了 properties 参数,则会合并 properties 中重复的属性,并应用到副本 VNode 中。如果传入了 children 参数,将在新的副本中完全覆盖原始 VNode 中的所有子节点。
Widget | constructor 通常,会传入 Widget,它将导入部件当作泛型类型引用。还可以传入几种类型的 constructor,它允许 Dojo 以各种不同的方式实例化部件。它们支持延迟加载等高级功能。
properties v: 是, w: 否 用于配置新创建的 VDOM 节点的属性集。它们还允许框架检测节点是否已更新,从而重新渲染。
children 一组节点,会渲染为新创建节点的子节点。如果需要,还可以使用字符串字面值表示任何文本节点。部件通常会封装自己的子节点,因此此参数更可能会与 v() 一起使用,而不是 w()

虚拟节点示例

以下示例部件包含一个更有代表性的渲染函数,它返回一个 VNode。它期望的结构描述为,一个简单的 div DOM 元素下包含一个文本节点:

src/widgets/MyWidget.ts

基于函数的部件:

import { create, v } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyWidget() {
    return v('div', ['Hello, Dojo!']);
});

基于类的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v } from '@dojo/framework/core/vdom';

export default class MyWidget extends WidgetBase {
    protected render() {
        return v('div', ['Hello, Dojo!']);
    }
}

组合部件的示例

类似地,也可以使用 w() 方法组合部件,还可以混合使用两种类型的节点来输出多个节点,以形成更复杂的层级结构:

src/widgets/MyComposingWidget.ts

基于函数的部件:

import { create, v, w } from '@dojo/framework/core/vdom';

const factory = create();

import MyWidget from './MyWidget';

export default factory(function MyComposingWidget() {
    return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
});

基于类的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v, w } from '@dojo/framework/core/vdom';

import MyWidget from './MyWidget';

export default class MyComposingWidget extends WidgetBase {
    protected render() {
        return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
    }
}

渲染到 DOM 中

Dojo 为应用程序提供了一个渲染工厂函数 renderer()@dojo/framework/core/vdom 模块默认导出该函数。提供的工厂函数定义了应用程序的根节点,会在此处插入 VDOM 结构的输出结果。

应用程序通常在主入口点 (main.tsx/main.ts) 调用 renderer() 函数,然后将返回的 Renderer 对象挂载到应用程序的 HTML 页面中指定的 DOM 元素上。如果挂载应用程序时没有指定元素,则默认挂载到 document.body 下。

例如:

src/main.tsx

import renderer, { tsx } from '@dojo/framework/core/vdom';

import MyComposingWidget from './widgets/MyComposingWidget';

const r = renderer(() => <MyComposingWidget />);
r.mount();

MountOptions 属性

Renderer.mount() 方法接收一个可选参数 MountOptions,该参数用于配置如何执行挂载操作。

属性 类型 可选 描述
sync boolean 默认为: false。 如果为 true,则渲染生命周期中相关的回调(特别是 afterdeferred 渲染回调函数)是同步运行的。 如果为 false,则在 window.requestAnimationFrame() 下一次重绘之前,回调函数被安排为异步运行。在极少数情况下,当特定节点需要存在于 DOM 中时,同步运行渲染回调函数可能很有用,但对于大多数应用程序,不建议使用此模式。
domNode HTMLElement 指定 DOM 元素,VDOM 的渲染结果会插入到该 DOM 节点中。如果没有指定,则默认为 document.body
registry Registry 一个可选的 Registry 实例,可在挂载的 VDOM 间使用。

例如,将一个 Dojo 应用程序挂载到一个指定的 DOM 元素,而不是 document.body 下:

src/index.html

<!DOCTYPE html>
<html lang="en-us">
    <body>
        <div>This div is outside the mounted Dojo application.</div>
        <div id="my-dojo-app">This div contains the mounted Dojo application.</div>
    </body>
</html>

src/main.tsx

import renderer, { tsx } from '@dojo/framework/core/vdom';

import MyComposingWidget from './widgets/MyComposingWidget';

const dojoAppRootElement = document.getElementById('my-dojo-app') || undefined;
const r = renderer(() => <MyComposingWidget />);
r.mount({ domNode: dojoAppRootElement });

卸载应用程序

为了卸载(unmount)一个 Dojo 应用程序,renderer 提供了一个 unmount API,用于删除 DOM 节点,并对当前渲染的所有部件执行注册的所有销毁操作。

const r = renderer(() => <App />);
r.mount();
// To unmount the dojo application
r.unmount();

向 VDOM 中加入外部的 DOM 节点

Dojo 可以包装外部的 DOM 元素,有效地将它们引入到应用程序的 VDOM 中,用作渲染输出的一部分。这是通过 @dojo/framework/core/vdom 模块中的 dom() 工具方法完成的。它的工作原理与 v() 类似,但它的主参数使用的是现有的 DOM 节点而不是元素标记字符串。在返回 VNode 时,它会引用传递给它的 DOM 节点,而不是使用 v() 新创建的元素。

一旦 dom() 返回的 VNode 添加到应用程序的 VDOM 中,Dojo 应用程序就实际获得了被包装 DOM 节点的所有权。请注意,此过程仅适用于 Dojo 应用程序的外部节点,如挂载应用程序元素的兄弟节点,或与主网页的 DOM 断开连接的新创建的节点。如果包装的节点是挂载了应用程序的元素的祖先或子孙节点,将无效。

dom() API

  • dom({ node, attrs = {}, props = {}, on = {}, diffType = 'none', onAttach })
参数 可选 描述
node 添加到 Dojo VDOM 中的外部 DOM 节点
attrs 应用到外部 DOM 节点上的 HTML 属性(attributes)
props 附加到 DOM 节点上的属性(properties)
on 应用到外部 DOM 节点上的事件集合
diffType 默认为: none更改检测策略,确定 Dojo 应用程序是否需要更新外部的 DOM 节点
onAttach 一个可选的回调函数,在节点追加到 DOM 后执行

检测外部 DOM 节点的变化

通过 dom() 添加的外部节点是从常规的虚拟 DOM 节点中移除的,因为它们可能会在 Dojo 应用程序之外被处理。这意味着 Dojo 不能主要使用 VNode 的属性设置元素的状态,而是必须依赖 DOM 节点本身的 JavaScript 属性(properties)和 HTML 属性(attributes)。

dom() 接收 diffType 属性,允许用户为包装的节点指定属性变更检测策略。一个指定的策略,会指明如何使用包装的节点,以帮助 Dojo 来确定 JavaScript 属性和 HTML 属性是否已变化,然后将变化应用到包装的 DOM 节点上。默认的策略是 none,意味着 Dojo 只需在每个渲染周期将包装好的 DOM 元素添加到应用程序输出中。

注意: 所有的策略都使用前一次 VNode 中的事件,以确保它们会被正确的删除并应用到每个渲染中。

可用的 dom() 变化检测策略:

diffType 描述
none 此模式会为包装的 VNode 的前一次 attributesproperties 传入空对象,意味着在每个渲染周期,都会将传给 dom()propsattrs 重新应用于包装的节点。
dom 此模式基于 DOM 节点中的 attributesproperties 与传入 dom()propsattrs 进行比较计算,确定是否存在差异,然后应用这些差异。
vdom 此模式与前一次的 VNODE 做比较,这实际上是 Dojo 默认的 VDOM 差异对比策略。在变更检测和更新渲染时会忽略直接对包装的节点所做的任何修改。