【2万字长文】深刻浅出主流的几款小程序跨端框架原理

目前,小程序在用户规模及商业化方面都取得了极大的成功。微信、支付宝、百度、字节跳动等平台的小程序日活都超过了3亿。javascript

咱们在开发小程序时仍然存在诸多痛点:小程序孱弱简陋的原生开发体验,注定会出现小程序加强型框架,来提高开发者开发体验;各家厂商小程序API碎片化的现状,注定会有多端框架会成为标配,由跨端框架肩负跨平台移植挑战。css

正是由于开发者对于提高小程序开发效率有着强烈需求,小跨端框架发展到现在已经百花齐放、百家争鸣:除了美团的 mpvue 、网易的 megalo 、滴滴的 chameloen 已经趋于稳定,京东的 Taro开始探索 taro next, Hbuilder 的uni-app 产品和生态持续完善,微信新推出了支持H5和微信小程序的 kbone 框架,蚂蚁金服的 remaxhtml

上述的这么多跨端框架纷繁复杂,咱们能够从下面两个维度进行分类:vue

小程序跨端框架的分类

按语法分类

从框架的语法来讲,能够分为下面两类:java

  • Vue 语法node

  • React 语法 / 类 React 语法react

主流的跨端框架基本遵循 React、Vue 语法,这也比较好理解,能够复用现有的技术栈,下降学习成本。ios

remax Taro next Taro 1/2 megalo mpvue uni-app chameloen
语法 react react 类 react (nerve) vue vue vue 类 vue
厂家 蚂蚁金服 京东 京东 网易考拉 美团 Hbuilder 滴滴

按实现原理分类

从实现原理上,开源社区的跨端框架大体分为下面两类:git

  • compile time** 编译时**github

  • runtime** 运行时**

compile time** 编译时**的跨端框架,主要的工做量在编译阶段。他们框架约定了一套本身的 DSL ,在编译打包的过程当中,利用 babel 工具经过 AST 进行转译,生成符合小程序规则的代码。

这种方式容易出现 BUG ,并且开发限制过多。早期的** Taro 1.0 和 2.0** 的版本就是采用的这种方案,下文会有更具体的介绍。

而另一种runtime** 运行时模式**, 跨端框架真正的在小程序的逻辑层中运行起来 React 或者是 Vue 的运行时,而后经过适配层,实现自定义渲染器。这种方式比静态编译有自然的优点,因此 Taro 的最新 Next 版本和 Remax 采用的是这种方案。

写在小程序跨端原理以前

经过上文咱们知道小程序跨端框架目前有不少嘛,各个大厂都会有本身的一套,百花齐放。文章篇幅有限,若是要分别拆开讲清楚他们各家实现的细节,是一件很困难同时很费时间的事情。

因此,下文会尝试梳理一下主流小程序一些共用性的通用实现原理, 尽可能会屏蔽忽略掉各家实现一些细枝末节的细节差别,也不会在文章中贴大段的源码分析,而是经过伪代码来代替。

下面,咱们会从** Vue 跨端框架**和 React 跨端框架两个大方向,进入到小程序跨端原理的世界,讲解这些跨端框架的核心原理,深刻到源码底层去分析,揭开他们神秘的面纱。

Vue 跨端框架

当你使用 megalompvue 这些 Vue 跨端框架时,看上去,咱们写的是vue 的代码,而后打包编译以后就能够运行在小程序内,是否是很神奇?这些框架背后作了哪些事情呢?

实际上,这些** Vue的跨端框架 **核心原理都差很少,都是把 Vue 框架拿过来强行的改了一波,借助了 vue 的能力。好比说,vue 的编译打包流程(也就是vue-loader的能力), vue 的响应式双向绑定、虚拟dom、diff 算法。上面这些东西跨端框架都没有修改,直接哪来用的。

那么哪些部分是这些跨端框架本身新加的东西呢?

涉及到 Vue 框架中操做DOM节点的代码

这些跨端框架,把本来Vue框架中原生 javascript 操做 DOM 的方法,替换成小程序平台的 setData()。为何要这样呢?不着急,下文会有比较详细的讲解。

不着急,慢慢来,咱们先从一个最简单的问题开始。

vue 到 小程序

首先咱们来看,一个 vue 的单文件,究竟作了啥,怎么就能跑在小程序里面了?

咱们知道,对于微信小程序来讲,须要有 4份文件:.wxml.wxss.js.json

(上面是去微信小程序官网截的图)

而对于一个 Vue 组件来讲,一个 vue 文件有三个部分组成:template, script, style

那么,这些跨端框架把Vue 单文件中的 <template><script><style>这三个部分对应的代码,拆一拆,分别处理编译一下,分到 .wxml.wxss.js.json 这 4 份文件中,以下图所示:

咱们分别从<template><script><style>这三个部分来讨论:

  • <style> 部分是最简单的。通常来讲,在 h5 环境中的 css 样式,大部分均可以直接挪到 .wxss,须要处理的部分比较少,除了少部分不支持的属性和 小程序的单位转换
  • <template>** 转换到 **.wxml稍微复杂一点。咱们须要把 h5 的标签啊、vue特殊的语法替换成小程序的标签、小程序特殊的语法。替换的工做咱们称为 模板替换 ,下文会有一个章节用来介绍。
  • 最难的是<script>** 到 **.js, 涉及到 vue 的运行时 如何和 小程序的实例通信的问题,这一部分会用比较多的章节去介绍。

接下来,咱们先看模板替换 ,也就是template 生成 .wxml 文件的过程。

<template>.wxml

Vue 是采用 template 语法的,各大厂商的小程序也是采用了 template 语法。从 Vue 的 template 转变成小程序的 template 相对比较简单,React 的 jsx 转变为小程序的 template 就相对比较棘手啦。

Vue 的 template 与小程序的 template 大致是上的语法很相似,可是仍是有不同的地方。例如小程序里没有 <div> 标签,而是小程序厂商提供的 <view> 标签等等

所以咱们须要把 Vue 模版转换为微信小程序的 .wxml

例如上图所示,<div> 标签须要转换成 <view> 标签,一些 vue 中的语法也须要进行转化成对应小程序平台的语法。

再好比说,在 Vue 里面绑定事件经常使用 @methodName 的语法, 转成小程序模版则须要用 bind,同时用 tap 事件替换 click 事件。

除了这个,还有一些vue的模板语法,也须要转成小程序的模板语法

Vue 和小程序插值表达式则是同样的,采用了双花括号,能够不须要作任何转化

