---
title: 实现原理
summary: "介绍 Ray 基于 react-reconciler 的实现原理，包括 VNode 虚拟节点树和小程序模板递归渲染机制。"
tags: [Ray实现原理, react-reconciler渲染器, VNode虚拟节点树, 小程序模板递归渲染]
questions:
  - Ray 框架的实现原理是什么？它和 Remax 的关系是什么？
  - Ray 中的 VNode 是什么？它的基本结构包含哪些字段？
  - React 组件在 Ray 中是如何被渲染成小程序界面的？
  - 小程序屏蔽了 DOM，Ray 是怎么解决这个问题的？
  - VNode 的 toJSON 方法有什么作用？
  - Ray 生成的小程序模板是如何递归渲染 VNode 树的？
  - VNode 中的 id 字段和 container 字段分别有什么作用？
  - VNode 上的 appendChild、removeChild、insertBefore 方法模拟了什么？
  - VNode 渲染出来的 JSON 数据是如何成为小程序 Page 的 data 的？
---

# 实现原理

Ray 本质上是站在 Remax 的基础上完成的运行时改造，本质是一个通过 `react-reconciler` 实现的一个小程序端的渲染器。关于 `react-reconciler` 和 React 渲染器相关的内容推荐观看[这个视频](https://www.youtube.com/watch?v=CGpMlWVcHok)，这里不再赘述。

众所周知，小程序屏蔽了 DOM，我们的代码运行在一个 worker 线程中，无法直接去操作视图层的 DOM。Ray 通过引入 `VNode`，让 React 在 reconciliation 过程中不是直接去改变 DOM，而先更新 `VNode`。

`VNode` 的基本结构如下：

```js
interface VNode {
  id: number;
  container: Container;
  children: VNode[];
  mounted: boolean;
  type: string | symbol;
  props?: any;
  parent: VNode | null;
  text?: string;
  appendChild(node: VNode): void;
  removeChild(node: VNode): void;
  insertBefore(newNode: VNode, referenceNode: VNode): void;
  toJSON(): RawNode;
}
```

- `id` - 节点 id，这是一个自增的唯一 id，用于标识节点。
- `container` - 类似 `ReactDOM.render(<App />, document.getElementById('root'))`，Ray 中会把组件渲染到一个容器中，容器的作用是保存 `VNode` 的引用。
- `children` - 子节点。
- `mounted`- 标识节点是否已经显示到视图层上。
- `type` - 节点的类型，也就是小程序中的基础组件，如：`view`、`text`等等。
- `props` - 节点的属性。
- `parent` - 父节点。
- `text` - 文本节点上的文字。

可以看到，`VNode` 也是一个树结构，我们在 `VNode` 上实现了类似 `DOM` 中的节点操作方法。在 React 的更新完成后，我们会调用节点上的 `toJSON` 方法，把这个 `VNode` 变成一个 JSON 对象。

举个例子，假设我们有这样一个页面组件：

```js
import React from 'react';
import { View, Text } from '@ray-js/ray';
const IndexPage = () => {
  return (
    <View className="greeting">
      <Text>Hello Ray</Text>
    </View>
  );
};
export default IndexPage;
```

Ray 在渲染这个组件时，会把它渲染成如下的 `VNode` 结构：

```json
{
  "id": 0,
  "type": "root",
  "children": [
    {
      "id": 1,
      "type": "view",
      "props": {
        "className": "greeting"
      },
      "children": [
        {
          "id": 2,
          "type": "text",
          "props": {},
          "children": [
            {
              "type": "plain-text",
              "text": "Hello Ray"
            }
          ]
        }
      ]
    }
  ]
}
```

其中 `root` 节点是由 Ray 内部创建的，这个渲染出来的 `VNode` 数据就会成为小程序 `Page` 的 `data`。

## 在视图层显示界面

上面讲到我们的 React 组件最终会被渲染成一个我们称之为 `VNode` 的 JSON 对象，并且这个对象会作为小程序 `Page` 的 `data`。现在我们要做的就是在小程序的模板里怎么把这个 `data` 给显示出来了。

我们在构建 Ray 应用时，会生成一个页面模板显示这个 `VNode`，这个模板大概是下面这个样子：

```xml
<block a:for="{{root.children}}" a:key="{{item.id}}">
  <template is="{{'REMAX_TPL_' + item.type}}" data="{{item: item}}" />
</block>

<template name="REMAX_TPL_view">
  <view class="{{item.props['className']}}">
    <block a:for="{{item.children}}" key="{{item.id}}">
      <template is="{{'REMAX_TPL_' + item.type}}" data="{{item: item}}" />
    </block>
  </view>
</template>

<template name="REMAX_TPL_text">
  <text>
    <block a:for="{{item.children}}" key="{{item.id}}">
      <template is="{{'REMAX_TPL_' + item.type}}" data="{{item: item}}" />
    </block>
  </text>
</template>

<template name="REMAX_TPL_plain-text">
  <block>{{item.text}}</block>
</template>
```

可以看到，我们会先去遍历根节点的子元素，再根据每个子元素的类型选择对应的模板来渲染子元素，然后在每个模板中我们又会去遍历当前元素的子元素，以此把整个节点树递归遍历出来。

以上就是 Ray 实现的基本原理，在具体实现上我们还会去做一些优化，想深入了解的同学可以直接看代码。
