十五分钟读懂 React 17
关联面试卡(快速跳转)
- 技术卡:
tech__react - 性能方法论:
tech__performance
作为时下最火的前端框架之一,React 每次发版都会带来创新的改变,如 React 最早提出虚拟 DOM、React 16 引入 fiber 架构,再到后来 React 16.8 提出令人耳目一新的 Hooks,这些创新也是很多人推崇 React 的一个重要原因。然而,到了 React 17,rc 发布日志上竟然说这次版本最大的特点就是无新特性,从目前来说,这个日志是让很多人失望了。
这么多人对这次发版失望,那 React 17 就真的没什么好说的吗?显然不是,至少我认为不是的,从长远来看,无论是项目角度,还是源码学习角度,作为一个资深 reactress,我还是有很多东西要学习的。
首先面对用户的更改,React 官网上说的很详细了。如果你是一个 React 开发者,并且不想永远停留在老版本,想深入了解 React 17,想知道新版本对你开发的影响,那接下来我们来聊聊应该从哪些角度。
一、全新的 JSX 转换
React 17 以前,React 中如果使用 JSX,则必须像下面这样导入 React,否则会报错,这是因为旧的 JSX 转换会把 JSX 转换为 React.createElement(...) 调用。
import React from 'react'
export default function App(props) {
return <div>app </div>
}
2
3
4
5
当然,这并不完美,除了增加了学习成本,还有无法做到的性能优化和简化open in new window, 如 createElement 里还要动态做 children 的拼接、依赖于 React 的导入等等。
而 React 17 带来了改变,可以让我们单独使用 JSX 而无需引入 React。这是因为新的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。另外此次升级不会改变 JSX 语法,旧的 JSX 转换也将继续工作。
二、事件委托的变更
在 React 16 或更早版本中,React 会由于事件委托对大多数事件执行 document.addEventListener()。但是一旦你想要局部使用 React,那么 React 中的事件会影响全局,如下面这个例子,当把 React 和 jQuery 一起使用,那么当点击 input 的时候,document 上和 React 不相关的事件也会被触发,这符合 React 的预期,但是并不符合用户的预期。
令人开心的是,这次的 React 17 就解决了这个问题~,这次 React 不再将事件添加在document 上,而是添加到渲染 React 树的根 DOM 容器中:
const rootNode = document.getElementById('root')
ReactDOM.render(<App />, rootNode)
复制代码
2
3
这种改变不仅方便了局部使用 React 的项目,还可以用于项目的逐步升级,如一部分使用 React 18,另一部分使用 React 19,事件是分开的,这样也就不会相互影响。当然这并不是鼓励大家在一个项目中使用多个 React 版本,而只是作为一种临时处理的过渡~
好了,如果你只是励志做个普通工程师,可以跳到下个小章节看了,如果是 Reactress,继续往下看:
下图形象描述了这次的变更,图片来自 React 官网react.docschina.org/blog/2020/1…open in new window
自从其发布以来,React 一直自动进行事件委托。当触发 DOM 事件时,React 会找出调用的组件,然后 React 事件会在组件中向上 “冒泡”。这被称为事件委托open in new window。除了在大型应用程序上具有性能优势外,它还使添加类似于 replaying eventsopen in new window 这样的新特性变得更加容易。
事件委托,也就是我们通常提到的事件代理机制,这种机制不会把时间处理函数直接绑定在真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射表里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。
三、事件系统相关更改
除了事件委托这种比较大的更改,事件系统上还发生了一些小的更改,
与以往不同,React 17 中onScroll 事件不再冒泡,以防止出现常见的混淆open in new window。
React 的 onFocus 和 onBlur 事件已在底层切换为原生的 focusin 和 focusout 事件。它们更接近 React 现有行为,有时还会提供额外的信息。
捕获事件(例如,onClickCapture)现在使用的是实际浏览器中的捕获监听器。
这些更改会使 React 与浏览器行为更接近,并提高了互操作性。
注意:
尽管 React 17 底层已将
onFocus事件从focus切换为focusin,但请注意,这并未影响冒泡行为。在 React 中,onFocus事件总是冒泡的,在 React 17 中会继续保持,因为通常它是一个更有用的默认值。请参阅 sandboxopen in new window,以了解为不同特定用例添加不同检查。
四、去除事件池
在 React 17 以前,如果想要用异步的方式使用事件 e,则必须先调用调用 e.persist() 才可以,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。如下面的例子:
function FunctionComponent(props) {
const [val, setVal] = useState('')
const handleChange = (e) => {
// setVal(e.target.value);
// React 17以前,如果想用异步的方式使用事件e,必须要加上下面的e.persist()才可以
// e.persist();
// setVal(data => e.target.value);
}
return (
<div className="border">
<input type="text" value={val} onChange={handleChange} />
</div>
)
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
但是这种使用方式有点抽象,经常会让对 React 不太熟悉的开发者懵掉,但是值得开心的是,React 17 中移除了 “event pooling(事件池)“,因为以前加入事件池的概念是为了提升旧浏览器的性能,对于现代浏览器来说,已经不需要了。因此,上面的代码中不使用 e.persist();也能达到预期效果。
五、副作用清理时间
React 17 以前,当组件被卸载时,useEffect 和 useLayoutEffect 的清理函数都是同步运行,但是对于大型应用程序来说,这不是理想选择,因为同步会减缓屏幕的过渡(例如,切换标签),因此React 17 中的 useEffect 的清理函数异步执行,也就是说如果要卸载组件,则清理会在屏幕更新后运行。如果你某些情况下你仍然希望依靠同步执行,可以用 useLayoutEffect。
当然 React 17 中的 useEffect 的清理函数异步执行之后,有一个隐患:
useEffect(() => {
someRef.current.someSetupMethod()
return () => {
someRef.current.someCleanupMethod()
}
})
复制代码
2
3
4
5
6
7
问题在于 someRef.current 是可变的,因此在运行清除函数时,它可能已经设置为 null。解决方案是在副作用内部存储会发生变化的值:
useEffect(() => {
const instance = someRef.current
instance.someSetupMethod()
return () => {
instance.someCleanupMethod()
}
})
复制代码
2
3
4
5
6
7
8
我们不希望此问题对大家造成影响,我们提供了 eslint-plugin-react-hooks/exhaustive-deps 的 lint 规则open in new window(请确保在项目中使用它)会对此情况发出警告。
六、返回一致的 undefined 错误
在 React 16 及更早版本中,返回 undefined 始终是一个错误,当然这是 React 的预期,但是由于编码错误 ,forwardRef 和 memo 组件的返回值是 undefined 的时候没有做为错误,React 17 中修复了这个问题。React 中要求对于不想进行任何渲染的时候返回 null。
七、原生组件栈
在 React 17 中,使用了不同的机制生成组件调用栈,该机制会将它们与常规的原生 JavaScript 调用栈缝合在一起。这使得你可以在生产环境中获得完全符号化的 React 组件调用栈信息。
八、移除私有导出
React 17 删除了一些以前暴露给其他项目的 React 内部组件。特别是,React Native for Webopen in new window 过去常常依赖于事件系统的某些内部组件,但这种依赖关系很脆弱且经常被破坏。
在 React 17 中,这些私有导出已被移除。据我们所知,React Native for Web 是唯一使用它们的项目,它们已经完成了向不依赖那些私有导出函数的其他方法迁移。
九、启发式更新算法更新
React 16 开始替换掉了Stack Reconciler,开始使用启发式算法架构的的Fiber Reconciler。那么为什么要发生这个改变呢?
React 的 killer feature: virtual dom
- React15.x - Stack Reconciler
- React16 - Fiber Reconciler
- React17 - Fiber Reconciler (进阶版 - 优先级区间)
为什么需要 fiber
对于大型项目,组件树会很大,这个时候递归遍历的成本就会很高,会造成主线程被持续占用,结果就是主线程上的布局、动画等周期性任务就无法立即得到处理,造成视觉上的卡顿,影响用户体验。
任务分解的意义
解决上面的问题
增量渲染(把渲染任务拆分成块,匀到多帧)
更新时能够暂停,终止,复用渲染任务
给不同类型的更新赋予优先级
并发方面新的基础能力
更流畅
React 17 中更新了启发式更新算法,具体表现为曾经用于标记 fiber 节点更新优先级的 expirationTime 换成了为 lanes,前者为普通数字,而后者则为 32 位的二进制,了解二进制运算的都比较熟悉了,这种二进制的 lanes 是可以指定几个优先级的,而不是像以前 expirationTime 只能标记一个。
之所以做这种改变,原因就是在于expirationTimes模型不能满足IO操作(Suspense),Suspense 用法如下:
<React.Suspense fallback={<Loading />}>
<Content />
</React.Suspense>
2
3