上面展现的这些模板替换,都只是替换为微信小程序的语法。转化为其余小程序平台的语法也是相似的思路,以下图所示:

那么,模板的转化具体是如何实现的呢? 咱们的第一想法是经过正则来匹配,可是要写出匹配出全部状况的正则是很是困难的。

实际上,mpvuemegalouni-app 的框架是采用了 ast 来解析转化模板的。

模板替换过程其实就是两侧对齐语法的过程,把语法不一致的地方改为同样的,是一个 case by case 的过程,只要匹配到不一样状况下语法便可,比较费功夫可是难度系数不是很高。

接下来咱们看如何把 <script> 中的内容,挪到小程序 .js 中呢?

<script>.js

咱们在 .vue 单文件中的 script 部分中, 一般会写下面的代码,咱们会写一个 Vue 的配置项对象传入到 Vue 构造函数中,而后 new Vue() 会实例化出来一个 vue 实例。

new Vue({
  data(){},
  methods: {},
  components: {}  
})
复制代码

上面的代码是彻底能够跑在小程序的逻辑层里面的,只要引入vue 便可,毕竟 Vue 大部分就是纯粹的 javascript。也就是说,小程序的渲染层里面是彻底能够直接运行起来 Vue 的运行时和 React 的运行时的。

可是这样还不够,小程序平台还规定,要在小程序页面中调用 Page() 方法生成一个 page 实例, Page() 方法是小程序官方提供的 API。

在一个小程序的页面中,是必须有 Page() 方法的。微信小程序会在进入一个页面时,扫描该页面中的 .js 文件,若是没有调用 Page() 这个方法,页面会报错。

以下图所示,咱们在 <script> 中写的是 new Vue() 这样子的代码,而微信想要的是 Page()

那么,应该怎么解决呢?

Vue 跨端框架他们拓展了 Vue 的框架,把 Vue2.0 的源码直接拷贝过来,改了里面的初始化方法,在初始化方法中调用了 Page() 方法, 以下面伪代码所示:

new Vue() {};
Vue.init = () => { 
  // 在 vue 初始化的时候,调用了 page() 方法
  Page()
}   
复制代码

在 vue 实例化的时候,会调用 init 方法,在 init 方法里面会调用 Page() 函数,生成一个小程序的 page 实例。

这样,咱们在一个小程序页面中,就会同时存在一个 vue 的实例,和一个小程序的 page 实例,他们是如何融合起来一块儿工做的呢?他们之间是如何作到数据管理的? 如何进行通信的呢?

接下来就涉及到 Vue 框架的核心流程了,为了方便一些不了解 Vue 同窗,同时也为了更好的深刻理解下面讲的内容,接下来会稍微讲一丢丢 vue 的核心流程。

  • ( 真的只有一丢丢 )

Vue 的核心流程

以下图左侧所示,简单来讲, 一个 .vue 的单文件由三部分构成: template, script, style

咱们先看上图中的橙黄色的路径,也就是 template 部分的处理过程。

以下图所示,template 模板部分会在编译打包的过程当中,被 vue-loader 调用 compile 方法经过词法分析生成一个 ast 对象,而后调用代码生成器,通过遍历 AST 树递归的拼接字符串操做,最终生成一段 render 函数, render函数最后会存在打包生成的dist 文件中。

能够看下面这个例子,一段简单的 template 模板以下所示:

<div class="ctl-view" @click="handleClick">
  {{ a }}
</div>
复制代码

通过编译以后,经过 ast 进行分析,生成的 render 函数以下:

_c("div", 
    { staticClass: "ctl-view", on: { click: _vm.handleClick } },
    [_vm._t("default")]
)
复制代码

render 函数会在第一次 mount时,或者Vue 维护的 data 有更新产生的时候会被执行。

那么执行下面这段 render 函数会拿到什么呢?

上面图中蓝色圆圈中的 _c 方法是建立元素类型的vnode, 而 _v 方法是建立 文本类型的vnode。

Render 函数中会调用这些方法建立不一样类型的vnode,最终的产物是生成好的虚拟DOM树 vnode tree,对应上面图中 render 函数的下一个阶段 vnode。

虚拟DOM树是对真实DOM树的抽象,树中的节点被称做 vnodevnode 有一个特色, 它保存了这个DOM节点用到了哪些数据 ,这一点很是重要。

Vue拿到 虚拟dom树以后,就能够去和上次老的虚拟dom树patch diff 对比。

这一步的目的是找出,咱们应该怎么样改动现存的老的DOM树,代价才最小。

patch 阶段以后,若是是运行在浏览器环境中, vue 实例就会使用真实的原生 javascript 操做DOM的方法(好比说 insertBefore , appendChild 之类的),去操做DOM节点,更新浏览器页面的视图。

接下来,咱们再来看一下上面图中,蓝色的线条的路径。

在new Vue 的时候,Vue 在初始化的时候会对数据 data 作响应式的处理,当有数据发生更新时,最后会调用上文的 render 函数,生成最新的虚拟DOM树。

接着对比老的虚拟DOM 树进行 patch, 找出最小修改代价的vnode 节点进行修改。

上面介绍的流程就是 vue 的总体流程啦。

(若是有不理解的地方,不重要,也不须要担忧会阻塞下文的阅读)

咱们要关心的是,下面的类 vue 小程序跨端框架的核心流程。接下来一块儿来看吧。

类 vue 小程序跨端框架的核心流程

在进一步讲解以前,咱们先思考一个问题。上图中,Vue 在 diff 以后就是操做了原生的 DOM 元素,可是各家厂商的小程序不支持原生DOM操做,所以也就没有修改视图节点的能力。那么咱们怎么样才能更新小程序的视图呢?

下面这张图表明了类 vue 小程序跨端框架的核心流程图。

咋一看这张图,会发现和上面Vue的图是很像的。毕竟 megalompvue 等小程序框架,本质都是对 vue 的拓展(copy过来改了改)

仔细和上面的 vue 的核心流程图一对比,咱们发现,小程序跨端框架的流程图替换掉 vue 本来的 DOM 操做,替换为新增的绿色的setData 操做, 同时还多了一个绿色框框中的的 Page() 方法。

Page() 方法上文有介绍过缘由

setData() 是小程序官方提供的 API,用来修改小程序 page 实例上的数据,从而会更新小程序的视图。

『替换掉 vue 本来的 DOM 操做』这一个点比较容易理解,由于小程序容器并无提供操做小程序节点的 API 方法,这是由于小程序隔离了渲染进程 (渲染层)和逻辑进程 (逻辑层),以下图所示:

