实现React过程中一次有趣的问题排查经历

大家好,我卡颂。
最近关于React的新书交稿了(预计年底出版),时间比较多。
【实现React过程中一次有趣的问题排查经历】趁着对React内部运行流程还记得住,业余时间尝试复刻一个React —— big-react。
即然是复刻一个React,那肯定得跑通部分官方的测试用例。
在跑一个用例时遇到个很有意思的问题,以下是排查过程。
欢迎加入人类高质量前端框架群,带飞
问题现象 以下是这个用例的内容:

it('uses the fallback value when in an environment without Symbol', () => { expect(().$$typeof).toBe(0xeac7); });

他测试的是在不支持Symbol的环境,jsx的内部属性$$typeof是否正确。
我们知道,jsx仅仅是JS的语法糖,在编译时会被编译成函数调用,比如:
// 编译前// 编译后 React17之前 React.createElement('div'); // 编译后 React17之后 jsxRuntime.jsx('div');

React.createElement(或jsxRuntime.jsx)方法的实现中,最终会返回如下数据结构:
const element: ReactElement = { $$typeof: REACT_ELEMENT_TYPE, type, key, ref, props };

其中$$typeof属性用于区分jsx对象的类型,比如REACT_ELEMENT_TYPE代表这个jsx对象是一个React Element
在支持Symbol的环境,$$typeof对应一个唯一的symbol。在不支持的环境,对应一个16进制数字。
比如REACT_ELEMENT_TYPE的定义如下:
const supportSymbol = typeof Symbol === 'function' && Symbol.for; export const REACT_ELEMENT_TYPE = supportSymbol ? Symbol.for('react.element') : 0xeac7;

回到我们的测试用例,他的测试意图就很明显了:在不支持Symbol的环境,div对应jsx对象的$$typeof属性应该返回数字0xeac7
it('uses the fallback value when in an environment without Symbol', () => { expect(().$$typeof).toBe(0xeac7); });

那么如何制造一个不支持Symbol的环境呢?
很简单,在所有用例执行前的beforeEach钩子函数(jest提供的)中将global.Symbol置为undefined
beforeEach(() => { jest.resetModules(); originalSymbol = global.Symbol; // 制造不支持Symbol的环境 global.Symbol = undefined; React = require('react'); ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils'); });

当引入reactreact-dom时,其内部执行时global.Symbol === undefined
这就模拟了不支持Symbol的环境。
但是这个用例却挂了:

上述代码应该是没问题的,毕竟是React官方会跑的用例。那么问题出在哪儿呢?
babel的锅 在React17发布时,带来了全新的 JSX 转换。
在17之前,jsx会编译为React.createElement,17之后会编译为jsxRuntime.jsx
同时会在模块顶部引入如下语句:
import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime";

上述被引入的语句的执行先于下述语句:
originalSymbol = global.Symbol; global.Symbol = undefined;

所以在语句执行时,环境中还存在global.Symbol,就造成开篇提到的问题。
那为什么React官方跑用例时没有问题呢?
答案是:React跑用例时会将jsx编译为React.createElement
这样不会在模块顶部插入新的引入语句。
当引入React时,环境中已经不存在global.Symbol了:
originalSymbol = global.Symbol; global.Symbol = undefined; React = require('react'); ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils');

总结 由于编译在内存中进行,不太好排查编译后代码。所以如果对React各方面特性了解不深的话,这个问题真不太好排查。
当前big-react代码量还比较少,对从0实现React感兴趣的朋友可以关注下,给个star哦~

    推荐阅读