专注React,学不会你打我!
卡颂日常从事基础架构相关工作。这次接到一个任务:封装一个React
组件交给业务方使用。
组件本地开发无误,自测无误,交给业务方接入无误,业务方测试环境验收无误。
然而,当包含该组件的页面小流量上线后,监控平台立刻收到报警:
代码语言:javascript复制Minified React error #31 ......
打开监控看板,发现大部分报错来自于一款机型:「Vivo x7」,Android
版本5.1。完整报错信息如下:
看看时间:周五晚上6点半。呵,还有我解决不了的React
问题?半小时搞定,过周末去~
然而......
找准问题原因
简单描述下#31号错误信息描述的内容:
React
的render
函数可接受的返回值类型包括:
string
,比如return 'I am kasong';
number
,比如return 123;
array
,比如return [<p>ka</p>, <p>song</p>];
。
其中[]
会被处理为React.Fragment
object
,比如return <p>ka song</p>;
。
因为该返回值会被编译为React.createElement
(或jsx.createElement
,视React
版本不同而不同)。
而React.createElement
的返回值是一个对象(即object
类型)。
这里的报错信息是说:你某个组件返回了一个非法值。因为这个值是object
类型,但是他不是一个JSX
对象。
想要复现这个问题也很简单,比如如下代码:
代码语言:javascript复制function App() {
reutrn {};
}
返回值是个object
,但非JSX
对象。
作为React
老司机,是绝对不可能写错返回值类型的。况且,写错的话,本地开发就会报错了。
而且很奇怪,这个问题,为什么只在这款机型复现呢?
初见端倪
现在我们掌握的线索是:
- 这是个个别机型复现的报错
- 报错原因是因为
render
函数返回了错误的类型
我们需要更多线索!!
虽然是被压缩的线上代码,但好在React
提供了必要的错误信息。
这个错误的object
包含了如下字段:
found: object with keys {$$typeof, type, key, ref, props, _owner}).
居然包含$$typeof
!!!
$$typeof
是React
源码内部用来判断JSX
对象类型的字段。
比如React.Fragment
、React.portal
、div
这三种JSX
分别对应三种$$typeof
。
换言之,包含$$typeof
的object
类型,大概率是一个JSX
对象。
既然这个报错的object
对象就是一个JSX
对象,那React
为什么还认为他是一个非法的返回值呢?
React
狠起来连自己都杀?
深入源码
要解答这个问题,必须深入React
源码。
由于我的组件中没有使用Fragment
或Portal
这样的特性,所以将问题定位在普通React
组件对应的$$typeof
。
在源码中,这种类型被称为REACT_ELEMENT_TYPE
。
代码语言:javascript复制PS:
Fragment
类型为REACT_FRAGMENT_TYPE
,Portal
类型为REACT_PORTAL_TYPE
,
var hasSymbol = typeof Symbol === 'function' && Symbol.for;
var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;
可以看到,当宿主环境支持Symbol
时,REACT_ELEMENT_TYPE === Symbol.for('react.element')
。
不支持时,REACT_ELEMENT_TYPE === 0xeac7
与此同时,REACT_ELEMENT_TYPE
这个变量的定义不仅存在于React
这个包中,也存在于ReactDOM
包中。
Vivo x7
的webview
原生不支持Symbol
。似乎有点儿苗头了!
审查业务方代码后发现,业务方使用core-js
实现了Symbol polyfill
。
那么设想如下场景:
假如业务方代码打包的顺序是:
React -> core-js -> ReactDOM
那么在运行时,React
首先加载,执行到定义REACT_ELEMENT_TYPE
变量这行代码时,宿主环境全局不存在Symbol
。
所以在React
这个包中,REACT_ELEMENT_TYPE === 0xeac7
接着运行core-js
,他会在window
上挂载Symbol
。
接着ReactDOM
在运行时,执行到定义REACT_ELEMENT_TYPE
变量这行代码时,宿主环境已经存在全局变量Symbol
。
所以在ReactDOM
中,REACT_ELEMENT_TYPE === Symbol.for('react.element')
而React.createElement
方法来自React
包,组件的render
方法是在ReactDOM
包中的reconciler
模块调用的,
所以就会发生判断组件类型时,0xeac7 !== Symbol.for('react.element')
,让React
认为这是个非法的返回值。
在遥远的2016年,就有人就此给React
提issue[1]。
事实真的如此么?
然而审视业务方代码后发现,依赖打包的顺序是:
core-js -> React -> ReactDOM
按照刚才的推理,React
和ReactDOM
都能拿到core-js
提供的Symbol polyfill
。
拨云见日
此时早已华灯初上,我为我对React
的轻视流下了不争气的泪水。
亏我还是React Contributor
,React技术揭秘[2]作者,React 17
的源码方法名都能背下来。
嗯?React 17
??难道!
v16.14
之前版本的React
中JSX
对象会被编译为React.createElement
。
此版本之后createElement
被从React
包中拆分出来,独立在react/jsx-runtime
中。
编译工作则由@babel/plugin-transform-react-jsx
插件完成。
那么此时,REACT_ELEMENT_TYPE
的定义存在于jsx-runtime
、React
、ReactDOM
三个包中。
也就是说,如果编译后包的执行顺序是:
jsx-runtime -> core-js -> React -> ReactDOM
在低端Android
机上还会复现这个问题!
这里截取一个网友遇到同样问题后他的截图:
这个问题的讨论见Bug: IE 11 not working with latest React version
可以看到,jsx-runtime
被babel
编译成第一个依赖,破案!
所以,这实际上是个babel
编译后产物顺序造成的bug
,已经有人给babel
提相关issue[3]
最终将JSX
的编译方式从编译为jsx.createElement
降级为React.createElement
解决了这个报错。
此时,夜已深。。。
总结
每一片雪花都是那么纯洁,直到他们组成了一场雪崩。
这个bug
的各方,React
、babel
、提供组件的我、业务方代码,单独来看,没有一方有问题。
但是,当一系列巧合合并在一起,就是一个线上bug
。
这也显示了线上小流量、报错监控基建的重要性。
最后,给大家一个彩蛋:
访问React官网链接[4]看我给你的留言
参考资料
[1]
issue: https://github.com/facebook/react/issues/8379#issuecomment-264858787
[2]
React技术揭秘: http://react.iamkasong.com/
[3]
issue: https://github.com/babel/babel/issues/12522
[4]
React官网链接: https://reactjs.org/docs/error-decoder.html/?invariant=31&args[]=object with keys {欢迎关注, 我的公众号, 魔术师卡颂, 专注React技术栈, 和小伙伴们, 一起进步}&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
送你一本源码学习指南
加入专业React进阶群