在小程序容器中,逻辑层到渲染层的更新,只能经过 setData() 来实现。

无论是 mpvuemegalo ,仍是uniapp,这些类 vue 跨端框架,都是经过这种方法来更新视图的。并且,在将来可预见的几年里,只要小程序厂商不提供修改小程序节点的 API 方法,小程序跨端框架更新 DOM 节点仍然会经过 setData 这种 API

好了,到了这一步,咱们已经知道了,跨端框架替换了 Vue 框架中 **JS 操做DOM 原生节点的 API **为 **setData() **来更新小程序的页面。

可是咱们仍是不知道具体背后作了什么,接下来,看一个具体的例子:

new Vue({
  data(){
    return {
      showToggle: true
    }
  }
})


// 下面是通过 模板替换 以后的代码
<view wx:if="{{showToggle}}">
</view>
复制代码

在上面的例子中,showToggle 这个变量表明的数据是维护在Vue 实例上的。

在页面初始化的时候,咱们的小程序跨端框架就开始执行了,它会先实例化一个Vue 实例,而后调用小程序官方的 Page() API 生成了小程序的page 实例,并在在 Vue 的 mounted 中会把数据同步到小程序的 page 实例上。

所以在实际页面打开以后,会同时存在小程序原生的Page 实例和 Vue 实例。vue 实例上有数据(咱们的 data 原本就是定义在 vue 里面的),小程序Page 实例上也有数据(小程序实例上没数据无法渲染页面对吧)。

当 Vue 中的数据发生变化时,会触发 Vue 响应式的逻辑,走 上图中Vue 更新的那一套逻辑:从新执行 render 函数 👉🏻 得出一份最新的 Vnode 树 👉🏻 接下来 Vue去 diff 新旧两个 Vnode 的树,找出修改 DOM 节点最高效的操做。注意!接下来不是调用操做 DOM 的 API, 而是调用小程序的 setData() API 方法, 👉🏻 修改小程序实例上的对应的数据, 从而让小程序渲染层层去更新视图。

这一套流程下来咱们发现,通俗来说,数据是归 Vue 管。 Vue 是一个双向数据绑定的框架,小程序也是一个双向数据绑定的东西,这两个东西放一块,经过跨端框架的运行时来作中间桥接,把数据同步到小程序中。

事件归到小程序容器管,小程序触发各类事件,好比说滚动,事件点击,小程序容器捕获到事件后,会去调用在 Vue 注册的对应的事件处理函数。

上面介绍的模型,是一个通用的Vue 小程序跨端框架的实现。 Vue 的小程序跨端框架基本上思路是一致的。有了这些理解和认识,咱们再来看一下各家小程序框架是如何实现的:

Mpvue 模型

下面是 mpvue 官方网站上的一张原理图:

从右到左来看,当 Vue 上数据变化时,会经过 mpVue 运行时来通知小程序的实例,从而更新小程序 page 视图。从左到右,当小程序的渲染层容器触发了事件后,会经过跨端框架运行时来找到注册的 vue 的事件回调函数

Uni-app 模型

咱们接着来看,下面是 uni-app 的官网的原理图,和上面的图像素级别的类似啊

从右到左来看,当 Vue 上数据变化时,会经过uni-app运行时来通知小程序的实例,从而更新小程序 page 视图。从左到右,当小程序的渲染层容器触发了事件后,会经过跨端框架运行时来找到注册的 vue 的事件回调函数

Megalo 模型

下面是 megao 官方的一张原理图,这两张图和上面看似长的不同,但表达的的意思是同样的。

小结

在这个小节中,重点部分有跨端框架模板替换、 vue 的核心流程、跨端框架替换了 Vue 的 javascript 操做真实 DOM 的 API 等。

至此,一个 vue 跨端框架的核心流程就已经走完了。这个流程中,一些跨端框架会进行优化,不一样的跨端框架会采用不一样的优化策略,下面咱们以网易的 Megalo 为例探讨。

网易考拉 Megalo 的优化

如今,咱们先假设 Vue 中维护的数据小程序中维护的数据如出一辙,数据结构彻底相同。那么当 Vue 中维护的数据发生变化时,直接把**Vue 中维护的数据 **原模原样的同步到小程序中,以下所示,紫色的部分是直接同步过去的数据。

// vue 中维护的数据
new Vue({
  data(){
    return {
      showToggle: true,
      bigObj1: {},
      bigObj2: {}
    }
  }
})


// 小程序中维护的数据
{
      showToggle: true,
      bigObj1: {},
      bigObj2: {}
 }
复制代码

这样有一个问题是,小程序 setData 是有性能问题的,若是频繁地进行调用或者一次型更新大量数据,容易形成页面卡顿。

为了下降更新频率的问题,咱们能够经过加一个截流函数进行限制。

那么怎样减小数据更新的量呢?

上面的代码中, bigObj1bigObj2 是很是很是巨大的对象,可是小程序页面中彻底没有用,那么同步bigObj1bigObj2 对于 setData 来讲就是巨大的浪费。

事实上,不少框架都对此作了优化,咱们来看 megalo 是怎么作的。

假设咱们写的 Vue 实例上的数据是这样的:

而后假设咱们写的 Vue 的 template 是这样写:

上面咱们写的 <template> 模板代码在编译的时候会有两个做用,一方面把模板替换为小程序的标签,另外一方面经过 vue-loader 编译生成 render 函数,以下图所示:

若是咱们仔细观察上面编译出来的 render 函数,会发现 megalo 作了一些手脚,多了一些参数,这些参数是 megalo 本身加上去的标记。

这些标记被 uglify 了,很难懂是什么意思,下面介绍一下:

h_: 是独一无二的 id, 是一个累加的数字。只有当前节点依赖了 data 数据时才有 h_
f_: v-for 循环时的 index
c_: 父亲组件的 id
复制代码

执行上面这段 megalo 生成的 render 函数,一样会生成一个 vnode 树用于 patch。其中的某一些** vnode 节点会稍显特殊,当一个 vnode节点依赖了vue 实例上的 data 数据时,该 vnode 节点** 的 attrs 属性上就会有 h_f_c_ 的值。

以下图所示的那样,只有依赖了 data 数据的那三个节点,才会有 h_f_c_ 的值。

在第一次 mount 的时候,megalo 同步到小程序实例上的数据不是本来 vue 上的数据,那是什么呢?

