自己实现一个 ReactDOM & Remax 初识

这篇文章写于 2019-11-30 可能其中的内容已经发生更改

1574993212386

如上图,可以看到 react 实际上分成了几个部分,

  • react

    react 本身代码量并相比于整体来说并不算多,他主要的工作就是调用 react-reconciler 的一些接口,定义了一些 Symbol 之类的工作。

  • react-reconciler

    那么 react-reconciler 就主要维护 VirtualDOM 树,完成什么时候需要更新要更新什么之类的操作,你们可能常听到的 Fiber Diff 之类的东西就在这里面。

  • ReactDOM

    可以看到这里并排了几个东西 比如 react-dom 对应于浏览器,react-native 对应了移动平台...,其中的 react-dom 就实现了怎样将 react 中的东西渲染到网页上面。

可能上面的东西说起来不怎么具体,那举一些 api 的例子可能能够更清楚的分辨他们

  • 主机环境 (dom / native)

    • 浏览器

    div, span,img...

    • Native

    View, Text, Image...

  • 共用的部分 (reconciler)

    • function components
    • class components
    • props, state
    • effects, lifecycles
    • key, ref, context
    • lazy, error boundaries
    • concurrent mode, suspense

其实我们在 npm 安装的时候就只装了 react 和 react-dom,因为 reconciler 是在 react-dom 这个包中有的,但是我们仍然可以单独安装 react-reconciler 来构建我们自己的 render

react-reconciler

react-reconciler 有两种模式

  • mutation mode

    view = createView()
    updateView(view, { color: 'red' })

    对应到 dom 操作就是

    div = document.createElement('div')
    div.style.color = 'red'
  • persistent mode (immutable)

    view = createView()
    view = cloneView(view, { color: 'red' })

现在 ReactNative 正考虑使用后者,据说能提升性能

1575031823870

可以看见我们需要定义的就是 hostConfig 部分,我们还是直接开始吧。当一个舒适的 api 仔。

开始

安装 CRA (create react app)

npx create-react-app test-app
cd test-app
npm start

安装 react-reconciler

npm i react-reconciler
npm i @types/react-reconciler -D

显示元素

src/index.js

import React from 'react';
- import ReactDOM from 'react-dom';
+ import ReactDOM from './ReactDOM'
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

src/ReactDOM

import ReactReconciler from 'react-reconciler'

const reconciler = ReactReconciler({
  // 定义一些东西怎样与 render 环境进行交互
  supportsMutation: true,
  /**
   * createInstance
   * @param type 字符串 eg: img, p, a (这里没有 App 因为 react 已经提前处理了)
   * @param props props eg: className, src
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @return {Element}
   */
  createInstance (type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
    const el = document.createElement(type);
    // if (props.className) el.className = props.className
    ['alt', 'className', 'href', 'rel', 'src', 'target'].forEach(item => {
      if (props[item]) el[item] = props[item]
    })
    return el // 这里 react 并没有规定返回的是什么,也就意味这里可以在这里返回一个你自定的 dom
  },
  /**
   * createTextInstance
   * @param text 文字信息,例如下面的 click
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @returns {Text}
   */
  createTextInstance (text, rootContainerInstance, hostContext, internalInstanceHandle) {
    // <div>click</div>
    return document.createTextNode(text)
  },
  appendChildToContainer (container, child) {
    container.appendChild(child)
  },
  appendChild (parentInstance, child) {
    parentInstance.appendChild(child)
  },
  appendInitialChild (parentInstance, child) {
    parentInstance.appendChild(child)
  },
  prepareUpdate (instance, type, oldProps, newProps, rootContainerInstance, hostContext) {},
  commitUpdate (instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) {},
  finalizeInitialChildren (parentInstance, type, props, rootContainerInstance, hostContext) {},
  getChildHostContext (parentHostContext, type, rootContainerInstance) {},
  getPublicInstance (instance) {},
  getRootHostContext (rootContainerInstance) {},
  prepareForCommit (containerInfo) {},
  resetAfterCommit (containerInfo) {},
  shouldSetTextContent (type, props) {
    return false
  },
})

