Remax 使用及实现原理

3,147 阅读3分钟

作者:Chryseis,未经授权禁止转载。

remax项目搭建

remax

remax-demo

  1. 使用Remax cli命令
npx create-remax-app remax-wechat
  1. 命令成功后生成如下目录接口
remax-wechat/
┳ package.json
┣ dist/
┣ node_modules/
┣ public/
┣ src/
┗━┓ app.js
  ┣ app.css
  ┣ app.config.js
  ┣ pages/
  ┗━┓ index/
    ┗━┓
      ┣ index.js
      ┣ index.css
      ┣ index.config.js

  1. 引入状态管理,由于Remax的特性,状态管理可以是任何react的状态管理库,我采用dva作为状态管理(原因是与vuex基本一致)
yarn add remax-dva
  1. 安装dva后,需要修改app.js,添加model文件
// app.js
import './app.less';
import app from '@/models';

const App = app.start(({ children }) => {
  return children;
});

export default App;
// model/index.js
import dva from 'remax-dva';
import global from './global';

const app = dva();
app.model(global);

export default app;
//model/global.js
export default {
  namespace: 'global',
  state: {},
  effects: {},
  reducers: {},
};
  1. 配置remax.config
const path = require('path');
const less = require('@remax/plugin-less');

module.exports = {
  plugins: [less()],
  configWebpack({ config, webpack, addCSSRule }) {
    config.resolve.alias.merge({
      src: path.resolve(__dirname, 'src'),
    });
  },
};
  1. remax会根据NODE_ENV自动读取不同的环境变量
NODE_ENVenv文件
development.env.development.local,.env.development,.env.local,.env
test.env.test.local,.env.test,.env
production.env.production.local,env.production,env.local,.env

只有REMAX_APP_开头的变量才会注入

remax特点

  • 采用React coding,且React hooks语法全部支持
  • 引入原生组件无需配置usingComponents,直接import即可
  • 采用css modules 避免了样式重复
  • js方法,js变量,新的组件创建使用更加方便
  • ts完美支持
  • 调试更方便,可以接入react devtools
  • 原生小程序页面和remax页面可以共存

remax实现原理

react-reconciler

react-reconciler-demo

react-reconciler是什么?

官方定义

个人理解:react-reconciler充当了一个调节器,在react-core执行各种虚拟dom操作过程中提供hooks和已处理完成的vNode,给渲染器使用。

remax实现原理采用了react-reconcilerreact-reconciler是react和render之间的一个自定义处理器。

Remax通过reconciler的生成一份自定义的VNode Tree,再遍历Tree递归模版渲染出对应的小程序页面。在生成之前Remax会为每个组件生成一份模版,然后把这份模版写入每个page页面里。由于小程序本身ViewJs分离,因此在拿到Vnode时,需要通过小程序自身的setData触发小程序渲染。

//page.wxml

<import src="/base.wxml"/>
<template is="REMAX_TPL" data="{{root: root}}" />
<template is="REMAX_TPL" data="{{ root: modalRoot }}" />
// data

{
  "root": {
    "children": [
      4
    ],
    "nodes": {
      "4": {
        "id": 4,
        "type": "view",
        "props": {
          "hover-class": "none",
          "hover-stop-propagation": false,
          "hover-start-time": 50,
          "hover-stay-time": 400
        },
        "children": [
          3
        ],
        "nodes": {
          "3": {
            "id": 3,
            "type": "plain-text",
            "text": "Subpage Test"
          }
        }
      }
    }
  },
  "modalRoot": {
    "children": []
  },
  "__webviewId__": 34
}

Remax在reconciler中是如何运行的

reconciler中的hostConfig

协调阶段开始提交提交阶段提交完成
createInstanceprepareCommitappendChildresetAfterCommit
createTextInstanceappendChildToContainercommitMount
shouldSetTextContentinsertBefore
appendInitialChildinsertInContainerBefore
finalizeInitialChildrenremoveChild
prepareUpdateremoveChildFromContainer
commitTextUpdate
commitUpdate
resetTextContent

这些api提供了React在执行阶段中自定义的能力。

remax中的hostConfig