megalo 会对上面生成的蓝色的 vnode 树的结构进行摘取 + 扁平化的处理,变成一维数组,而后同步到小程序实例上。

如上图所示,先把**用到 data 的 Vnode 节点(上图带有属性的那三个节点)**摘取出来再打平成一维数组。

这样的好处是:当 vue 实例上的数据有不少时,只有那些真正在模板中使用到的数据,才会在编译的阶段被拼接到 render() 函数中,而后出如今生成的 Vnode 树上(在编译阶段作的事情),以后才有资格被 megalo 摘出来扁平化压缩成一维数组,最后同步到小程序实例上。

那些多余的没有用的数据,是永远不会出如今Vnode 树上的。

为啥要扁平化为一维数组呢?

这是由于 Vnode 层级关系多是很深很复杂的,若是把这种复杂的层级关系也维护在小程序实例中,会比较麻烦。

小程序 setData 方法支持传入一个对象,对象的 key 是能够有层次关系的,好比说下面代码中的 0 表示父组件的 id, 1 表示节点上的id

setData({ '$root.0.h.1.t', 'a' })
复制代码

若是咱们把节点打平,标识一个惟一的 id, 就能够只维护一个扁平化的一维数组。

这样,当 Vue 的 Vnode tree 上某一个 Vnode 节点发生了变更时,咱们须要同步更新小程序上的数据,不须要关心那个 Vnode 节点在树上的哪一个位置,只须要知道那个节点上的id ——也就是** attrs.h_ 值和父组件的 id ——也就是 attrs.c_** 值 ,而后拼出上面 setData 的 key 路径——$root.0.h.1.t,就能够精准且方便修改小程序实例上的数据了

计算 vnode 节点上的id 是经过 getHid(), 计算父组件的 id 是经过 getVMId() 方法

getHid()

getHid() 方法本质上是返回节点上的** attrs.h_ **的值, 上面说过了 **h_ **是独一无二的 id, 是一个累加的数字。若是模板中有 v-if 循环的话,则返回 attrs.h_ + '-' + attrs.f_ 的值。

getVMId()

getVMId() 方法本质上是返回节点上的 attrs.c_ 的值,也就是所在的组件的 id。 Vue 中组件是有层级关系的,在小程序中数据被打平了怎么表示究竟是修改哪一层的组件的数据呢?

Megalo 经过拼接v 字符来表示组件的层级

0v0 表示是第二层组件,也就是 Vnode 中第一个组件中的第一个子组件

0v1 也表示是第二层组件,也就是 Vnode 中第一个组件中的第二个子组件

0v0v0 表示是第三层组件,也就是 Vnode 中第一个组件中的第一个子组件中的第一个孙子组件

只要能算出正确的相似如$root.0.h.1.t 的 key 路径,咱们就能够正确的把 Vnode 树,更新到微信小程序上。

模板改造

固然,模板部分也要配合上面扁平化的修改,差值的 `{{}}` 中,应该在编译的时候替换为适配扁平化的数据

小结

总结一下,Megalo 小程序实例上数据,既不是 vue 实例上原模原样的数据,也不是 vue 生成那颗 Vnode 树,而是 Megao 从 Vnode 树上摘取后再通过扁平化压缩后获得的数据结构。这样能够带来性能上的提高。

类 React 跨端框架

类 React 框架存在一个最棘手的问题: 如何把灵活的 jsx 和动态的 react 语法转为静态的小程序模板语法。

为了解决问题,不一样的的团队实践了不一样的方案,大致上能够把全部的类 React 框架分类两类:

  • 静态编译型。 表明有:京东的 Taro 1/2 , 去哪儿的 Nanachi,淘宝的rax
  • 运行时型。表明有: 京东的 Taro Next ,蚂蚁的 remax

静态编译型小程序框架

所谓静态编译,就是上面说的这些框架会把用户写的业务代码解析成 AST 树,而后经过语法分析强行把用户写的类 react 的代码转换成可运行的小程序代码。

以下图所示的Taro 1版本或者2版本的逻辑图,整个跨端的核心逻辑是落在编译过程当中的抽象语法树转化中作的。

Taro 1/2 在编译的时候,使用 babel-parser 将 Taro 代码解析成抽象语法树,而后经过 babel-types 对抽象语法树进行一系列修改、转换操做,最后再经过 babel-generate 生成对应的目标代码。

有过 Babel 插件开发经验的同窗应该对上面流程十分熟悉了,无非就是调用 babel 提供的 API 匹配不一样的状况,而后修改 AST 树。

下面咱们来举一个例子,若是咱们使用 Taro 1/2 框架来写小程序页面组件,极可能是长成下面这样:

能够看到上面组件很是像一个 React 组件,你须要定义一个 Componentrender 方法,而且须要返回一段 JSX

这段的代码,会在 Taro1/2 编译打包的时候,被框架编译成小程序代码。具体来讲, render 方法中的 JSX 会被提取出来,通过一系列的重重转换,转换成小程序的静态模板,其余 JS 的部分则会保留成为小程序页面的定义,以下图所示:

这听上去是一件很美好的事情,可是现实很骨感,为啥呢?

JSX 的语法过于灵活。

JSX 的灵活是一个双刃剑,它可让咱们写出很是复杂灵活的组件,可是也增长了编译阶段框架去分析和优化的难度。

你在使用 JavaScript 的时候,编译器不可能hold住全部可能发生的事情,由于 JavaScript 太过于动态化。你想用静态的方式去分析它是很是复杂一件事情,咱们只要稍微在上面的图中例子中加入一点动态的写法,这些框架就可能编译失败。

虽然这块不少框架已经作了不少尝试,但从本质上来讲,框架很难经过这种方式对其提供安全的优化。

这也是 React 团队花了3 年的时候搞出来 fiber 的意义, React 的优化方案并非在编译时优化,而是在运行时经过时间分片不阻塞用户的操做让页面感受快起来。

因此,React 解决不了的问题,这些小程序跨端框架一样也解决不了。

他们都会告诉开发者要去避免不少的动态写法。好比说 Taro 1 /2 版本的文档里面就给出了很是清晰的提示

Taro 1/2 的弯路

Taro 发展到了2019年,他们终于意识到了上面问题的紧迫性: JSX 适配工做量大,很难追上 react 的更新。

这些问题归根到底,很大一部分是 Taro 1/2 的架构问题。Taro 1/2 用 穷举 的方式对 JSX 可能的写法进行了一一适配,这一部分工做量很大,彻底就是堆人力去适配 jsx ,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各类写法

