文/游鹿javascript
多是React15到16的不兼容变动太多,开发者们升级至关痛苦,因此很长一段时间React开发者都没有再发布新版本,而是在 v16 上集成各类新能力,16.3/16.8/16.12 几乎每隔几个版本就有有趣的新特性出现。 html
在长达2年半的 v16 版本后,React团队发布了 v17,同时宣布这一版本的定位是一版技术改造的过渡版本,主要目标是下降后续版本的升级成本。在 v17 以前,不一样版本的 React 没法混用,很重要的一个缘由是以前版本中事件委托是挂在document上的,v17 开始,事件委托挂载到了渲染 React 树的根 DOM 容器中,这使多 React 版本并存成为了可能。(意味着React 17+可混用,老页面维持 v17,新页面使用v18 v19 等)前端
咱们愈来愈能感觉到,React的开发者把升级重点放到了**「渐进升级」**上,仅在v17发布了2个小版本后,v18的alpha就出现了,而且只须要用户作极小、甚至不须要改动就能让现有React APP在 v18 上工做。那么v18中有哪些新变化、新特性呢?java
注:本文内容来自 reactwg/react-18 的部分discussion,笔者翻译阅读理解后写出来的。若是有理解不到位的地方,欢迎各位大佬评论区交流、讨论、指正~react
React18的升级策略是「渐进升级」,包括名声在外的并发渲染等在内的新能力都是可选的,不会马上对组件行为带来任何明显的破坏性变化。git
You can upgrade to React 18 with minimal or no changes to your application code, with a level of effort comparable to a typical major React release. github
你几乎不须要对应用程序中的代码进行任何改动就能够直接升级到 React 18,并不会比以往的 React 版本升级要困难。 浏览器
**React官网 ** reactjs.org/blog/2021/0…安全
并发渲染是React底层的一次重要架构设计升级,并发渲染的优点在于提升React APP性能。当你使用了一些React18新特性后,你可能已经用上了并发渲染。 服务器
在React18中, ReactDOM.render()
正式成为Legacy,并增长了新的RootAPI ReactDOM.createRoot()
,他们的用法差异以下:
import ReactDOM from ‘react-dom’;
import App from 'App';
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
import ReactDOM from ‘react-dom’;
import App from 'App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
复制代码
能够看到,经过新的API,咱们能够为一个React App建立多个根节点,甚至在将来能够用不一样版本的React来建立。React18 保留了上述两种用法,老项目不想改仍然能够用 ReactDOM.render()
;新项目想提高性能,能够用 ReactDOM.createRoot()
借并发渲染的东风。
Batching is when React groups multiple state updates into a single re-render for better performance.
为了使应用得到更好的性能,React把屡次的状态更新(state updates),合并到一次渲染中。 React17只会把浏览器事件(如点击)发生期间的状态更新合并掉。而React18会把事件处理器发生后的状态更新也合并掉。举个例子:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClickPrev() {
setCount(c => c - 1); // Does not re-render yet
setFlag(f => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
function handleClickNext() {
fetchSomething().then(() => {
// React 17 and earlier does NOT batch these because
// they run *after* the event in a callback, not *during* it
setCount(c => c + 1); // Causes a re-render
setFlag(f => !f); // Causes a re-render
});
}
return (
<div> <button onClick={handleClickPrev}>Prev</button> <button onClick={handleClickNext}>Next</button> <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> </div>
);
}
复制代码
在React 18中,只要使用 新的 Root API ReactDOM.createRoot()
方法,就能直接享受自动batching的能力!这里列举一些自动更新的场景:
batching 是安全的,但也存在一些特殊状况不但愿batching发生,好比:你须要在状态更新后,马上读取新DOM上的数据等。这种状况下请使用 ReactDOM.flushSync()
(React官方不推荐常态化使用这一API):
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}
复制代码
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 }));
});
};
复制代码
若是不想经过调整代码逻辑的方式进行修正,能够直接采用 ReactDOM.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 }));
});
};
复制代码
Suspense早在React16就以试验性API的形式出来了,相比较旧版本的Legacy Suspense,新版本的Concurrent Suspense更符合用户直觉。
React官方说这两个版本存在比较小的差别,但因为新版 Suspense 的实现是基于并发渲染的,因此这仍然是一个Breaking Changes,这里介绍下差别:
<Suspense fallback={<Loading />}>
<ComponentThatSuspends />
<Sibling />
</Suspense>
复制代码
旧版本Legacy Suspense | 新版本Concurrent Suspense | |
---|---|---|
从表现上看 | **<Sibling>** ** 组件是当即渲染到DOM中** |
**<Sibling>** ** 组件并不会当即渲染到DOM中** **<ComponentThatSuspends>** 组件完成数据解析后(英文是resolve)才会被触发 |
从原理上看 | <ComponentThatSuspends> 时,它会被跳过<Sibiling> 组件会被渲染到DOM Tree中,只不过它会被设置 display: hidden 直到 <ComponentThatSuspends> resovle才会出现。 |
<Suspense> 中的数据都resolve以后,才会在一个单一的、一致的批次中同时提交整个树。从开发角度来讲,新版本的 <Suspense> 行为更符合预期由于它可被预测。 |
现存SSR架构原理很少解释,它的问题在于,一切都是串行的,在任一前序任务没完成以前,后一任务都没法开始,也就是“All or Nothing”,通常是以下流程:
**React18提供了Suspense,打破了这种串行的限制,优化前端的加载速度和可交互所需等待时间。**这一SSR架构依赖两个新特性:
pipeToNodeWritable
<Suspense>
新版本Suspense SSR速度更快的原理是什么呢?如下面的结构为例:
<Layout>
<NavBar />
<Sidebar />
<RightPane> <Post /> <Suspense fallback={<Spinner />}> <Comments /> </Suspense> </RightPane>
</Layout>
复制代码
如上一个页面结构,<Comments>
是经过接口异步获取的,这一过程数据请求比较慢,因此咱们把它包裹在 <Suspense>
里。在普通的SSR架构里,通常只能等<Comments>
加载进来以后才能进行下一环节。在新模式下,HTML流首先返回的内容里是不会有 <Comments>
组件相关HTML信息的,取而代之的是 <Spinnger>
的HTML:
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
复制代码
等服务端获取到了 <Comments>
的数据后,React再把后加入的 <Comments>
的HTML信息,经过同一个流(stream)发送过去,React会建立一个超小的内联
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script> // This implementation is slightly simplified document.getElementById('sections-spinner').replaceChildren( document.getElementById('comments') ); </script>
复制代码
因此与传统的 HTML 流不一样,它没必要按自上而下的顺序发生。
代码拆分是咱们经常使用的手段,咱们能够用 React.lazy
把一部分代码从主包中拆出来。
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}> <Comments /> </Suspense>
复制代码
在React18之前,React.lazy
不支持服务端渲染,即使是最流行的解决方案也让你们从「为了代码拆分不使用SSR」和「使用SSR但要在全部js加载完成后才能hydratie」中二选一。
而在React18版本,被 <Suspense>
包裹的子组件能够延后hydratie,这一行为是React内部自动作掉的,因此React.lazy
也默认支持了SSR。
使用此 API 能够防止内部函数执行拖慢 UI 响应速度。
以查询选择器为例:用户输入关键词,请求远程数据并展现搜索结果。
// Urgent: Show what was typed
setInputValue(input);
// Not urgent: Show the results
setSearchQuery(input);
复制代码
输入文字时用户是但愿获得即时反馈的,而查询并展现结果则是容许有延迟的(事实上,开发者常常人为地用一些手段让他们延迟更新,好比debounce)
在引入 startTransition 后用法是:
import { startTransition } from 'react';
// Urgent: Show what was typed
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setSearchQuery(input);
});
复制代码
更新能够分为两类:
其实在React应用中,大部分更新在概念上都应当是Transition Updates,可是出于向后兼容的角度考虑,transition是可选的,因此在React18中默认的更新方式仍然是Urgent Updates,想要使用Transition Updates能够把函数用startTransition
包裹起来。
主要有两点: