读 React 源码,找 Scripts 的 Bug

Last updated:

今年五月,我遇到了一件奇怪的事情:在 React 中,如果 Scripts 用了 onLoad,那么 Scripts 就会失效。

// 有效
<script async src="foo.js" />

// 失效
<script async src="boo.js" onLoad={() => {}} />

过去几天,我又遇到了一次,和上次一样,又陷进去了个把小时。

于是我决定读读源码,找找 Bug。

# 为何 Scripts 会失效?

根据源码,React 会把某些 Scripts 提升到 Head,就像下面这样:

// React
<head />

<script async src="foo.js" />
<script async src="boo.js" onLoad={() => {}} />

// HTML
<head>
  <script async src="foo.js" />
</head>

<script async src="boo.js" onLoad={() => {}} />

对于 Hoistable Scripts(会被提升到 Head 的 Scripts),React 会用 createElement 来创建。对于 Non-Hoistable Scripts,React 则会用 innerHTML,而浏览器会 故意忽略 此类 Scripts。

于是,Scripts 就失效了。

注意:提升异步脚本是合理的,因为既能提早下载资源,又不会有副作用。不提升带 onLoad | onError 的脚本也是合理的,因为如果资源下载完成/失败时,Scripts 的宿主组件还没有挂载,那么就会发生问题。

# 哪些 Scripts 会失效?

Non-Hoistable Scripts 都会失效,而符合下面任意一个条件的 Scripts 就是 Non-Hoistable Scripts(源码见 ):

  1. 没使用 src
  2. 没使用 async
  3. 使用了 onLoadonError

# 如何修复?

这显然是一个 Bug,因为既然 Async Scripts 会有效,那么带 onLoad 的 Async Scripts 也应该有效,可实际却没有,并且用 onLoad 也算合理用法。

那么,只要不提升带 onLoad 的 Async Scripts,然后不“灭活”它,不就可以了吗?是的,我是这么想的。但源码看得越多,就越是觉得这是 React 团队故意的设计决策,比如为了防止某些 XSS,或配合 RSC 的水合。

最后,我决定先在 相关 issue 上提供这些信息,如果 React 团队也认同这是一个 Bug,那么就修。

React 有两个渲染器,Fiber 和 Fizz。

Fiber 负责在 Client 端操纵 DOM,Fizz 负责在 Server 端生成 HTML 字符串,它们有各自的生成 Scripts 的实现,不过实现逻辑是一致的。尽管如此,Scripts 在 CSR 和 SSR 中的行为仍不完全相同,本文是在 CSR 的视角上写的。