于此同时,蚂蚁金服的@边柳在第三届 SEE Conf 介绍了 Remax ,走了不一样于静态编译的一条路,推广的口号是 『使用真正的 React 来构建小程序』。由于 Taro 1/2是假的 React,只是在开发时遵循了 React 的语法,在代码编译以后实际运行时的和 React 并无半毛钱关系,所以也无法支持 React 最新的特性。

Taro 团队从活跃的社区中受到了启发,彻底重写了 Taro 的架构,带来了 Taro Next 版本。

接下来,咱们会一点点揭开 React 运行时跨端框架的面纱。Taro NextRemax 原理类似,Remax 已经比较稳定了,下面会着重讲解 Remax 的原理,Taro Next 放在最后做为比较。

你须要对 React 的基本原理有必定的了解。

React 前置知识

在深刻阅读本文以前,先要确保你可以理解如下几个基本概念:

Element

经过 JSX 或者 React.createElement 来建立 Element,好比:

<button class='button button-blue'>
  <b>
    OK!
  </b>
</button>
复制代码

JSX 会被转义译为:

React.createElement(
  "button",
  { class: 'button button-blue' },
  React.createElement("b", null, "OK!"))
复制代码

React.createElement 最终构建出相似这样的对象:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}
复制代码

**Reconciler 调和器 **& Renderer 渲染器

React 16版本带来了全新的 fiber 的架构,代码拆分也很是清晰,大致上能够拆分红这三大块:

  • React component API 代码量比较少

  • Reconciler 调和器 代码量很是大,是fiber 调度的核心

  • **Renderer 渲染器,**负责具体到某一个平台的渲染,最多见的 ReactDOM 就是 web 浏览器平台的自定义渲染器

ReconcilerRenderer 的关系能够经过下图缕清楚

  • Reconciler 调和器的职责是负责 React 的调度和更新,内部实现了 Diff/Fiber 算法,决定何时更新、以及要更新什么。

  • **Renderer自定义渲染器,**负责具体到哪个平台的渲染工做,它会提供宿主组件、处理事件等等。

Renderer 自定义渲染器里面定义了一堆方法,是提供给 React 的 reconciler 使用的。React 的 reconciler 会调用渲染器中的定义一系列方法来更新最后的页面。

咱们接下来会重点介绍Renderer自定义渲染器, 暂且先无论 Reconciler 调和器 ,就先认为它是一个React 提供的黑盒。这个黑盒里面帮咱们作了时间分片、任务的优先级调度和 fiber 节点 diff 巴拉巴拉一系列的是事情,咱们都不关心。咱们只须要知道 Reconcier 调和器在作完 current fiber tree 和 workIn progress fiber tree 的 diff 工做后,收集到 effects 准备 commit 到真实的 DOM 节点,是调用了的自定义渲染器中提供的方法。

若是在自定义渲染器中,你调用了操做 WEB 浏览器 web DOM的方法,诸如咱们很熟悉的 createElementappendhild,那么就建立/更新浏览器中的 web 页面;若是渲染器中你调用了iOS UI Kit API,那么则更新 ios ,若是渲染器中调用了 Android UI API, 则更新 Android。

Renderer 自定义渲染器有不少种,咱们最多见的ReactDOM就是一个渲染器,不一样的平台有不一样的 React 的渲染器,其余还有不少有意思的自定义渲染器,可让 React 用在TV 上,Vr 设备上等等,能够点击这个连接进行了解: github.com/chentsulin/…

事实上,Remax 和 Taro Next 至关因而本身实现了一套能够在 React 中用的,且能渲染到小程序页面的自定义渲染器。

总结来讲,React 核心调度工做是在 Reconciler 中完成;『画』到具体的平台上,是自定义渲染器的工做。

关于React 渲染器的基本原理,若是对这个话题感兴趣的同窗推荐观看前 React Team 成员 Sophie Alpert 在 React Conf 上分享的《Building a Custom React Renderer》,也特别推荐这个系列的文章 Beginners guide to Custom React Renderers,讲解的比较细致

Fiber 架构的两个阶段

React 16 版本Fiber 架构以后,更新过程被分为两个阶段:

  • 协调阶段(Reconciliation Phase) 这个阶段 Reconciler 调度器会根据事件切片,按照任务的优先级来调度任务,最终会找出须要更新的节点。协调阶段是能够被打断的,好比有优先级更高的事件要处理时。

  • 提交阶段(Commit Phase) 将协调阶段计算出来的须要处理的**反作用(Effects)**一次性执行,也就是把须要作出的更改,一会儿应用到 dom 节点上,去修改真实的 DOM 节点。这个阶段必须同步执行,不能被打断

这两个阶段按照render为界,能够将生命周期函数按照两个阶段进行划分:

  • 协调阶段
    • constructor
    • componentWillMount 废弃
    • componentWillReceiveProps 废弃
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • componentWillUpdate 废弃
    • render
    • getSnapshotBeforeUpdate()
  • 提交阶段
    • componentDidMount

    • componentDidUpdate

    • componentWillUnmount

自定义渲染器 Rerender

建立一个自定义渲染器只需两步:

  1. 宿主配置HostConfig,也就是下图中绿色方框 HostConfig 的配置

  2. 实现渲染函数,相似于 ReactDOM.render() 方法

宿主配置 HostConfig,这是react-reconciler要求宿主平台提供的一些适配器方法和配置项。这些配置项定义了如何建立节点实例、构建节点树、提交和更新等操做。下文会详细介绍这些配置项

const Reconciler = require('react-reconciler');
const HostConfig = {
  // ... 实现适配器方法和配置项
};
复制代码

渲染函数就比较套路了,相似于 ReactDOM.render() 方法,本质就是调用了 ReactReconcilerInst 的两个方法 createContainerupdateContainer

// 建立Reconciler实例, 并将HostConfig传递给Reconcilerconst 


MyRenderer = Reconciler(HostConfig);  


// 假设和ReactDOM同样,接收三个参数 
// render(<MyComponent />, container, () => console.log('rendered')) 


export function render(element, container, callback) {  // 建立根容器  
if (!container._rootContainer) {    container._rootContainer = ReactReconcilerInst.createContainer(container, false);  }   // 更新根容器  
return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback); 
}
复制代码