export default {
	...,

  resetAfterCommit: (container: Container) => {
    container.applyUpdate();
  },

  getChildHostContext: () => {
    return childHostContext;
  },

  createInstance(type: string, newProps: any, container: Container) {
    const id = generate();
    const node = new VNode({
      id,
      type: DOM_TAG_MAP[type] ?? type,
      props: {},
      container,
    });
    node.props = processProps(newProps, node, id);

    return node;
  },

  createTextInstance(text: string, container: Container) {
    const id = generate();
    const node = new VNode({
      id,
      type: TYPE_TEXT,
      props: null,
      container,
    });
    node.text = text;
    return node;
  },

  commitTextUpdate(node: VNode, oldText: string, newText: string) {
    if (oldText !== newText) {
      node.text = newText;
      node.update();
    }
  },

  prepareUpdate(node: VNode, type: string, lastProps: any, nextProps: any) {
    lastProps = processProps(lastProps, node, node.id);
    nextProps = processProps(nextProps, node, node.id);

    return diffProperties(lastProps, nextProps);
  },

  commitUpdate(node: VNode, updatePayload: any, type: string, oldProps: any, newProps: any) {
    node.props = processProps(newProps, node, node.id);
    node.update(updatePayload);
  },
  
	...
};

remax如何React应用到小程序呢?

通过remax的编译后的源码可以知道,remax生成页面的Page对象的方法是createPageConfig,然后createPageConfig中页面onLoad生命周期中,把react注入。

this.container = new Container(this, 'root');
this.modalContainer = new Container(this, 'modalRoot');
const pageElement = React.createElement(PageWrapper, {
  page: this,
  query,
  modalContainer: this.modalContainer,
  ref: this.wrapperRef,
});

if (app && app._mount) {
  this.element = createPortal(pageElement, this.container, this.pageId);
  app._mount(this);
} else {
  this.element = render(pageElement, this.container);
}

其中Container构建起由react reconciler生成的VNode传递给小程序data的工作。

requestUpdate(update: SpliceUpdate | SetUpdate) {
  this.updateQueue.push(update);
} 

applyUpdate() {
  ...
  this.context.setData(updatePayload, () => {
    nativeEffector.run();
    /* istanbul ignore next */
    if (RuntimeOptions.get('debug')) {
      console.log(`setData => 回调时间:${new Date().getTime() - startTime}ms`, updatePayload);
    }
  });

  this.updateQueue = [];
}

requestUpdate负责接收VNode的改变,累积在updateQueue中,applyUpdate负责提交VNode给小程序data,触发页面渲染。

requestUpdateapplyUpdate
commitTextUpdateresetAfterCommit
commitUpdate
appendInitialChild
appendChild
insertBefore
removeChild
appendChildToContainer
insertInContainerBefore
removeChildFromContainer
hideInstance
hideTextInstance
unhideInstance
unhideTextInstance

Remax 和 mpVue 运行模式比较

动态模版

Remax采用动态模版,以Vnode作为更新数据,可以更精准更新数据,实现更纯粹,不需要分析语法重新构建小程序wxml,把React原汁原味使用到小程序中

静态模版

mpvue 的运行时和 Vue 的运行时是强关联的,首先我们来看看 Vue 的运行时。

一个 .vue 的单文件由三部分构成: template, script, style

橙色路径部分, template 会在编译的过程中,在 vue-loader 中通过 ast 进行分析,最终生成一段 render 函数,执行 render 函数会生成虚拟 dom 树,虚拟 DOM 树是对真实 DOM 树的抽象,树中的节点被称作 vnode 。

Vue 拿到 虚拟 DOM 树之后,就可以去和上次老的 虚拟 DOM 树 做 patch diff 对比。patch 阶段之后,vue 就会使用真实的操作 DOM 的方法(比如说 insertBefore , appendChild 之类的),去操作 DOM 结点,更新视图。

同时,绿色路径的部分,在实例化 Vue 的时候,会对数据 data 做响应式的处理,在监测到 data 发生改变时,会调用 render 函数,生成最新的虚拟 DOM 树, 接着对比老的虚拟 DOM 树进行 patch, 找出最小修改代价的 vnode 节点进行修改。

(图片来源于网络)

而 mpvue 的运行时,会首先将 patch 阶段的 DOM 操作相关方法置空,也就是什么都不做。其次, 在创建 Vue 实例的同时,还会偷偷的调用 Page() 用于生成了小程序的 page 实例。然后 运行时的 patch 阶段会直接调用 $updateDataToMp() 方法,这个方法会获取挂在在 page 实例上维护的数据 ,然后通过 setData 方法更新到视图层。

(图片来源于网络)

Remax的限制

原生组件中不支持带有function类型的prop

参考链接

remax

zhuanlan.zhihu.com/p/83324871 zhuanlan.zhihu.com/p/79788488

react

zh-hans.reactjs.org/docs/codeba… github.com/facebook/re… juejin.cn/post/684490…

taro

mp.weixin.qq.com/s?__biz=MzU…

fiber

github.com/acdlite/rea… medium.com/react-in-de… zhuanlan.zhihu.com/p/57346388

小程序

mp.weixin.qq.com/s/3QE3g0Nma…

reconciler

reactjs.org/docs/reconc… www.codementor.io/@manasjayan…