我的项目目前react17和react18都有应用,但在开发者角度绝大部分场景还是感知不到多大变动,但也要具体理解分明具体更新了什么。本文就来一次性梳理下 react17与react18的变动。
首先,官网公布日志称react17最大的特点就是无新个性,这个版本次要指标是让React能渐进式降级,它容许多版本混用共存,能够说是为更远的将来版本做筹备了。
在React17之前,如果应用异步的形式来获取事件e对象,会发现合成事件对象被销毁,如下:
function App() {
const handleClick = (e: React.MouseEvent) => {
console.log('间接打印e', e.target) //
// v17以下在异步办法拿不到事件e,必须先调用 e.persist()
// e.persist()
// 异步形式获取事件e
setTimeout(() => {
console.log('setTimeout打印e', e.target) // null
})
}
return (
)
}
如果你须要在事件处理函数运行之后获取事件对象的属性,你须要调用 e.persist()
,它会将以后的合成事件从事件池中删除,并容许保留对事件的援用。
事件池:合成事件对象会被放入池中对立治理。这意味着合成事件对象能够被复用,当所有事件处理函数被调用之后,其所有属性都会被回收开释置空。
事件池的益处是在较旧浏览器中重用了不同事件的事件对象以进步性能,但它对古代浏览器的性能优化微不足道,反而给开发者带来困惑,因而去除了事件池,因而也没有了事件复用机制。
function App() {
// v17 去除了 React 事件池,异步形式应用e不再须要 e.persist()
const handleClick = (e: React.MouseEvent) => {
console.log('间接打印e', e.target) //
setTimeout(() => {
console.log('setTimeout打印e', e.target) //
})
}
return (
)
}
reactv17前,React 将事件委托到 document 上,在react17中,则委托到根节点
const rootNode = document.getElementById('root');
ReactDOM.render( , rootNode);
import { useState, useEffect } from 'react'
function App() {
const [isShowText, setIsShowText] = useState(false)
const handleShowText = (e: React.MouseEvent) => {
// e.stopPropagation() // v16有效
// e.nativeEvent.stopImmediatePropagation() // 阻止监听同一事件的其余事件监听器被调用
setIsShowText(true)
}
useEffect(() => {
document.addEventListener('click', () => {
setIsShowText(false)
})
}, [])
return (
{isShowText && 展现文字}
)
}
如上代码,在react16和v17版本,点击按钮时,都不会显示文字。这是因为react的合成事件是基于事件委托的,有事件冒泡,先执行React事件,再执行document上挂载的事件。
v16:出于对冒泡的理解,咱们间接在按钮事件上加e.stopPropagation()
,这样就不会冒泡到document,isShowText
也不会被置为false了。但因为v16版本的事件委托是绑在document
上的,它的事件源跟document
就是同级了,而不是上下级,所以e.stopPropagation()
并没有起作用。如果要阻止冒泡,能够应用原生的e.nativeEvent.stopImmediatePropagation()
阻止同级冒泡,这样文字就能够显示了。
v17:因为事件委托到根目录root节点,与document
属于上下级关系,所以能够间接应用e.stopPropagation()
阻止
stopImmediatePropagation() 办法能够阻止监听同一事件的其余事件监听器被调用
这种更新不仅不便了部分应用 React 的我的项目,还能够用于我的项目的渐进式降级,解决不同版本的 React 组件嵌套应用时,e.stopPropagation()无奈失常工作的问题
对事件零碎进行了一些较小的更改:
onScroll
事件不再冒泡,以防止出现常见的混同onFocus
和 onBlur
事件已在底层切换为原生的 focusin
和 focusout
事件。它们更靠近 React 现有行为,有时还会提供额定的信息。blur、focus 和 focusin、focusout 的区别:blur、focus 不反对冒泡,focusin、focusout 反对冒泡
onClickCapture
)当初应用的是理论浏览器中的捕捉监听器。这些更改会使 React 与浏览器行为更靠近,并进步了互操作性。
只管 React 17 底层已将 onFocus 事件从 focus 切换为 focusin,但请留神,这并未影响冒泡行为。在 React 中,onFocus 事件总是冒泡的,在 React 17 中会持续放弃,因为通常它是一个更有用的默认值
总结下来就是两点:
jsx()
函数替换 React.createElement()
jsx()
函数,无需手写引入react
在v16中,咱们写一个React组件,总要引入
import React from 'react'
这是因为在浏览器中无奈间接应用 jsx,所以要借助工具如@babel/preset-react
将 jsx 语法转换为 React.createElement
的 js 代码,所以须要显式引入 React,能力失常调用 createElement。
通过React.createElement()
创立元素是比拟频繁的操作,自身也存在一些问题,无奈做到性能优化,具体可见官网优化的 动机
v17之后,React 与 Babel 官网进行单干,间接通过将 react/jsx-runtime
对 jsx 语法进行了新的转换而不依赖 React.createElement
,因而v17应用 jsx 语法能够不引入 React,应用程序仍然能失常运行。
function App() {
return Hello World
;
}
// 新的 jsx 转换为
// 由编译器引入(禁止本人引入!)
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
如何降级至新的 JSX 转换
如果你还在应用v16,也可降级至 React v16.14.0 的版本,官网在该版本也反对这个个性。
@babel/preset-react
编译减少 runtime: 'automatic'
配置// 如果你应用的是 @babel/preset-react
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic"
}]
]
}
// 如果你应用的是 @babel/plugin-transform-react-jsx
{
"plugins": [
["@babel/plugin-transform-react-jsx", {
"runtime": "automatic"
}]
]
}
tsconfig.json
配置,具体配置可见TS官网文档{
"compilerOptions": {
// "jsx": "react",
"jsx": "react-jsx",
},
}
从 Babel 8 开始,”automatic” 会将两个插件默认集成在 rumtime 中
useEffect(() => {
// This is the effect itself.
return () => {
// This is its cleanup.
}
})
useEffect
的清理函数都是同步运行的;对于大型应用程序来说,同步会减缓屏幕的过渡(如切换标签)useEffect
副作用清理函数是异步执行的,如果要卸载组件,则清理会在屏幕更新后运行此外,v17 将在运行任何新副作用之前执行所有副作用的清理函数(针对所有组件),v16 只对组件内的副作用保障这种程序。
不过须要留神
useEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
问题在于 someRef.current 是可变的且因为异步的,在运行革除函数时,它可能曾经设置为 null。
// 用一个变量量在 ref 每次变动时,将 someRef.current 保存起来,放到副作用清理回调函数的闭包中,来保障不可变性。
useEffect(() => {
const instance = someRef.current
instance.someSetupMethod()
return () => {
instance.someCleanupMethod()
}
})
或者用 useLayoutEffect
useLayoutEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
useLayoutEffect 能够保障回调函数同步执行,这样就能确保 ref 此时还是最初的值。
在v17以前,组件返回undefined
始终是一个谬误。然而有漏网之鱼,React 只对类组件和函数组件执行此操作,但并不会查看 forwardRef
和 memo
组件的返回值。
function Button() {
return; // Error: Nothing was returned from render
}
function Button() {
// We forgot to write return, so this component returns undefined.
// React surfaces this as an error instead of ignoring it.
;
}
在 v17 中修复了这个问题,forwardRef 和 memo 组件的行为会与惯例函数组件和类组件保持一致,在返回 undefined 时会报错
let Button = forwardRef(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
;
});
let Button = memo(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
;
});
v16中谬误调用栈的毛病:
整体来说不如原生的 Javascript 调用栈,不同于惯例压缩后的 Javascript 调用栈,它们能够通过 sourcemap 的模式主动复原到原始函数的地位,而应用 React 组件栈,在生产环境下必须在调用栈信息和 bundle 大小间进行抉择。
在v17应用了不同的机制生成组件调用栈,间接从 Javascript 原生谬误栈生成的,所以在生产环境也能按sourcemap 还原回来,且反对点击跳到源码地位。
想具体理解的可见该 PR
v17 删除了一些公有 API,次要是 React Native for Web 应用的
另外,还删除了ReactTestUtils.SimulateNative
工具办法,因为其行为与语义不符,如果你想要一种简便的形式来触发测试中原生浏览器的事件,可间接应用 React Testing Library
援用 React17新个性:启发式更新算法
expirationTimes
模型只能辨别是否>=expirationTimes
决定节点是否更新。lanes
模型能够选定一个更新区间,并且动静的向区间中增减优先级,能够解决更细粒度的更新。这个我目前也不是太分明具体算法,先不开展了有趣味的可去查阅相干材料
v18的新个性是应用古代浏览器的个性构建的,彻底放弃对 IE 的反对。
v17 和 v18 的区别就是:从同步不可中断更新变成了异步可中断更新,v17能够通过一些试验性的API开启并发模式,而v18则全面开启并发模式。
并发模式可帮忙利用放弃响应,并依据用户的设施性能和网速进行适当的调整,该模式通过使渲染可中断来修复阻塞渲染限度。在 Concurrent 模式中,React 能够同时更新多个状态。
这里参考下文辨别几个概念:
useDeferredValue
/useTransition
可浏览参考 Concurrent Mode(并发模式)
v18 应用 ReactDOM.createRoot() 创立一个新的根元素进行渲染,应用该 API,会主动启用并发模式。如果你降级到v18,但没有应用ReactDOM.createRoot()
代替ReactDOM.render()
时,控制台会打印谬误日志要揭示你应用React,该正告也象征此项变更没有造成breaking change,而能够并存,当然尽量是不倡议。
// v17
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render( , document.getElementById('root'))
// v18
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( )
批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以晋升性能
在v17的批处理只会在事件处理函数中实现,而在Promise链、异步代码、原生事件处理函数中生效。而v18则所有的更新都会主动进行批处理。
// v17
const handleBatching = () => {
// re-render 一次,这就是批处理的作用
setCount((c) => c + 1)
setFlag((f) => !f)
}
// re-render两次
setTimeout(() => {
setCount((c) => c + 1)
setFlag((f) => !f)
}, 0)
// v18
const handleBatching = () => {
// re-render 一次
setCount((c) => c + 1)
setFlag((f) => !f)
}
// 主动批处理:re-render 一次
setTimeout(() => {
setCount((c) => c + 1)
setFlag((f) => !f)
}, 0)
如果在某些场景不想应用批处理,能够应用 flushSync
退出批处理,强制同步执行更新。
flushSync 会以函数为作用域,函数外部的多个 setState 依然是批量更新
const handleAutoBatching = () => {
// 退出批处理
flushSync(() => {
setCount((c) => c + 1)
})
flushSync(() => {
setFlag((f) => !f)
})
}
SSR 一次页面渲染的流程:
上述流程是串行执行的,v18前的 SSR 有一个问题就是它不容许组件”期待数据”,必须收集好所有的数据,能力开始向客户端发送HTML。如果其中有一步比较慢,都会影响整体的渲染速度。
v18 中应用并发渲染个性扩大了Suspense
的性能,使其反对流式 SSR,将 React 组件分解成更小的块,容许服务端一点一点返回页面,尽早发送 HTML和选择性的 hydrate, 从而能够使SSR更快的加载页面
}>
具体可参考文章 React 18 中新的 Suspense SSR 架构
Transitions 是 React 18 引入的一个全新的并发个性。它容许你将标记更新作为一个 transitions(过渡),这会通知 React 它们能够被中断执行,并防止回到曾经可见内容的 Suspense 降级计划。实质上是用于一些不是很急切的更新上,用来进行并发管制
在v18之前,所有的更新工作都被视为急切的工作,而Concurrent Mode 模式能将渲染中断,能够让高优先级的工作先更新渲染。
React 的状态更新能够分为两类:
startTransition
API 容许将更新标记为非紧急事件解决,被startTransition
包裹的会提早更新的state,期间可能被其余紧急渲染所抢占。因为 React 会在高优先级更新渲染实现之后,才会渲染低优先级工作的更新
React 无奈自动识别哪些更新是优先级更高的。比方用户的键盘输入操作后,setInputValue会立刻更新用户的输出到界面上,是紧急更新。而setSearchQuery是依据用户输出,查问相应的内容,是非紧急的。
const [inputValue, setInputValue] = useState()
const OnChange= (e)=>{
setInputValue(e.target.value) // 更新用户输出值(用户打字交互的优先级应该要更高)
setSearchQuery(e.target.value) // 更新搜寻列表(可能有点提早,影响)
}
return (
)
React无奈自动识别,所以它提供了 startTransition
让咱们手动指定哪些更新是紧急的,哪些是非紧急的,从而让咱们改善用户交互体验。
// 紧急的更新
setInputValue(e.target.value)
// 开启并发更新
startTransition(() => {
setSearchQuery(input) // 非紧急的更新
})
这里有个比拟好的在线例子,能够间接感触到 startTransition
的优化
当有过渡工作(非紧急更新)时,咱们可能须要通知用户什么时候以后处于 pending(过渡) 状态,因而v18提供了一个带有isPending
标记的 Hook useTransition
来跟踪 transition 状态,用于过渡期。
useTransition
执行返回一个数组。数组有两个状态值:
isPending
: 指处于过渡状态,正在加载中startTransition
: 通过回调函数将状态更新包装起来通知 React这是一个过渡工作,是一个低优先级的更新function TransitionTest() {
const [isPending, startTransition] = useTransition()
const [count, setCount] = useState(0)
function handleClick() {
startTransition(() => {
setCount((c) => c + 1)
})
}
return (
{isPending && spinner...}
)
}
直观感觉这有点像 setTimeout
,而防抖节流其实实质也是setTimeout
,区别是防抖节流是管制了执行频率,让渲染次数缩小了,而 v18的 transition 则没有缩小渲染的次数。
useDeferredValue
和 useTransition
一样,都是标记了一次非紧急更新。useTransition
是解决一段逻辑,而useDeferredValue
是产生一个新状态,它是延时状态,这个新的状态则叫 DeferredValue。所以应用useDeferredValue
能够推延状态的渲染
useDeferredValue
承受一个值,并返回该值的新正本,该正本将推延到紧急更新之后。如果以后渲染是一个紧急更新的后果,比方用户输出,React 将返回之前的值,而后在紧急渲染实现后渲染新的值。
function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);
// Memoizing 通知 React 仅当 deferredQuery 扭转,
// 而不是 query 扭转的时候才从新渲染
const suggestiOns= useMemo(() =>
,
[deferredQuery]
);
return (
<>
{suggestions}
>
);
}
这样一看,useDeferredValue
直观就是提早显示状态,那用防抖节流有什么区别呢?
如果应用防抖节流,比方提早300ms显示则意味着所有用户都要延时,在渲染内容较少、用户CPU性能较好的状况下也是会提早300ms,而且你要依据理论状况来调整提早的适合值;然而useDeferredValue
是否提早取决于计算机的性能。
useId
反对同一个组件在客户端和服务端生成雷同的惟一的 ID,防止 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级构造。
function Checkbox() {
const id = useId()
return (
<>
>
)
}
这里波及到 SSR 局部常识,这里不开展了,能够浏览该篇文章了解:
这两个 Hook 日常开发根本用不到,简略带过
useSyncExternalStore
个别是第三方状态治理库应用如 Redux
。它通过强制的同步状态更新,使得内部 store 能够反对并发读取。它实现了对外部数据源订阅时不再须要 useEffect
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
useInsertionEffect
仅限于 css-in-js 库应用。它容许 css-in-js 库解决在渲染中注入款式的性能问题。 执行机会在 useLayoutEffect
之前,只是此时不能应用ref和调度更新,个别用于提前注入款式。
useInsertionEffect(() => {
console.log('useInsertionEffect 执行')
}, [])