容器既是 React 组件树挂载的目标(例如 ReactDOM 咱们一般会挂载到 #root 元素,#root 就是一个容器)、也是组件树的 根Fiber节点(FiberRoot)。根节点是整个组件树的入口,它将会被 Reconciler 用来保存一些信息,以及管理全部节点的更新和渲染。

Remax 的自定义渲染器

HostConfig 支持很是多的参数,这些参数很是多,并且处于 API 不稳定的状态,你们稍微了解一下便可,不用深究。另外,没有详细的文档,你须要查看源代码或者其余渲染器实现。

若是感兴趣的同窗能够移步这篇文章 react 渲染器了解一下?。常见配置能够按照下面的阶段来划分:

经过上面代码,咱们能够知道 HostConfig 配置比较丰富,涉及节点操做、挂载、更新、调度、以及各类生命周期钩子, Reconciler 会在不一样的阶段调用配置方法。好比说在协调阶段会新建节点,在提交阶段会修改子节点的关系。

为了思路清晰,咱们按照 【协调阶段】——【提交阶段】—— 【提交完成】这三个阶段来看,咱们接下来先看一下协调阶段。

协调阶段

在协调阶段, Reconciler 会调用 HostConfig 配置里面的 createInstancecreateTextInstance 来建立节点。咱们接下俩看看 Remax 源码是怎么样子的

const HostConfig = {
  // 建立宿主组件实例
  createInstance(type: string, newProps: any, container: Container) {
    const id = generate();
    // 预处理props, remax会对事件类型Props进行一些特殊处理
    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;
  },
}
复制代码

你们能够回想一下,若是是本来的 ReactDOM 中的话,上面两个方法应该是经过 javascript 原生的 API document.createElementdocument.createTextNode 来建立浏览器环境的中的DOM节点

由于在小程序的环境中,咱们没有办法操做小程序的原生节点,因此Remax 在这里,不是直接去改变 DOM,而建立了本身的 VNode 节点。

你可能会感到惊讶,还能这样玩,不是说好要操做平台的节点嘛,这样不会报错吗?

缘由是,React 的 Reconciler 调和器在调度更新时,不关心 hostConifg 里你新建的一个节点究竟是啥,也不会改写你在 hostConifg 中定义的节点属性。

因此自定义渲染器Renderer中一个节点能够是一个 DOM 节点,也能够是本身定义的一个普通 javascript 对象,也能够是 VR 设备上的一个元素。

总而言之,React 的 Reconciler 调度器并不关心自定义渲染器 Renderer 中的节点是什么形状的,只会把这个节点透传到 hostConfig 中定义的其余方法中,好比说 appendChildremoveChildinsertBefore 这些方法中。

上面 Remax 的代码中建立了本身的 VNode 节点, VNode 的基本结构以下:

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;
}
复制代码

友情提示,这里的 VNode 是 Remax 中本身搞出来的一个对象,和 React 或者 Vue 中的 virtual dom 没有半毛钱的关系

能够看到,VNode 其实经过 childrenparent 组成了一个树状结构,咱们把它称为一颗镜像树(Mirror Tree),这颗镜像树最终会渲染成小程序的界面。 VNode** 就是镜像树中的**虚拟节点,主要用于保存一些节点信息。

因此, Remax在 HostConfig 配置的方法中,并无真正的操做 DOM 节点,而是先构成一颗镜像树(Mirror Tree), 而后再同步到渲染进程中,以下图绿色的方框所示的那样,咱们会使用 React 构成一个镜像树的 Vnode Tree,而后交给小程序平台把这个树给渲染出来。

提交阶段

提交阶段也就是 commit 阶段,react 会把 effect list 中存在的变动同步到渲染环境的 DOM 节点上去,会分别调用 appendChildremoveChildinsertBefore 这些方法

const HostConfig = {


  // 用于初始化(首次)时添加子节点
  appendInitialChild: (parent: VNode, child: VNode) => {
    parent.appendChild(child, false);
  },


  // 添加子节点
  appendChild(parent: VNode, child: VNode) {
    parent.appendChild(child, false);
  },


  // 插入子节点
  insertBefore(parent: VNode, child: VNode, beforeChild: VNode) {
    parent.insertBefore(child, beforeChild, false);
  },


  // 删除节点
  removeChild(parent: VNode, child: VNode) {
    parent.removeChild(child, false);
  },


  // 添加节点到容器节点,通常状况咱们不须要和appendChild特殊区分
  appendChildToContainer(container: any, child: VNode) {
    container.appendChild(child);
    child.mounted = true;
  },


  // 插入节点到容器节点
  insertInContainerBefore(container: any, child: VNode, beforeChild: VNode) {
    container.insertBefore(child, beforeChild);
  },


  // 从容器节点移除节点
  removeChildFromContainer(container: any, child: VNode) {
    container.removeChild(child);
  },
}
复制代码

下面咱们看,Remax 源码里面到底是如何实现这些方法的。

appendChild

若是是原生的浏览器环境中,appendChild 比较简单,直接调用 javascript 原生操做 DOM 的方法便可。若是是小程序的环境中,你得本身实现 hostConfig 中定义的 VNode 节点上的 appendChild 的方法,源码实现以下:

VNode.prototype.appendChild = function (node) {
  // 把 node 挂载到 child 链表上 
  // firstChild指针指向链表的开头
  // lastChild 指针指向链表的结尾
  if (!this.firstChild) {
    this.firstChild = node;
  }


  if (this.lastChild) {
    this.lastChild.nextSibling = node;
    node.previousSibling = this.lastChild;
  }


  this.lastChild = node;


  // 若是节点已经挂载了,则调用 requestUpdate 方法,传入一些参数
  if (this.isMounted()) {
    this.container.requestUpdate({
      type: 'splice',
      path: this.path,
      start: node.index,
      id: node.id,
      deleteCount: 0,
      children: this.children,
      items: [node.toJSON()],
      node: this
    });
  }
};
复制代码

上面代码中,并无直接操做小程序的 DOM ,而是操做存内存中的 VNode 组成的镜像树:

  1. 把入参 node 挂载到 child 链表上 ;
  2. 最后调用了 requestUpdate 这个方法,下面会有详细的讲到。

removeChild

removeChild 方法和上面是同一个套路,先是修改了 VNode 镜像树上的节点关系,而后调用了 requestUpdate 这个方法

insertBefore

insertBefore 方法和上面是同一个套路,先是修改了 VNode 镜像树上的节点关系,而后调用了 requestUpdate 这个方法

上面介绍的这些方法,都是对节点位置关系的更新,好比说子节点位置的移动啊之类的。

现实中确定也会有一些更新是不涉及到节点移动,而是好比说,节点上的属性发生了变化、节点的文本发生了变化,Reconciler 就会在协调阶段调用下面的这些方法。

