Relyzer —— 一个 React FC 的运行分析工具

React Hooks 带来了很多好处,同时也引入了一些困扰。常见的问题有:
为什么我的 useEffect 比我预期执行多得多?为什么 useCallback / useMemo 的返回值总是发生变化?

于是开发了这样的一个工具,配合 React Devtool 使用,在代码块中可视化地展示出 React 函数组件的属性及内部变量的变化情况,可以方便地定位到哪些值总是在发生变化。

截图是基于一个第三方的 TodoMVC,集成上了 Relyzer 工具。

从这张截图里可以得到这些信息:

1. 组件 TodoItem 的实例一共更新了 10 次,这 10 次更新中,todo 属性更新了 10 次(因此可以认为更新都是因为 todo 属性变化导致的)。
2. onDelete 始终没有变化,是因为它的依赖 todo.id 没有变化。 
3. finishedCallback 变化了 10 次,因为它的依赖项 todo 变化了 10 次。 (Relyzer 对 useCallback / useEffect / useMemo 做了处理,也会观测它们的依赖)
4. handleViewClick 变化了 10 次。
5. useRef 的返回是不会变的。
...

视频演示

Relyzer 视频演示https://www.zhihu.com/video/1426961395342114816

安装

Relyzer 提供给用户的是一个 Babel 插件,只要在开发环境下启用这个 Babel 插件,网页上就会自动注入 Relyzer 工具面板。

// babel 配置
{
  plugins: [
    // enable only for development
+    isDevelopment ? 'module:@relyzer/babel' : null,
  ].filter(Boolean),
}

如果 babel 配置是 JSON 格式,不能使用 JS 逻辑,也可以直接引入 module:@relyzer/babel. 只需要保证生产构建时 process.env.NODE_ENV === 'production'.

因为实现原理的限制,Relyzer 需要确保能正确判断出某个函数是否是一个 React 组件,只对 React 函数组件进行 Babel 转换。所以需要使用者选择两种方式其中一种:

  1. 手动指定某个函数为 React 组件,只有指定过的函数才能进行调试。
// 给注释加上 @component
/**
 * @component
 */
function MyComponent() {
  const [val, setVal] = useState();

  return (
    <div />
  )
}

// 但是某些编译工具(esbuild)会提前抹去注释,如果是在 esbuild 后面使用 relyzer/babel
// 可以添加一个自定义的指令 use relyzer
function MyComponent() {
  'use relyzer'
  const [val, setVal] = useState();

  return (
    <div />
  )
}

2. 让 relyzer/babel 自动检测

// 使用如下 babel plugin 配置项
['module:@relyzer/babel', { autoDetect: true }]

这种模式下,relyzer/babel 会对所有大写字母开头的函数进行转换,然后在运行时通过当前调用栈信息判断是否是 React 组件。

实现原理

了解过 Istanbul(JS 代码覆盖率工具)的人应该知道,Istanbul 是通过 babel 插件,往源代码中插入大量的计数代码,这样代码执行之后,就能知道哪些行或者条件分支被执行了。

Relyzer 使用了类似的实现原理:

Relyzer 提供了一个 Babel 插件,用来将正常的函数组件代码:

// input
function MyComponent({ foo }) {
  const bar = useXxx()

  return <div />
}

转化成这样:

// output
import useRelyzer from '@relyzer/runtime';

function MyComponent({ foo }) {
  const x = useRelyzer(...)
  x(foo, ...)
  const bar = useXxx()
  x(bar, ...)

  return <div />
}

在组件运行时,runtime 会把组件每次 render 过程中收集到的信息发送给 client,通过可视化界面展示出来。

那么 Relyzer 是如何跟 Chrome React Devtool 配合使用的呢?

在 React Devtool 的 DOM 树中选择组件节点后,Relyzer Client 中会自动切换到这个组件实例。这是因为 React Devtool 选中节点后会把当前节点写入到 window.$r,那么只需要劫持这个值就能实时监听 Devtool 的选中情况了。

项目地址

github.com/meowtec/rely

编辑于 2021-10-01 18:23