一 现有思路的局限性
先看看 Taro
, nanachi
是怎么在小程序端处理 JSX
语法的。简单来说,主要是通过在编译阶段把 JSX
转化为等效的小程序 wxml
来把 React
代码运行在小程序端的
比如将
1 | xx && <Text>hello</Text> |
将会被转化为wx:if
1 | <Text wx:if="{{xx}}">Hello</Text> |
这种方式把对 JSX
的处理,主要放在了编译阶段,他依赖于编译阶段的信息收集,以上面为例,它必须识别出逻辑表达式,然后做对应的 wx:if
转换处理。
那编译阶段有什么问题和局限呢?
1 | class App extends React.Component { |
首先我们声明 consta=<Text>Hello</Text>
,然后把 a
赋值给了 b
,在最新版本 Taro1.3
的转换后报错了!!!
a is not defined
为什么呢?
想理解上面的代码为什么报错,我们首先要理解编译阶段。本质上来说在编译阶段,代码其实就是‘字符串’,而编译阶段处理方案,就需要从这个‘字符串’中分析出必要的信息(通过 AST
,正则等方式)然后做对应的等效转换处理。
而对于上面的例子,需要做什么等效处理呢?需要我们在编译阶段分析出 b
是 JSX
片段:b=a=<Text>Hello</Text>
,然后把 <View>{b}</View>
中的 {b}
等效替换为 <Text>Hello</Text>
。然而在编译阶段要想确定 b
的值是很困难的,有人说可以往前追溯来确定b的值,也不是不可以,但是考虑一下 由于 b=a
,那么就先要确定 a
的值,这个 a
的值怎么确定呢?需要在 b
可以访问到的作用域链中确定 a
,然而 a
可能又是由其他变量赋值而来,循环往复,期间一旦出现不是简单赋值的情况,比如函数调用,三元判断等运行时信息,追溯就宣告失败,要是 a
本身就是挂在全局对象上的变量,追溯就更加无从谈起。
所以在编译阶段 是无法简单确定 b
的值的。
我们再仔细看下报错信息:a is not defined
。
什么说 a
未定义呢?这是涉及到另外一个问题,我们知道 <Text>Hello</Text>
,其实等效于 React.createElement(Text,null,'Hello')
,而 React.createElement
方法的返回值就是一个普通 JS
对象
1 | { |
所以上面那一段代码在 JS
环境真正运行的时候,大概等效如下:
1 | class App extends React.Component { |
但是,之前说编译阶段需要对 JSX
做等效处理,需要把 JSX
转换为 wxml
,所以 <Text>Hello</Text>
这个 JSX
片段被特殊处理了, a
不再是一个普通 js
对象,这里我们看到 a
变量甚至丢失了,这里暴露了一个很严重的问题:代码语义被破坏了,也就是说由于编译时方案对 JSX
的特殊处理,真正运行在小程序上的代码语义并不是你的预期。
二 新的思路
下面我们介绍一种全新的处理思路,这种思路在小程序运行期间和真正的 React
几无区别,不会改变任何代码语义, JSX
表达式只会被处理为 React.createElement
方法调用,实际运行的时候就是普通 js
对象,最终通过其他方式渲染出小程序视图。
1 | 第一步:给每个独立的 `JSX`片段打上唯一标识 `uuid`,例如: |
在这整个过程中,你的所有 JS
代码都是运行在 React
过程中的,语义完全一致, JSX
片段也不会被任何特殊处理,只是简单的 React.createElement
调用,另外由于这里的 React
过程只是纯 js
运算,执行是非常迅速的,通常只有几ms。最终会输出一个 uiDes
数据到小程序,小程序通过这个 uiDes
渲染出视图。
现在我们在看之前的赋值 const b=a
,就不会有任何问题了,因为 a
不过是普通对象。另外对于常见的编译时方案的限制,比如任意函数返回 JSX
片段,动态生成 JSX
片段, for
循环使用 JSX
片段等等,都可以完全解除了,因为 JSX
片段只是 js
对象,你可以做任何操作,最终 ReactDOM.render
会搜集所有执行结果的片段的 uuid
标识,生成 uiDes
,而小程序会根据这个 uiDes
数据结构渲染出最终视图。
可以看出这种新的思路和以前编译时方案还是有很大的区别的,对 JSX
片段的处理是动态的,你可以在任何地方,任何函数出现任何 JSX
片段, 最终执行结果会确定渲染哪一个片段,只有执行结果的片段的 uuid
会被写入 uiDes
。这和编译时方案的静态识别有着本质的区别。
评论加载中