commitUpdate

commitUpdate: function (node, updatePayload, type, oldProps, newProps) {
    // 处理一下 props
    node.props = processProps(newProps, node, node.id);
    node.update(updatePayload);
},
复制代码

上面调用了 node.update 方法,定义以下

VNode.prototype.update = function (payload) {
    if (this.type === 'text' || !payload) {
        this.container.requestUpdate({
            type: 'splice',
            // root 不会更新,因此确定有 parent
            path: this.parent.path,
            start: this.index,
            id: this.id,
            deleteCount: 1,
            items: [this.toJSON()],
            node: this,
        });
        return;
    }
    for (var i = 0; i < payload.length; i = i + 2) {
        var _a = __read(toRawProps(payload[i], payload[i + 1], this.type), 2), propName = _a[0], propValue = _a[1];
        var path = __spread(this.parent.path, ['nodes', this.id.toString(), 'props']);
        if (RuntimeOptions.get('platform') === 'ali') {
            path = __spread(this.parent.path, ["children[" + this.index + "].props"]);
        }
        this.container.requestUpdate({
            type: 'set',
            path: path,
            name: propName,
            value: propValue,
            node: this,
        });
    }
};
复制代码

真神奇鸭,最后仍是调用了 requestUpdate 方法,异曲同工的感受。

上面的方法中,最后都调用了神奇的 requestUpdate 方法,咱们看一下这个方法里面作了什么

requestUpdate

requestUpdate 方法定义以下:

没想到吧, 这个requestUpdate方法那么简单。

  1. 接受一个对象做为参数

  2. 而后把接收的参数 update 推入到 this.updateQueue 这个数组里面,暂存起来,以后会在【提交完成阶段】派上大用场。

提交完成阶段

在这个阶段以前,Remax 构成的 VNode镜像树的这个JSON 数据仍是在 Remax 世界中被管理和维护,接下来,咱们会看如何更新 小程序的世界中。

React 会在提交完成阶段执行 hostConfig 中定义的 resetAfterCommit 方法,这个方法本来是用React 想来作一些善后的工做。可是Remax 在这个resetAfterCommit 方法作了一个及其重要的工做,那就是同步镜像树到小程序**** data

接下来咱们来看 resetAfterCommit 方法的源码

resetAfterCommit: function resetAfterCommit(container) {
    container.applyUpdate();
  },
复制代码
AppContainer.prototype.applyUpdate = function () {
  this.context._pages.forEach(function (page) {
    page.container.applyUpdate();
  });
};
复制代码
Container.prototype.applyUpdate = function () {
    // 省略了 其余的逻辑
   
    var updatePayload = this.updateQueue.reduce(function (acc, update) {
         //  经过以前缓存的updateQueue 计算出来 updatePayload
        return acc;
    }, {});
    
    // 小程序的setData 终于出现了!!!! 把 updatePayload 同步到小程序的逻辑层
    this.context.setData(updatePayload, function () {
        nativeEffector.run();
    });
    this.updateQueue = [];
};
复制代码

上面代码的意思是, **经过以前缓存的updateQueue 计算出来 updatePayload, **updatePayload 是一个什么东东呢?咱们能够经过 debug 断点来一览它的风采。

在某一次更新以后的断点:

updatePayload 是一个 javascript 的对象,对象的 key 是数据在小程序世界中的路径,对象的 value 就是要更新的值。

小程序的 setData 是支持这样的写法: setData({ root.a.b.c: 10 }), key 能够表达层次关系

在第一次 mount 时的断点:

咱们能够在开发者工具中看到小程序实例上的数据,大概长下面这个样子。

{
  "root": {
    "children": [
      7
    ],
    "nodes": {
      "7": {
        "id": 7,
        "type": "view",
        "props": {
          "class": "app___2lhPP",
          "hover-class": "none",
          "hover-stop-propagation": false,
          "hover-start-time": 50,
          "hover-stay-time": 400
        },
        "children": [
          4,
          6
        ],
        "nodes": {
          "4": {
            "id": 4,
            "type": "button",
            "props": {
              "bindtap": "$$REMAX_METHOD_4_onClick",
              "hover-class": "button-hover",
              "hover-start-time": 20,
              "hover-stay-time": 70
            },
            "children": [
              3
            ],
            "nodes": {
              "3": {
                "id": 3,
                "type": "plain-text",
                "text": " click me"
              }
            }
          },
          "6": {
            "id": 6,
            "type": "view",
            "props": {
              "hover-class": "none",
              "hover-stop-propagation": false,
              "hover-start-time": 50,
              "hover-stay-time": 400
            },
            "children": [
              5
            ],
            "nodes": {
              "5": {
                "id": 5,
                "type": "plain-text",
                "text": ""
              }
            }
          }
        }
      }
    }
  },
  "modalRoot": {
    "children": []
  },
  "__webviewId__": 31
}
复制代码

在第一次 mount 时,Remax** 运行时**初始化时会经过小程序的 setData 初始化小程序的 JSON 树状数据

而后,Remax** 运行时在数据发生更新时,就会经过小程序的 setData更新**上面小程序的 JSON 树状数据

那么,剩下最后一个问题,如今咱们知道了,小程序实例上有了一个 JSON 的树状对象,如何渲染成小程序的页面呢?

从 JSON 数据到小程序渲染

若是在浏览器环境下,这个问题很是简单,JavaScript 能够直接建立 DOM 节点,只要咱们实现使用递归,即可完成从 VNodeDOM 的还原,渲染代码以下:

function render(vnode) {
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode)
  }


  const props = Object.entries(vnode.props)
  const element = document.createElement(vnode.type)


  for (const [key, value] of props) {
    element.setAttribute(key, value)
  }


  vnode.children.forEach((node) => {
    element.appendChild(render(node))
  })


  return element
}
复制代码

但在小程序环境中,不支持直接建立 DOM ,仅支持模板渲染,该如何处理?

上文中,咱们讲到类 Vue 的小程序框架的模板是从 Vue 的 template 部分转成的;

类 React 的运行时小程序框架,jsx 很难转成模板,只有一个 Vnode 节点组成的镜像树。

若是咱们去看 Remax 打包以后的模板代码,也会发现空空如也,只有三行代码,第一行引用了一个 **base.wxml **文件,第二行是一个叫 REMAX_TPL 的模板

<template is="REMAX_TPL" data={{root: root}}>  </template>
复制代码

