写在前面
半个月前,React发布了v17的第一个RC版本。距离上一个Major版本发布已经快两年半了。最近事情比较多,这周才有时间来“观察”一下v17版本。话不多少,让我们来看看这个版本中,包含了哪些内容呢?
新版本没有新特性
React v17中不会有用户可见的新特性。React v17是一个过渡版本,其存在的意义是为了让React的更新变得更容易一些。大家比较期待的Concurrent 模式并没有发布。
渐进式更新
在以前版本的React中,React采用的是一种"all-or-nothing"的更新策略。要么你完全使用一个新的版本,要么完全使用老版本,中间没有缓冲。这种更新策略会造成一定的问题。比如context api。如果你维护的一个老项目使用的是老版本的React,并且大量使用了context api,那么升级React版本将会是比较困难的。需求压得紧,老板不给你排期的情况下,你不得不继续使用老版本。
React v17将支持渐进式更新策略。说白了,从React v17后,如果你在项目中同时使用两个版本的React时,将不会出现任何问题。这将更有利于进行老项目的迁移和升级。官方还给出了一个渐进式更新的的例子:https://github.com/reactjs/react-gradual-upgrade-demo/
事件系统更新
尽管 React v17 没有带来用户可见的更新,但其实其内部的机制是有更新的。比如,事件系统就进行了调整。
我们都知道,React有自己的一套事件代理系统。比如我们的jsx中有以下代码:
<button onClick={handleClick}>点我,点我</div>
在代码中,我们是将点击函数绑定在了button上,但其实React是会进行事件代理的。最终的点击函数会被绑定在document
上。React之所以这样做,是考虑了性能问题。这个不难理解。但是这样会影响到React的渐进式更新策略。当页面中包含了多个版本的React时,就会出现问题:假设react v16的组件包含了一个react v15的组件,事件处理函数会在document上绑定两次,那么e.stopPropagation()
就失效了,最终外层的DOM依然会收到事件。
于是,React更新了事件代理策略:事件处理函数不再绑定在document
上,而是绑定在React组件树的根DOM上。
这样,就可以更安全的进行多React版本的组件树的嵌套。并且,React嵌入使用其他技术构建的应用程序变得更加容易。
不过,本次事件系统的更新,有以下场景需要注意。
如果你手动在document
上绑定了事件函数,在React v16 及以前,如果你在组件中绑定的事件函数中调用了e.stopPropagation()
,那么你在document
中绑定的事件函数依然会收到原生事件,因为原生事件本身已经在document
这一层了。在React v17中,这种情况将不会发生。
document.addEventListener('click', function() { // v17中,原生的事件处理函数不再收到事件,如果你在React组件中调用了 e.stopPropagation() // v16及以下,会。});
要修正这个问题,可以在事件捕获阶段进行事件处理。
document.addEventListener('click', function() { // 现在这个事件处理函数使用了事件捕获, // 所以它可以接收到所有的点击事件!}, { capture: true });
一句话说,新的React事件系统中,事件冒泡更接近原生的DOM事件系统。
其他破坏性更新
React虽然已经将破坏性更新降低到了最少,但还是有少量的修改。
对齐浏览器
React还对事件系统进行了如下的更改:
1、onScroll
事件不再冒泡。
2、React的onFocus
和onBlur
事件已在底层切换为原生的focusin
和focusou
t事件。它们更接近React现有行为,有时还会提供额外的信息。
3、捕获事件(例如,onClickCapture)现在使用的是实际浏览器中的捕获监听器。
取消了事件池
React v16及以下,是有一个事件池来专门管理事件的。起初,设计这个事件池,是考虑到了性能。但是在现代浏览器中,并没有对性能有多大提升,反而会让开发者疑惑,考虑下面的代码:
function handleChange(e) { setData(data => ({ ...data, // 在react v16及以下版本中,这段代码会出错 text: e.target.value }));}
这是因为React在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为null。在 React 16及更早版本中,使用者必须调用e.persist()才能正确的使用该事件,或者正确读取需要的属性。
在React v17中,上述代码将不会出现问题,可以按照预期执行。e.persist()在 React事件对象中仍然可用,只不过没有任何效果罢了。
副作用清理时机
在React v17中,useEffect
中的清理函数的调用时机进行了调整。
useEffect(() => { // 副作用 ... return () => { // 副作用清理函数 };});
大多数副作用(effect)不需要延迟刷新视图,因此React在屏幕上反映出更新后立即异步执行它们(在极少数情况下,你需要一种副作用来阻止重绘。例如,如果需要获取尺寸和位置,请使用useLayoutEffect)。
在 React v16中,副作用清理函数是同步执行的。在大型App中,这不太合适,因为这会影响到试图的更新。
在React 17中,副作用清理函数会异步执行 —— 如果要卸载组件,则清理函数会在视图更新后运行。
这个改变可能会造成一些case,比如下面的场景。
useEffect(() => { someRef.current.someSetupMethod(); return () => { someRef.current.someCleanupMethod(); };});
someRef.current
是可变的。同步执行时,上述代码没有问题。但是异步执行时,someRef.current
可能已经变成null了。这个时候,就需要在副作用中,保存对可变变量的引用:
useEffect(() => { const instance = someRef.current; instance.someSetupMethod(); return () => { instance.someCleanupMethod(); };});
返回一致的undefined错误
在React 16及更早版本中,返回undefined始终会报错:
function Button() { return; // Error: Nothing was returned from render}
上述情况其实是很容易发生的
function Button() { // 忘记return,该组件最终就是undefined // React会抛出这个错误,而不是忽略它。 <button />;}
在以前,针对上述情况,React只会检查类组件和函数组件,并不会检查forwardRef
和memo
。在v17中,上述情况也会check。
let Button = forwardRef(() => { // react 17 会抛出错误 <button />;}); let Button = memo(() => { // react 17 会抛出错误 <button />;});
原生组件栈
当你在浏览器中遇到错误时,浏览器会为你提供带有JavaScript函数的名称及位置的堆栈信息。然而JavaScript堆栈通常不足以诊断问题,因为React树的层次结构可能同样重要。你不仅要知道哪个Button抛出了错误,而且还想知道 Button在React树中的哪个位置。
为了解决这个问题,当你遇到错误时,从React 16开始会打印"组件栈"信息。尽管如此,它们仍然不如原生的JavaScript堆栈。特别是它们在控制台中不可点击,因为React不知道函数在源代码中的声明位置。此外,它们在生产中几乎无用。不同于常规压缩后的JavaScript堆栈,它们可以通过sourcemap的形式自动恢复到原始函数的位置,而使用React组件栈,在生产环境下必须在堆栈信息和bundle大小间进行选择。
在React 17中,使用了不同的机制生成组件堆栈,该机制会将它们与常规的原生JavaScript堆栈缝合在一起。这使得你可以在生产环境中获得完全符号化的React组件堆栈信息。
移除私有导出
最后,v17删除了一些以前暴露给其他项目的React内部组件。特别是,React Native for Web过去常常依赖于事件系统的某些内部组件,但这种依赖关系很脆弱且经常被破坏。
React v17删除了这些私有导出。对于大多数的React开发者来说,不会有任何的影响。
写在后面
本文内容完全来自官方blog:https://reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing
当然,这只是官方自己外公开的变化。其内部真正的一些策略变更,架构更新等,这个就需要阅读源码来寻找答案了。而React源码(hook除外)一直是我想吭,但是至今未行动的一座大山。从官方的Blog来看,最可见的更新(面试最可见),应该就是事件系统的更新了。
相关阅读