react17
1、新的JSX转换
React 16原理
babel-loader会预编译JSX为React.createElement(...)
React 17原理
React 17中的 JSX 转换不会将 JSX 转换为 React.createElement,
而是自动从 React 的 package 中引入新的入口函数并调用。
另外此次升级不会改变 JSX 语法,旧的 JSX 转换也将继续工作。
总结
React 17支持新的JSX转换。我们还将对它支持到React 16.14.0,React 15.7.0和0.14.10。
需要注意的是,这是完全选择启用的,您也不必使用它。
之前的JSX转换的方式将继续存在,并且没有计划停止对其支持。
2、事件代理更改
在React 17中,将不再在后台的文档级别附加事件处理程序,不在document对象上绑定事件,改为绑定于每个react应用的rootNode节点,因为各个应用的rootNode肯定不同,所以这样可以使多个版本的react应用同时安全的存在于页面中,不会因为事件绑定系统起冲突。react应用之间也可以安全的进行嵌套。
总结
在React 16和更早的版本中,React将对大多数事件执行document.addEventListener()。
React 17将在后调用rootNode.addEventListener()。
3、事件池(event pooling)的改变
React 17去除了事件池(event pooling),不在需要e.persist(),现在可以直接在异步事件中(回掉或timeout等)拿到事件对象,操作更加直观,不会令人迷惑。e.persist()仍然可用,但是不会有任何效果。
事件池:合成事件对象会被放入池中统一管理。这意味着合成事件对象可以被复用,当所有事件处理函数被调用之后,其所有属性都会被回收释放置空。
事件池的好处是在较旧浏览器中重用了不同事件的事件对象以提高性能,但它对现代浏览器的性能优化微乎其微,反而给开发者带来困惑,因此去除了事件池,因此也没有了事件复用机制。
function handleChange(e) {
// v16中,在异步方法中是拿不到e的,需要事先执行e.persist()
// e.persist()
setTimeout(() => {
console.log(e);
}, 1000);
}
4、副作用异步执行
React 17将副作用清理函数(如果存在)改为异步执行,即在浏览器渲染完毕后执行。
useEffect(() => {
return () => {
// 会在浏览器渲染完毕后执行
}
})
- v17前,组件被卸载时,
useEffect的清理函数都是同步运行的;对于大型应用程序来说,同步会减缓屏幕的过渡(如切换标签) - v17后,
useEffect副作用清理函数是异步执行的,如果要卸载组件,则清理会在屏幕更新后运行
此外,v17 将在运行任何新副作用之前执行所有副作用的清理函数(针对所有组件),v16 只对组件内的副作用保证这种顺序。
5、forwardRef 和 memo组件的行为
React 17中forwardRef 和 memo组件的行为会与常规函数组件和class组件保持一致。它们在返回undefined时会报错。
const Button = forwardRef(() => {
// 这里忘记写return,所以返回了undefined
// React17不会忽略检测它,会返回err
<button />;
});
const Button = memo(() => {
// 这里忘记写return,所以返回了undefined
// React17不会忽略检测它,会返回err
<button />;
});
6、全新的 JSX 转换器
总结下来就是两点:
- 用
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 <h1>Hello World</h1>;
}
// 新的 jsx 转换为
// 由编译器引入(禁止自己引入!)
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
7、更贴近原生浏览器事件
对事件系统进行了一些较小的更改:
onScroll事件不再冒泡,以防止出现常见的混淆- React 的
onFocus和onBlur事件已在底层切换为原生的focusin和focusout事件。它们更接近 React 现有行为,有时还会提供额外的信息。
blur、focus 和 focusin、focusout 的区别:blur、focus 不支持冒泡,focusin、focusout 支持冒泡
- 捕获事件(例如,
onClickCapture)现在使用的是实际浏览器中的捕获监听器。
这些更改会使 React 与浏览器行为更接近,并提高了互操作性。
尽管 React 17 底层已将 onFocus 事件从 focus 切换为 focusin,但请注意,这并未影响冒泡行为。在 React 中,onFocus 事件总是冒泡的,在 React 17 中会继续保持,因为通常它是一个更有用的默认值
React18
1、setState 自动批处理
批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能。比如
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}
在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 会 render 两次,每次 state 变化更新一次
}, 1000);
而在 React 18 中,所有的状态更新,都会自动使用批处理,不关心场景。
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会 re-render 一次,这就是批处理
}, 1000);
如果你在某种场景下不想使用批处理,你可以通过 flushSync来强制同步执行(比如:你需要在状态更新后,立刻读取新 DOM 上的数据等。)
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React 更新一次 DOM
flushSync(() => {
setFlag(f => !f);
});
// React 更新一次 DOM
}
React 18 的批处理在绝大部分场景下是没有影响,但在 Class 组件中,如果你在两次 setState 中间读取了 state 值,会出现不兼容的情况,如下示例。
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// 在 React17 及之前,打印出来是 { count: 1, flag: false }
// 在 React18,打印出来是 { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
当然你可以通过 flushSync来修正它。
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// 在 React18,打印出来是 { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
2、concurrent mode 并发模式
Concurrent Mode(以下简称 CM)翻译叫并发模式
CM 本身并不是一个功能,而是一个底层设计,它使 React 能够同时准备多个版本的 UI。
在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。
在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行。
CM 模式有点类似计算机的多任务处理,处理器在同时进行的应用程序之间快速切换,也许 React 应该改名叫 ReactOS 了。
这里举个例子:我们正在看电影,这时候门铃响了,我们要去开门拿快递。在 React 18 以前,一旦我们开始看电影,就不能被终止,必须等电影看完之后,才会去开门。而在 React 18 CM 模式之后,我们就可以暂停电影,等开门拿完快递之后,再重新继续看电影。
不过对于普通开发者来说,我们一般是不会感知到 CM 的存在的,在升级到 React 18 之后,我们的项目不会有任何变化。
我们需要关注的是基于 CM 实现的上层功能,比如 Suspense、Transitions、streaming server rendering(流式服务端渲染), 等等。
React 18 的大部分功能都是基于 CM 架构实现出来的,并且这只是一个开始,未来会有更多基于 CM 实现的高级能力。 18前:同步不可中断模式,18:异步可中断的更新
3、startTransition
我们如果要主动发挥 CM 的优势,那就离不开 startTransition。
React 的状态更新可以分为两类:
- 紧急更新(Urgent updates):比如打字、点击、拖动等,需要立即响应的行为,如果不立即响应会给人很卡,或者出问题了的感觉
- 过渡更新(Transition updates):将 UI 从一个视图过渡到另一个视图。不需要即时响应,有些延迟是可以接受的。
我以前会认为,CM 模式会自动帮我们区分不同优先级的更新,一键无忧享受。很遗憾的是,CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。
这是因为 React 并不能自动识别哪些更新是优先级更高的。
const [inputValue, setInputValue] = useState();
const onChange = (e)=>{
setInputValue(e.target.value);
// 更新搜索列表
setSearchQuery(e.target.value);
}
return (
<input value={inputValue} onChange={onChange} />
)
比如以上示例,用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。
但是 React 确实没有能力自动识别。所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的。
// 紧急的
setInputValue(e.target.value);
startTransition(() => {
setSearchQuery(input); // 非紧急的
});
如上代码,我们通过 startTransition来标记一个非紧急更新,让该状态触发的变更变成低优先级的。
4、引入新的root API
支持new concurrent renderer 并发模式的渲染
// react17
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
const root = document.getElementById('root')
ReactDom.render(<App />, root)
//卸载组件
ReactDom.unmountComponentAtNode(root)
// React18
import React from 'react'
import ReactDom from 'react-dom/client'
import App from './App'
const root = document.getElementById('root')
ReactDom.createRoot(root).render(App)
// 卸载
root.unmount()
5、react 组件返回值更新
react17 return null undefined报错 react18支持null undefined返回
6、Suspense 支持 SSR
SSR 一次页面渲染的流程:
- 服务器获取页面所需数据
- 将组件渲染成 HTML 形式作为响应返回
- 客户端加载资源
- (hydrate)执行 JS,并生成页面最终内容
上述流程是串行执行的,v18前的 SSR 有一个问题就是它不允许组件"等待数据",必须收集好所有的数据,才能开始向客户端发送HTML。如果其中有一步比较慢,都会影响整体的渲染速度。
v18 中使用并发渲染特性扩展了Suspense的功能,使其支持流式 SSR,将 React 组件分解成更小的块,允许服务端一点一点返回页面,尽早发送 HTML和选择性的 hydrate, 从而可以使SSR更快的加载页面
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
具体可参考文章 React 18 中新的 Suspense SSR 架构
7、新Hooks
useInsertionEffect
useInsertionEffect(didUpdate);
这个 Hooks 只建议 css-in-js库来使用。这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 生效之前,一般用于提前注入 <style> 脚本。
useDeferredValue
const deferredValue = useDeferredValue(value);
useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。
之前 startTransition 的例子,就可以用 useDeferredValue来实现。
const [treeLeanInput, setTreeLeanInput] = useState(0);
const deferredValue = useDeferredValue(treeLeanInput);
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value)
}
return (
<>
<input type="range" value={treeLeanInput} onChange={changeTreeLean} />
<Pythagoras lean={deferredValue} />
</>
)
useId
const id = useId();
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容。原理是每个 id 代表该组件在组件树中的层级结构。
useSyncExternalStore
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
useSyncExternalStore 能够让 React 组件在 Concurrent Mode 下安全地有效地读取外接数据源。在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。useSyncExternalStore 一般是三方状态管理库使用,一般我们不需要关注。