第二行代码表示使用 REMAX_TPL 模板,传入的数据是 root, root 是小程序实例上维护的数据,就是上面咱们提到的小程序的 JSON 树状数据,每个节点上保存了一些信息,以下所示:

{
 "root": {
 "children": [
  7
 ],
  "nodes": {
  "7": {
   "id": 7,
    "type": "view",
    "props": {
    "class": "app___2lhPP",
     "hover-class": "none",
     "hover-stop-propagation": false,
     "hover-start-time": 50,
     "hover-stay-time": 400
   },
   "children": [
    4,
    6
   ],
    "nodes": {
    "4": {
     "id": 4,
      "type": "button",
      "props": {
      "bindtap": "$$REMAX_METHOD_4_onClick",
       "hover-class": "button-hover",
       "hover-start-time": 20,
       "hover-stay-time": 70
     },
     "children": [
      3
     ],
      "nodes": {
      "3": {
       "id": 3,
        "type": "plain-text",
        "text": " click me"
      }
     }
    },
    "6": {
     "id": 6,
      "type": "view",
      "props": {
      "hover-class": "none",
       "hover-stop-propagation": false,
       "hover-start-time": 50,
       "hover-stay-time": 400
     },
     "children": [
      5
     ],
      "nodes": {
      "5": {
       "id": 5,
        "type": "plain-text",
        "text": "sss"
      }
     }
    }
   }
  }
 }
},
 "modalRoot": {
 "children": []
},
 "__webviewId__": 2
}
复制代码

咱们来看 base.wxml 里面是什么内容,发现 base.wxml 内容超级多,有3000多行。以下图:

这个 base.wxml 文件是固定的,每一次打包都会生成那么代码,代码中定义了好几种的小程序的 template 类型,而后重复定义了好几遍,只是 name 名字的值不一样。这是为了兼容某一些小程序平台不容许 <template> 组件本身嵌套本身,用来模拟递归嵌套的。

咱们回到刚才的那一行代码,有一个名字是 REMAX_TPL 的模板组件。

<template is="REMAX_TPL" data={{root: root}}>  </template>
复制代码

REMAX_TPL 的模板组件定义在base.wxml 里面,以下所示:

<template name="REMAX_TPL">
 <block wx:for="{{root.children}}" wx:key="*this">
  <template is="REMAX_TPL_1_CONTAINER" data="{{i: root.nodes[item], a: ''}}"/>
 </block>
</template>


<template name="REMAX_TPL_1_CONTAINER" data="{{i: i}}">
 <template is="{{_h.tid(i.type, a)}}" data="{{i: i, a: a + ',' + i.type, tid: 1}}"/>
</template>
复制代码

上面代码,首先遍历了 root 数据中的 children 数组,遍历到每一项的话,用名字是 REMAX_TPL_1_CONTAINER 的模板组件继续渲染数据中的 root.[item] 属性

REMAX_TPL_1_CONTAINER 的模板组件的定义,实际上是用当前数据的节点的类型——也就是调用 _h.tid(i.type, a) 方法来算出节点类型,多是 text, button ——找到节点类型对应的 template 模板,再次递归的遍历下去。

_h.tid 的方法定义以下,其实就是拼接了两个值: 1. 递归的深度deep的值,2.** 节点的 type**

tid = function (type, ancestor) {
var items = ancestor.split(',');
var depth = 1;


for (var i = 0; i< items.length; i++) {
if (type === items[i]) {
depth = depth + 1;
}
}


var id = 'REMAX_TPL_' + depth + '_' + type;
return id;
}
复制代码

能够看到,Remax 会根据每一个子元素的类型选择对应的模板来渲染子元素,而后在每一个模板中又会去遍历当前元素的子元素,以此把整个节点树递归遍历出来。

总结一下:

在第一次 mount 时,Remax 运行时初始化时会经过小程序的 setData 初始化小程序的 JSON 树状数据, 在小程序加载完毕后, Remax 经过递归模板的形式,把JSON 树状数据渲染为小程序的页面,用户就能够看到页面啦。

而后,Remax 运行时在数据发生更新时,就会经过小程序的 setData更新上面小程序的 JSON 树状数据, JSON 树状数据被更新了,小程序天然会触发更新数据对应的那块视图的渲染。

Remax 创造性的用递归模板的方式,用相对静态的小程序模板语言实现了动态的模板渲染的特性。

总结

看到这里,咱们已经对 remax 这种类 react 的跨端框架总体流程有了大概的了解

Taro Next 的实现原理

Taro Next 的原理和 Remax 是很像的,这里我就偷懒一下,直接把 Taro 团队在 GMTC大会上的 ppt 贴过来了,高清版本的 ppt 能够点击这个连接下载:程帅-小程序跨框架开发的探索与实践-GMTC 终稿.pdf

下面发现和 remax 是很像的。

Taro 团队实现了 taro-react 包,用来链接 react-reconcilertaro-runtime 的 BOM/DOM API

Taro-react 就作了两件事情:

  1. 实现 hostConfig 配置,咱们上面已经介绍过了

  2. 实现 render 函数(相似于 ReactDOM.render)方法,咱们上面也已经介绍过了

在更新的过程当中,一样是在 appendChild、 insertBefore、removeChild 这些方法里面调用了 enqueueUpdate 方法(人家 remax 叫updateQueue)

渲染的话,和 Remax 的作法同样,基于组件的 template 动态 “递归” 渲染整棵树。

具体流程为先去遍历 Taro DOM Tree ( 对应 Remax 中叫镜像树 )根节点的子元素,再根据每一个子元素的类型选择对应的模板来渲染子元素,而后在每一个模板中又会去遍历当前元素的子元素,以此把整个节点树递归遍历出来。

基本上和 remax 同样,换汤不换药。

参考资料

Taor 1/2 官方文档中关于 JSX 支持程度补充说明

用Vue.js开发微信小程序:开源框架mpvue解析

本身写个React渲染器: 以 Remax 为例(用React写小程序)

Remax 官网的原理分析

「2019 JSConf.Asia - 尤雨溪」在框架设计中寻求平衡

知乎 Remax 开发博客

一块儿脱去小程序的外套 - 微信小程序架构解析

Beginners guide to Custom React Renderers

Remax - 使用真正的 React 构建小程序

react 渲染器了解一下?

Taro Next 架构揭秘 | GMTC《小程序跨框架开发的探索与实践》万字无删减

Sophie Alpert 在 React Conf 上分享的《Building a Custom React Renderer》

相关文章
相关标签/搜索