React#31错误,让我熬夜,让我秃头这次接到一个任务:封装一个React组件,交给业务方。组件本地开发无误,自测无误,业务端接入无误,业务端测试环境无误。然而,当包含该组件的页面小流量上线后,监控平台立马收到告警:MinifiedReacterror#31...打开监控板,发现大部分错误都来自于一个机型:“vivox7",Android版本5.1。完整的报错信息如下:看时间:6:30p.m.在周五。呵呵,还有我解决不了的React问题?半小时搞定,周末再去~不过...找出问题原因,简单描述一下#31错误信息描述的内容:React的render函数接受的返回值类型包括:string,比如返回'我是卡松';号码,如返回123;数组,如return[
ka
,song
];。其中,[]会被当作React.Fragment对象处理,比如returnkasong
;。因为返回值会被编译成React.createElement(或者jsx.createElement,取决于React版本)。React.createElement的返回值是一个对象(即对象类型)。这里的错误信息是这样说的:你的一个组件返回了一个非法值。因为这个值是object类型的,但是他不是JSX对象。重现这个问题也很简单,比如下面的代码:functionApp(){reutrn{};}返回值是一个对象,而不是JSX对象。作为一个资深的React驱动,绝对不可能写错返回值类型。而且,如果你写错了,本地开发也会报错。而且很奇怪,为什么这个问题只在这个机型上重现?我们现在掌握的线索是:这是在某机型上复现的报错。报错的原因是render函数返回了错误的类型。我们需要更多线索!!虽然它是压缩的在线代码,但幸运的是React提供了必要的错误信息。这个错误的对象包含以下字段:found:objectwithkeys{$$typeof,type,key,ref,props,_owner})。它实际上包含$$typeof!!!$$typeof用于判断React源代码字段内部的JSX对象类型。比如React.Fragment、React.portal、div就是三种JSX,对应三种$$typeof。也就是说,包含$$typeof的对象类型很有可能是JSX对象。既然这个错误的object对象是JSX对象,为什么React会认为是非法返回值呢?React狠到连自己都作死?深入源码要回答这个问题,就必须深入React源码。由于我的组件没有使用Fragment或者Portal等特性,所以问题定位在普通React组件对应的$$typeof。在源代码中,这种类型称为REACT_ELEMENT_TYPE。PS:Fragment类型为REACT_FRAGMENT_TYPE,Portal类型为REACT_PORTAL_TYPE,varhasSymbol=typeofSymbol==='function'&&Symbol.for;varREACT_ELEMENT_TYPE=hasSymbol?Symbol.for('react.element'):0xeac7;可以看出,当宿主环境支持ForSymbol时,REACT_ELEMENT_TYPE===Symbol.for('react.element')。不支持时,REACT_ELEMENT_TYPE===0xeac7同时,变量REACT_ELEMENT_TYPE的定义不仅存在于React包中,也存在于ReactDOM包中。vivox7的webview本身不支持Symbol。好像有点预兆!查看业务方的代码,发现业务方使用了core-js来实现Symbolpolyfill。那么想象一下这样的场景:如果业务端代码打包的顺序是: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模块调用,所以componenttype会被判断为0xeac7!==Symbol.for('react.element')时,让React认为这是一个非法的返回值。在遥远的2016年,有人提出了React的问题[1]。真的是这样吗?但是查看业务端的代码,发现依赖打包的顺序是:core-js->React->ReactDOM根据刚才的推理,React和ReactDOM都可以得到core提供的Symbolpolyfill-js。此时已经是华灯初上,我为自己对React的轻视而流下了失望的泪水。幸运的是,我还是一个ReactContributor,ReactTechnologyDemystified[2]的作者,我可以记住React17的源代码方法的名称。嗯?反应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,这个问题在低端安卓机上会重现!下面是遇到同样问题的网友的截图。屏幕截图:有关此问题的讨论,请参阅Bug:IE11notworkingwithlatestReactversion。可以看到jsx-runtime被babel编译到了第一个依赖中,问题就解决了!所以,这其实是babel编译的products顺序导致的bug。已经有人提出了babel[3]的相关issue,最后我将JSX的编译方式从jsx.createElement降级为React.createElement来解决这个错误。此时,夜已深。..摘要每一片雪花在形成雪崩之前都是那么纯净。这个bug涉及的各方,React,babel,提供组件的我,还有业务方的代码,一个个都没有问题。然而,当一连串的巧合融合在一起时,就是一个在线bug。这也显示了小在线流量、错误报告和监控基础设施的重要性。参考文献[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%20with%20keys%20%7B%E6%AC%A2%E8%BF%8E%E5%85%B3%E6%B3%A8%2C%20%E6%88%91%E7%9A%84%E5%85%AC%E4%BC%97%E5%8F%B7%2C%20%E9%AD%94%E6%9C%AF%E5%B8%88%E5%8D%A1%E9%A2%82%2C%20%E4%B8%93%E6%B3%A8React%E6%8A%80%E6%9C%AF%E6%A0%88%2C%20%E5%92%8C%E5%B0%8F%E4%BC%99%E4%BC%B4%E4%BB%AC%2C%20%E4%B8%80%E8%B5%B7%E8%BF%9B%E6%AD%A5%7D&args[]=%20for%20the%20full%20message%20or%20use%20the%20non-minified%20dev%20environment%20for%20full%20errors%20and%20additional%20helpful%20warnings。