export default {
  render (whatToRender, div) {
    const container = reconciler.createContainer(div, false, false)
    reconciler.updateContainer(whatToRender, container, null, null)
  }
}

现在就可以看到已经有东西显示在页面上了

事件监听

接下来,我们添加一些事件上去

src/App.js

import React, { useState } from 'react'
import logo from './logo.svg'
import './App.css'

function App () {
+ const [showLogo, setShowLogo] = useState(true)
  return (
-   <div className="App">
+   <div className="App" onClick={() => setShowLogo(show => !show)}>
      <header className="App-header">
-       <img src={logo} className="App-logo" alt="logo"/>
+       {showLogo && <img src={logo} className="App-logo" alt="logo"/>}
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

export default App

src/ReactDOM

import ReactReconciler from 'react-reconciler'

const reconciler = ReactReconciler({
  // 定义一些东西怎样与 render 环境进行交互
  supportsMutation: true,
  /**
   * createInstance
   * @param type 字符串 eg: img, p, a (这里没有 App 因为 react 已经提前处理了)
   * @param props props eg: className, src
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @return {Element}
   */
  createInstance (type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
    const el = document.createElement(type);
    ['alt', 'className', 'href', 'rel', 'src', 'target'].forEach(item => {
      if (props[item]) el[item] = props[item]
    })
+   if (props.onClick) el.addEventListener('click', props.onClick)
    return el // 这里 react 并没有规定返回的是什么,也就意味这里可以在这里返回一个你自定的 dom
  },
  /**
   * createTextInstance
   * @param text 文字信息,例如下面的 click
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @returns {Text}
   */
  createTextInstance (text, rootContainerInstance, hostContext, internalInstanceHandle) {
    // <div>click</div>
    return document.createTextNode(text)
  },
  appendChildToContainer (container, child) {
    container.appendChild(child)
  },
  appendChild (parentInstance, child) {
    parentInstance.appendChild(child)
  },
  appendInitialChild (parentInstance, child) {
    parentInstance.appendChild(child)
  },
+ removeChildFromContainer (container, child) {
+   container.removeChild(child)
+ },
+ removeChild (parentInstance, child) {
+   parentInstance.removeChild(child)
+ },
+ insertInContainerBefore (container, child, beforeChild) {
+   container.insertBefore(child, beforeChild)
+ },
+ insertBefore (parentInstance, child, beforeChild) {
+   parentInstance.insertBefore(child, beforeChild)
+ },
  prepareUpdate (instance, type, oldProps, newProps, rootContainerInstance, hostContext) {},
  commitUpdate (instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) {},
  finalizeInitialChildren (parentInstance, type, props, rootContainerInstance, hostContext) {},
  getChildHostContext (parentHostContext, type, rootContainerInstance) {},
  getPublicInstance (instance) {},
  getRootHostContext (rootContainerInstance) {},
  prepareForCommit (containerInfo) {},
  resetAfterCommit (containerInfo) {},
  shouldSetTextContent (type, props) {
    return false
  },
})

export default {
  render (whatToRender, div) {
    const container = reconciler.createContainer(div, false, false)
    reconciler.updateContainer(whatToRender, container, null, null)
  }
}

然后我们发现 logo 实现了 toggle 的效果,当然真实情况肯定没有之前我们所写的那么简单,我们最开始学 react 的时候就知道,react 将所有的事件都绑定到顶层的

这里我们可以看到我们并没有写一些与 state 相关的东西,这就说明了 ReactReconciler 帮我们完成了 Diff 之类的东西,这样我们只需指导 react 怎样去处理 dom 就可以了,ReactReconciler 只要是 js runtime 就可以运行,这也提供了 react 能渲染到多个平台上的能力。

更新属性

src/App.js

import React, { useEffect, useState } from 'react'
import logo from './logo.svg'
import './App.css'

function App () {
  const [showLogo, setShowLogo] = useState(true)
+ const [color, setColor] = useState('red')
+ useEffect(()=>{
+   const colors = ['red', 'green', 'blue']
+   let i = 0
+   let interval = setInterval(()=>{
+     i++
+     setColor(colors[i % 3])
+   }, 1000)
+   return () => clearInterval(interval)
+ })
  return (
    <div className="App" onClick={() => setShowLogo(show => !show)}>
      <header className="App-header">
        {showLogo && <img src={logo} className="App-logo" alt="logo"/>}
-       <p>
+       <p bgColor={color}>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

export default App

src/ReactDOM

import ReactReconciler from 'react-reconciler'

const reconciler = ReactReconciler({
  // 定义一些东西怎样与 render 环境进行交互
  supportsMutation: true,
  /**
   * createInstance
   * @param type 字符串 eg: img, p, a (这里没有 App 因为 react 已经提前处理了)
   * @param props props eg: className, src
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @return {Element}
   */
  createInstance (type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
    const el = document.createElement(type);
    ['alt', 'className', 'href', 'rel', 'src', 'target'].forEach(item => {
      if (props[item]) el[item] = props[item]
    })
    if (props.onClick) el.addEventListener('click', props.onClick)
+   if (props.bgColor) el.style.backgroundColor = props.bgColor
    return el // 这里 react 并没有规定返回的是什么,也就意味这里可以在这里返回一个你自定的 dom
  },
  /**
   * createTextInstance
   * @param text 文字信息,例如下面的 click
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @returns {Text}
   */
  createTextInstance (text, rootContainerInstance, hostContext, internalInstanceHandle) {
    // <div>click</div>
   return document.createTextNode(text)
  },
  appendChildToContainer (container, child) {
    container.appendChild(child)
  },
  appendChild (parentInstance, child) {
    parentInstance.appendChild(child)
  },
  appendInitialChild (parentInstance, child) {
    parentInstance.appendChild(child)
  },
  removeChildFromContainer (container, child) {
    container.removeChild(child)
  },
  removeChild (parentInstance, child) {
    parentInstance.removeChild(child)
  },
  insertInContainerBefore (container, child, beforeChild) {
    container.insertBefore(child, beforeChild)
  },
  insertBefore (parentInstance, child, beforeChild) {
    parentInstance.insertBefore(child, beforeChild)
  },
  prepareUpdate (instance, type, oldProps, newProps, rootContainerInstance, hostContext)   {
+   let palyload
+   if(oldProps.bgColor !== newProps.bgColor) {
+     palyload = { newBgColor: newProps.bgColor }
+   }
+   return palyload
  },
  /**
   * commitUpdate
   * @param instance dom 实例
   * @param updatePayload 从 prepareUpdate 返回的
   * @param type
   * @param oldProps
   * @param newProps
   * @param internalInstanceHandle
   */
  commitUpdate (instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
+   if (updatePayload.newBgColor) {
+     instance.style.backgroundColor = updatePayload.newBgColor
+   }
  },
  finalizeInitialChildren (parentInstance, type, props, rootContainerInstance, hostContext) {},
  getChildHostContext (parentHostContext, type, rootContainerInstance) {},
  getPublicInstance (instance) {},
  getRootHostContext (rootContainerInstance) {},
  prepareForCommit (containerInfo) {},
  resetAfterCommit (containerInfo) {},
  shouldSetTextContent (type, props) {
    return false
  },
})

export default {
  render (whatToRender, div) {
    const container = reconciler.createContainer(div, false, false)
    reconciler.updateContainer(whatToRender, container, null, null)
  }
}

这里大概讲一下 hostConfig 定义的东西,也相当于对于前面的一些重新记忆吧,我们先将这些 api 分为以下部分。

协调阶段 开始提交 提交阶段
createInstance prepareCommit appendChildToContainer
createTextInstance insertBefore
appendInitialChild insertInContainerBefore
removeChild
removeChildFromContainer
import ReactReconciler from 'react-reconciler'

const reconciler = ReactReconciler({
  // 定义一些东西怎样与 render 环境进行交互
  supportsMutation: true,
  /**
   * 普通节点实例创建,例如 DOM 的 Element 类型
   * @param type 字符串 eg: img, p, a (这里没有 App 因为 react 已经提前处理了)
   * @param props props eg: className, src
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @return {Element}
   */
  createInstance (type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
    const el = document.createElement(type);
    ['alt', 'className', 'href', 'rel', 'src', 'target'].forEach(item => {
      if (props[item]) el[item] = props[item]
    })
    if (props.onClick) el.addEventListener('click', props.onClick)
    if (props.bgColor) el.style.backgroundColor = props.bgColor
    return el // 这里 react 并没有规定返回的是什么,也就意味这里可以在这里返回一个你自定的 dom
  },
  /**
   * 文本节点的创建,例如 DOM 的 Text 类型
   * @param text 文字信息,例如下面的 click
   * @param rootContainerInstance
   * @param hostContext
   * @param internalInstanceHandle
   * @returns {Text}
   */
  createTextInstance (text, rootContainerInstance, hostContext, internalInstanceHandle) {
    // <div>click</div>
    return document.createTextNode(text)
  },
  // 添加子节点到容器节点(根节点)
  // 也就是加载到 document.getElementById('root') 中去
  appendChildToContainer (container, child) {
    // console.log(container)
    container.appendChild(child)
  },
  // 如果节点在 未挂载 状态下,会调用这个来添加子节点
  appendInitialChild (parentInstance, child) {
    // console.log(parentInstance)
    parentInstance.appendChild(child)
  },
  // 插入子节点到容器节点(根节点)
  insertInContainerBefore (container, child, beforeChild) {
    // console.log(container)
    container.insertBefore(child, beforeChild)
  },
  // 插入子节点
  insertBefore (parentInstance, child, beforeChild) {
    // console.log(parentInstance)
    parentInstance.insertBefore(child, beforeChild)
  },
  // 从容器节点(根节点)中移除子节点
  removeChildFromContainer (container, child) {
    // console.log(container)
    container.removeChild(child)
  },
  // 删除子节点
  removeChild (parentInstance, child) {
    // console.log(parentInstance)
    parentInstance.removeChild(child)
  },
  /**
   * 准备节点更新. 如果返回空则表示不更新,这时候commitUpdate则不会被调用
   * @param instance dom 元素
   * @param type div, a
   * @param oldProps 以前的 Props
   * @param newProps 要更新的 Props
   * @param rootContainerInstance
   * @param hostContext
   * @returns {{newBgColor: *}}
   */
  prepareUpdate (instance, type, oldProps, newProps, rootContainerInstance, hostContext)   {
    let palyload
    if(oldProps.bgColor !== newProps.bgColor) {
      palyload = { newBgColor: newProps.bgColor }
    }
    return palyload
  },
  /**
   * commitUpdate
   * @param instance dom 实例
   * @param updatePayload 从 prepareUpdate 返回的
   * @param type div, a ...
   * @param oldProps
   * @param newProps
   * @param internalInstanceHandle
   */
  commitUpdate (instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
    if (updatePayload.newBgColor) {
      instance.style.backgroundColor = updatePayload.newBgColor
    }
  },
  finalizeInitialChildren (parentInstance, type, props, rootContainerInstance, hostContext) {},
  getChildHostContext (parentHostContext, type, rootContainerInstance) {},
  getPublicInstance (instance) {},
  getRootHostContext (rootContainerInstance) {},
  prepareForCommit (containerInfo) {},
  resetAfterCommit (containerInfo) {},
  shouldSetTextContent (type, props) {
    return false
  },
})

export default {
  render (whatToRender, div) {
    const container = reconciler.createContainer(div, false, false)
    reconciler.updateContainer(whatToRender, container, null, null)
  }
}

然后可以看到这里有很多的 api 都没有用到,实际的 react-dom 肯定特别复杂

然后可能这东西不怎么 practical(实际, 可能不会用上),但是大家应该对 react 有了一点认识了。

Remax

Remax 是蚂蚁金服搞的一个用 react 写小程序的框架,他也是通过编写自己的 react render ,渲染到维护在内存中的一个 vdom,然后再通过 setData 到小程序中的,然后这个项目才刚开始,不像 taro 是一个庞然大物,这里大概说一下他的实现。( taro next 也采用了类似的技术,但 taro 将 dom 的实现抽离,这样就能让 vue 也能用上这种方案)

组成

remax 通过 lerna 进行 monorepo 管理,分为以下部分

  • remax 提供运行时 -- 相当于 react dom
  • remax-cli 提供构建功能 -- 使用 EJS 生成 小程序的 wxml, wxs...,用 rollup 打包代码

现在主要来讲 Remax

VNode

小程序中我们拿不到 dom,所以就不能像之前那样做 dom 的操作了,然后之前说到了 remax 会在内存中创建一个 vdom,我们先来看看他的 vnode 的代码, 这里只贴了一些主要的代码,并且删除了 TypeScript 的一些东西

...

export default class VNode {
  ... 
  // 构造函数
  constructor({
    id, // 层级 id 会在生成的小程序自定义组件中出现
    type, // Text, Image
    props,
    container, // 上级的容器
  }) {
    this.id = id;
    this.container = container;
    this.type = type;
    this.props = props;
    this.children = [];
  }

  // 添加节点到父节点中
  appendChild(node, immediately) {
    node.parent = this;
    this.children.push(node);
    ...
  }

  // 从父节点中删除节点
  removeChild(node, immediately) {
    const start = this.children.indexOf(node);
    this.children.splice(start, 1);
    ...
  }

  ...
  // **与 dom 不同的 提交更新到 container**
  update() {
    // root 不会更新,所以肯定有 parent (container)
    this.container.requestUpdate(
		...
    );
  }
}

可见和 dom 上面能做的操作差不多

HostConfig(react-reconciler)

我们现在有了 vdom,那么我们来看怎么让 react 来操作 vdom,跟我们上面讲的在网页上的 hostConfig 需要定义的东西差不多,下面是 Remax hostConfig 的部分代码

const HostConfig = {
  // 创建宿主组件实例
  createInstance(type, newProps, container) {
    const id = generate();
    // 对 props 进行一些处理,如果 newProps 是 function 的话拿到顶部的 context 中进行 callback
    const props = processProps(newProps, container, id);
    return new VNode({
      id,
      type,
      props,
      container,
    });
  },

  // 创建宿主组件文本节点实例
  createTextInstance(text: string, container: Container) {
    const id = generate();
    const node = new VNode({
      id,
      type: TYPE_TEXT,
      props: null,
      container,
    });
    node.text = text;
    return node;
  },

  ...
  // 添加子节点到容器节点(根节点)
  appendChildToContainer(container: any, child: VNode) {
    container.appendChild(child);
    child.mounted = true;
  },
  // 删除子节点
  removeChild(parent: VNode, child: VNode) {
    parent.removeChild(child, false);
  },
  ...
  // 提交更新
  commitUpdate(node, updatePayload, type, oldProps, newProps) {
    node.props = processProps(newProps, node.container, node.id);
    node.update();
  },
}

更新 vdom 到 小程序

remax 在 Container 也就是网页中的 document.getElementById('root')

export default class Container {
  ...
  requestUpdate(
    path,
    start,
    deleteCount,
    immediately,
    ...items: RawNode[]
  ) {
    const update = {
      path, // 更新节点的树路径
      start, // 更新节点在children中的索引
      deleteCount, 
      items: items.map(normalizeRawNode), // 主要对 props 进行一些操作, 比如 className => class
    };
    if (immediately) { // 马上执行更新
      this.updateQueue.push(update); 
      this.applyUpdate();
    } else { // 进入队列中
      if (this.updateQueue.length === 0) {
       // 这里为什么是0, 因为 Promise 执行肯定在我们看到的代码之后,不为 0 的话 上一次的更新还未执行完
       // 比如一次更新(之前的东西已经执行完了) 那么队列的长度就为 0
       // 然后就进入这个 if 分支
       // 放入 promise
       // 然后执行其他代码
       // 执行完其他代码
       // 执行 () => this.applyUpdate()
        Promise.resolve().then(() => this.applyUpdate()); // 放到 Promise 中
      }
      this.updateQueue.push(update);
    }
  }

  applyUpdate() {
	...
    const action = { // 发送给小程序的对象
      type: 'splice',
      payload: this.updateQueue.map(update => ({
        path: stringPath(update.path),
        start: update.start,
        deleteCount: update.deleteCount,
        item: update.items[0],
      })),
      id: generateActionId(),
    };
	// this.context 就是小程序实例
    this.context.setData({ action: tree }); // setData 到小程序中
    this.updateQueue = [];
  }
  ...
}

小程序渲染

我们已经把数据给到了小程序,那么小程序这里是怎样渲染成真正的元素。

我们都知道小程序是以页面为基本单位,我们先来看 pages 是怎么样的

这里使用了 ejs 大家大概看看

<wxs src="<%= jsHelper %>" module="helper" />
<import src="<%= baseTemplate %>"/>
<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />

生成之后是这个样子

<wxs src="../../helper.wxs" module="helper" />
<import src="../../base.wxml"/>
<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />

wxs 可能大家之前之前不知道,wxs(qs, sjs) 是运行在小程序的渲染线程的被阉割的 js runtime 具体可以看 uni-app 写的文章 ,能减少通时的消耗的消耗,所以把小程序这边的 vdom 放在这里面

helper.wxs

var tree = {
  root: {
    children: [],
  },
};
...
function reduce(action) {
  switch (action.type) {
    case 'splice':
      for (var i = 0; i < action.payload.length; i += 1) {
        var value = get(tree, action.payload[i].path); // 通过 path 拿到对应的 vnode
        if (action.payload[i].item) { // 进行一些处理
          value.splice(
            action.payload[i].start,
            action.payload[i].deleteCount,
            action.payload[i].item
          );
        } else {
          value.splice(action.payload[i].start, action.payload[i].deleteCount);
        }
        set(tree, action.payload[i].path, value); // 将生成的进行替换
      }
      return tree; // 返回回去
    default:
      return tree;
  }
}
...
module.exports = {
  reduce: reduce,
};

base.wxml

定义了一些 image,text 等组件

<template name="REMAX_TPL"> // 每个 page 的顶部模板
	<block wx:for="{{tree.root.children}}" wx:key="{{id}}">
		<template is="REMAX_TPL_1_CONTAINER" data="{{i: item}}" />
	</block>
</template>
...
// image 的模板,并且是 page 最顶部的那个,因为微信小程序不能递归的调用,所以只能每层的template都不一样
<template name="REMAX_TPL_1_image">
  <image>
    <block wx:for="{{i.children}}" wx:key="{{id}}">
      <template is="REMAX_TPL_2_CONTAINER" data="{{i: item}}" />
    </block>
  </image>
</template>
...
<template name="REMAX_TPL_2_image">
  <image >
    <block wx:for="{{i.children}}" wx:key="{{id}}">
      <template is="REMAX_TPL_3_CONTAINER" data="{{i: item}}" />
    </block>
  </image>
</template>
...
...
...
...
// 所以一个页面只能套 20 层,不是就没 template 了
<template name="REMAX_TPL_20_image">
  <image >
    <block wx:for="{{i.children}}" wx:key="{{id}}">
      <template is="REMAX_TPL_21_CONTAINER" data="{{i: item}}" />
    </block>
  </image>
</template>

1575038083865

但是这有一些缺点,可能会影响性能,但是其实性能在出现之前都不是问题。

  • 一是因为 Remax 相较于浏览器多了两个或者三个 vdom

    • react 维护的 vdom
    • remax 在逻辑进程维护的 vdom
    • remax 在 wxs 中的 vdom
    • (可能有) 小程序自己的 vdom

    但是好处是我们在 setData 是经过 diff 的最小的数据量,而我们使用了 setData 也就意味者想用 react 来管理动画是不可能的了,但是目前没有办法改善,因为 wxs 定义的模块只能在 wxml 中用,也就是 remax 不能直接访问 wxs 的函数,即 逻辑进程维护的 vdom 必须存在然后通过 setData 传递。

  • 二是微信小程序限制,不能模板不能递归,只能套 20 层

参考