【小程序】自己实现一个 ReactDOM & Remax 初识

自己实现一个 ReactDOM & Remax 初识

skywalker512发布于 2020-05-20

【小程序】自己实现一个 ReactDOM & Remax 初识

如上图,可以看到 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 正考虑使用后者,据说能提升性能

【小程序】自己实现一个 ReactDOM & Remax 初识

可以看见我们需要定义的就是 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 分为以下部分。

协调阶段开始提交提交阶段
createInstanceprepareCommitappendChildToContainer
createTextInstanceinsertBefore
appendInitialChildinsertInContainerBefore
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 module="helper" />

<import/>

<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />

生成之后是这个样子

<wxs module="helper" />

<import/>

<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />

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>

【小程序】自己实现一个 ReactDOM & Remax 初识

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

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

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

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

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

参考

  • https://www.youtube.com/watch...
  • https://zhuanlan.zhihu.com/p/...
  • https://zhuanlan.zhihu.com/p/...
  • https://zhuanlan.zhihu.com/p/...
  • https://remaxjs.org/advanced-...

javascriptreact.jsreact-dom小程序remaxjs

阅读 613更新于 2020-05-20

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议

avatar

skywalker512

13 声望

0 粉丝

0 条评论

得票时间

avatar

skywalker512

13 声望

0 粉丝

宣传栏

【小程序】自己实现一个 ReactDOM & Remax 初识

如上图,可以看到 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 正考虑使用后者,据说能提升性能

【小程序】自己实现一个 ReactDOM & Remax 初识

可以看见我们需要定义的就是 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 分为以下部分。

协调阶段开始提交提交阶段
createInstanceprepareCommitappendChildToContainer
createTextInstanceinsertBefore
appendInitialChildinsertInContainerBefore
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 module="helper" />

<import/>

<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />

生成之后是这个样子

<wxs module="helper" />

<import/>

<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />

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>

【小程序】自己实现一个 ReactDOM & Remax 初识

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

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

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

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

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

参考

  • https://www.youtube.com/watch...
  • https://zhuanlan.zhihu.com/p/...
  • https://zhuanlan.zhihu.com/p/...
  • https://zhuanlan.zhihu.com/p/...
  • https://remaxjs.org/advanced-...

以上是 【小程序】自己实现一个 ReactDOM &amp; Remax 初识 的全部内容, 来源链接: utcz.com/a/106596.html

回到顶部