一 前言
React 在跨端动态化方向上有着得天独厚的优势,这和 React 本身的设计是密不可分的,目前市面上有很多成熟的方案,比如:
1 React Native 是一个经典动态化方案。React Native 逻辑层是由 JS 处理的,而渲染是由 Native 完成的,这使得 React Native 能够保持 React 开发特性,同样又有原生渲染的性能。
2 Taro React 也是一个不错的跨端解决方案,Taro 3 由重编译,轻运行时,变成了轻编译,重运行时,这使得 React 运行时的代码能够真正的运行到由 Taro 构建的 web 或者小程序应用中。
3 目前很多大厂自己也有一套以 React 为 DSL 的动态化框架,它们有一个共性就是,保持 JSX 灵活的语法特性,不仅如此,还可以保持 React 的活性,也就是说,同时保持了和 React 一样的 api 设计和语法规范。
如上是 React 在跨端领域落地的方案,为什么 React 在跨端领域这么受欢迎呢?接下来我们具体展开说说。
二 React 技术核心优势分析
1 数据驱动模型
React 和 Redux 的数据通信架构模型都和 Flux 架构类似,我们先来看看 Flux ,Flux 是 Facebook 提出的一种前端应用架构模式,它本身并不是一个 UI 框架,而是一种以单向数据流为核心思想的设计理念。
在 Flux 思想中有三个组成部分,那就是 dispatcher,store,和 view。下面来看一下三者的职责。
- dispatcher: 其中更改数据,分发事件,就是由 dispatcher 来实现的。
- store : store 为数据层,负责保存数据,并且相应事件,更新数据源。
- view :view 层可以订阅更新,当数据发生更新的时候,负责通知视图重新渲染 UI。
在 React 应用中,setState 为更改视图的工具,state 为数据层,view 为视图层,如果想要更改视图,那么通过 setState 改变 state ,在重新渲染组件得到新的 element ,接下来交给浏览器渲染就可以了。
这种数据驱动模式一定程度上,并不受到平台的影响,或者说受到平台影响较小,因为在整个数据流过程中,从 setState 的触发,到 state 改变,再到组件 render 得到 element,接下来形成新的虚拟 DOM ,都是在 js 层完成的,而最后涉及到渲染绘制的时候,在通过不同的平台做差异化处理就可以了,比如在 web 交给浏览器去绘制,在移动端交给 Native 去绘制。
2 从 JSX 到虚拟 DOM
JSX 的灵活性也是 React 在跨端领域备受欢迎的原因之一,JSX 会被 babel 编译成 React element 对象形式,也就是当我们在组件中写了一个页面结构,但是本质上就是一个对象结构。比如我们在页面中这么写:
/* 子组件 */
function Children(){
return <div>子组件</div>
}
/* 父组件 */
function Index(){
const element = <div>
<p> hello,React </p>
<Children />
</div>
console.log(element,'element')
return element
}
我们先来看看经过 babel 和 createElement 处理后,会变成什么样子:
通过上面可以看到当 Index 渲染的时候,会把当前组件视图层所有元素转换成 element 对象,最终形成 element 对象结构如下所示:
相比 template 模板形式,JSX 的优势非常明显:
第一个就是通过 JSX 方式,一些设计模式会变得非常灵活,比如组合模式,render props 模式,这些模式的本质就是更灵活的组装 element 对象。在 template 模式中,组合模式需要通过 slot 插槽来实现,如果多个 slot 嵌套的,会让 template 结构变得非常复杂,难以理解。
第二个原因就是 JSX 语法本质上就是 JS ,所以对于写法非常灵活,包括在视图层写判断,循环,抽象状态等,而 template 一般都会有写法的限制,比如 vue 中,必须遵循 vue 的模版逻辑,在微信小程序中亦是如此。
第三个就是 JSX 对于数据的预处理能力非常好,开发者可以在 render 函数中,对于数据进行格式化处理,比如一些来源于父组件的 props ,把数据处理成视图层需要的结构,再进行渲染。在 vue 可能会用过滤器或者计算属性解决这个问题。但是在小程序中会变得很棘手,有一些数据的处理,比如依赖小程序通过监听器的方式,对数据进行处理,然后通过 setData 的方式,将 props 中的数据,映射到 data 中,这无疑是一种性能上的浪费。
在 web 领域,因为驱动视图都是由 js 来完成的,所以少了很多通信成本,但是在跨端领域就不是这样子了,跨端无论是 webview 渲染还是 native 渲染都有一定的通信成本。
webview 渲染得益于微信小程序那套通信模型,一些数据需要通过桥的方式通信,而且在这其中,可能还需要将数据进行序列化之后传递,这其中的每个环节都需要通信成本,在微信小程序文档中,也对 setData 的频和量都有严格的限定,也就是不建议频繁使用 setData ,同样也不建议将所有数据都 setData ,setData 只更新相关视图的部分。
Native 渲染,以 RN 为例子,通信模型下需要 C++ 做中间层,一方面与 Native 进行通信,另外一方面与 JS 进行通信,所以也会有一定的通信成本,通过渲染形成树结构,也是要通过通信方式传递给 Native ,Native 是根据这个进行渲染绘制的。
还有一点就是 template 需要由独立的模版解析器去解析,再转化成虚拟 DOM ,这样就多了一道工序。
JSX 和 template 相比流程:
如上说到了 JSX 的优势,我们再看一下 element 转化成 fiber 的流程。fiber 架构是 React 的核心,fiber 上保存了当前节点的信息,fiber 树能够直接反映出整个 React 应用的原貌,跨端应用也可以利用 React fiber 这一点,在跨端应用中,也可以存活 fiber 树,不过在浏览器端,fiber 上保存了真实 DOM 信息,但是在跨端应用中,就可以保存其他有关视图元素的信息。
element 转化成 fiber 后:
Taro 3 中可以选择运行时的 Vue 和 React 做基础开发框架,当选择 React 开发小程序后,整个应用还是能够正常运行 React 应用,保持 React 的活性,这是为什么呢?
虽然微信小程序是采用 webview 的方式,但是对于原生 DOM 的操作,小程序并没有给开发者开口子,也就是说小程序里如果想要使用 React 框架,就不能使用 DOM 的相关操作,也就不能直接操作 DOM 元素,既然不能操作 DOM ,那么 React fiber 如何处理的呢?
原来在 Taro React 中,会改变 reconciler 中涉及到 DOM 操作的部分,我们来看看部分改动:
taro-react/src/reconciler.ts
import Reconciler, { HostConfig } from 'react-reconciler' /* 引入 taro 中兼容的 document 对象 */ import { document, TaroElement, TaroText } from '@tarojs/runtime' const hostConfig = { /* 创建元素 */ createInstance (type) { return document.createElement(type) }, /* 创建文本 */ createTextInstance (text) { return document.createTextNode(text) }, /* 插入元素 */ appendChild (parent, child) { parent.appendChild(child) }, ... }
这个是 taro 对 react-reconciler 的改动,在 react-reconciler 中,HostConfig 里面包含了所有有关真实 DOM 的操作,taro reconciler 会通过向 tarojs/runtime 引入 document 的方式,来劫持原生 DOM 中的 document,然后注入兼容好的方法,这样的话,当 fiber 操作 DOM 的时候,本质上是使用 Taro 提供了方法。
接下来 taro 就可以通过递归的方式插入自己伪造的 DOM 元素树,这样就可以在小程序中正常运行 React 框架了。
3 独立事件系统
React 做跨端还有一个好处,就是 React 有一套独立的事件系统,在我之前的文章中,也讲到过 React 事件系统的原理。React 事件系统的设计,能够把原生 DOM 元素和事件执行函数隔离开来,统一管理事件,这样事件的触发,由 DOM 层面变成了 JS 层面。为 React 做跨平台兼容提供了技术支撑。
上面说到了 React 的事件系统做的很好的一点就是,事件并非直接绑定在原生的 DOM 上,而是由 React 的事件系统统一控制。这样在跨端应用中,就可以有不同的平台独立处理这些事件。
三 React 能为跨端动态化做些什么?
React 到底了跨端动态化做了些什么呢? 我们来从内到外来分析一下:
1 React 语法做 DSL
React 对跨端领域的贡献。第一种就是以 React 作为 DSL 的跨端方案,对于 DSL 可能有些同学比较陌生,什么是 DSL 。
DSL 即「Domain Specific Language」,中文一般译为领域特定语言。
这种方案保留了 React 的语法,比如说 JSX 和 React 完全一致,包括有渲染函数 render,触发更新的方法 setState 等,但是不具有 React 的活性,也就是形似神不似,其内部并不是由 React 系统驱动,这种方案,最后会被编译成 JS 文件,接下来只要由 JS 引擎解析 JS,再由端上进行绘制就可以了。
以 React 作为 DSL 的应用,有一个好处就是不需要预编译处理。
比如微信小程序有自己 wxml,但是 wxml 只是一个模板,最后想要被 JS 识别,就必须进行预编译,编译成 JS 能够识别的抽象语法树形式。
还有一个优点是 React DSL 一般都采用 css in js 作为样式处理方法,所以也不需要用专门的解释器去解析 css 样式。
但是这种方案也有一些弊端,就是 React 的一些新特性或者新功能,可能无法正常使用,比如说 hooks,Suspense 等。
2 保留 React 运行时
还有一种就是类似 Taro 的解决方案,用 React 做为跨端方案,不仅仅是神似,而且还是形似,也就是 React 完全应用于运行时,这依赖于 React 框架一些良好的特性,比如 react-reconciler 对 DOM 方法的隔离 hostConfig,或者独立的事件系统等。
但是也需要对跨端做一些兼容处理,比如像 Taro react 中对 reconciler 的兼容。
这种方式因为是 React 运行时,所以可以用 React 提供的 API,也能使用 React 的特性。
3 React 新领域延伸
还有就是像 React Native ,本质上是官方 React 在跨端领域的解决方案。react 在 web 端用的是 ReactDOM.render 或者 createRoot 创建整个应用,在 RN 中则是用的是 AppRegistry.registerComponent 。
React Native 像是 web 的功能,在 Native 端重新实现一遍。
四 总结
本文介绍了 React 做跨端动态化的一些优势分析,列举出一些经典通过 React 实现跨端动态化的案例,浅析了原理,感兴趣的同学可以尝试一下用 RN ,Taro 等技术从零到一开发一个简单的移动端应用,相信会有很多收获。