react18 对比之前的版本在以下方面带来提升和优化
- automatic batching
- startTransition
- 全新的SSR
automatic batching
了解18优化的automatic batching之前 先了解下之前的batching
18之前的batching
Demo1
function App() {
const [count, setCount] = useState(0);
const [color, setColor] = useState('black');
function handleClick() {
setCount(c => c + 1); //part1. 当运行完这行代码并不会立即触发re-render
setColor('pink'); //part2. 当运行完这行代码并不会立即触发re-render
//当都运行完成 会涉及到re-render的更新的操作会统一合并处理,此时此刻part1 part2 为一个batching
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: color }}>{count}</h1>
</div>
);
}
复制代码
上面的demo 会将part1和part2分区也就是batching(也就是合并一起执行re-render)这样能节省re-render的开销(reconsiler阶段的diff算法和commit及render阶段的渲染开销)
可能看到这边的小朋友会觉得 咦~ 这个batching不是挺好的 为什么还要优化呢?
因为之前的batching只能在浏览器的事件中进行分区为一个区(这里加个思考,是否之前batcing是基于事件及shedule机制处理的,以后了解了解再解答吧,或者有大佬解答解答不胜感激),但是对于一些异步操作没办法batching 例如:promise、延时系列(setTimeOut。。。)…
Demo2
function App() {
const [count, setCount] = useState(0);
const [color, setColor] = useState('black');
function handleClick() {
fetchData().then(() => {
//没办法合为一个batching,虽然是事件但是是在callback里面,click事件结束后才运行无法batching
setCount(c => c + 1); //part1. 一个独立的batching,会进行re-render
setColor('pink'); //part2. 一个独立的batching,会进行re-render
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: color }}>{count}</h1>
</div>
);
}
复制代码
18的automatic batching
如何触发batcing效果
18的自动batcing是基于createRoot方法,无论是什么方式,都会自动被分区
那就意味着demo2的promise请求会被分区到一起,统一re-render
!注意,一定是基于createRoot方法,render方法还是保持原先方式
Demo3
function App() {
const [count, setCount] = useState(0);
const [color, setColor] = useState('black');
const [status, setStatus] = useState("");
function handleClick() {
console.log("=== click ===");
setStatus("开始请求了"); // a
fetchSomething().then(() => {
setCount((c) => c + 1); // b
setColor('pink'); // c
setStatus("结束请求了"); // d
});
setStatus("我就请求了"); // e
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: color }}>{count}</h1>
<h1>{status}</h1>
</div>
);
}
复制代码
上面代码 a e为一个batching ,b c d为一个batching 所以re-render2次
基于createRoot才能实现自动batching:
const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);
复制代码
基于render仍然保持之前batching的机制并不会采用react18的自动batching:
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码
如何消除batching效果
有些时候我们并不需要将所有的变更都分区到同一块一起re-render,于是我们需要使用flushSync
来消除batching
Demo
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});// a
flushSync(() => {
setColor('pink');
});// b
}
复制代码
这样子就a就为1个batching,b也为一个batching,re-render2次
可支持场景
react18之前并不支持异步延时操作,react18的自动batching将会支持到batching,那么也就是说以下方式都可以batching
fetchData().then()
element.addEventListener
setTimeout
18的automatic batching对于class组件
上面的demo都是使用hooks,那对于class组件,automatic batching还是有同样的作用么
答案当然是肯定的
Demo:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
console.log(this.state);// { count: 0, flag: false } 如果不是18 那么上面setState是会触发re-render的,这里两个state的改变被batching在结尾一起re-render
this.setState(({ color }) => ({ color: 'pink' }));
});
};
复制代码
!注意:同样的要基于createRoot方法
startTransition
不知道各位小朋友有无在做频繁改变state从而改变视图,比如用了slider划动条的时候通过变更值来改变视图的时候操作过快或视图过度复杂,会造成视图的卡顿或者是不流畅,我们通常都会加上防抖来减少更改的频次从而减少state的改变使得视图变化的更加平滑,
startTransition是什么?
说startTransition之前我们先来了解一下视图的更新
- 一种是用户需要自己的操作马上呈现在界面上,需要有个实时的反馈 —急迫的呈现
- 还有一种是用户不需要那么实时的反馈,允许延迟展示 —不急迫的呈现
那startTransition又是什么呢,这个是react18对于解决因状态的改变过快导致视图的卡顿,通过使用标记一些过度状态的更新,降低被标记为过度状态更新的优先级,使得那些需要马上显示(急迫的呈现)的状态能够re-render,而不急迫的显示滞后re-render
源码
function startTransition(setPending, callback) {
var previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority));
setPending(true);
var prevTransition = ReactCurrentBatchConfig$2.transition;
ReactCurrentBatchConfig$2.transition = 1;
try {
setPending(false);
callback();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$2.transition = prevTransition;
}
}
复制代码
如何使用startTransition?
通过startTransition方法
startTransition(()=>{
//将需要标志的代码放入startTransition中
setState()
...
})
复制代码
通过useTransition方法
Demo
import { useState, useLayoutEffect, useTransition } from "react";
import * as ReactDOM from "react-dom";
function App() {
const [flag, setFlag] = useState("black");
const [isPending, startTransition] = useTransition();
function handleClick() {
console.log("=== click ===");
startTransition(() => {
setFlag((f) => !f);
});
}
console.log(isPending, "=-=-isPending");
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "pink" }}>0</h1>
<LogEvents />
</div>
);
}
function LogEvents(props) {
useLayoutEffect(() => {
console.log("Commit");
});
console.log("Render");
return null;
}
const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);
复制代码
useTransition是对startTransition的一个包装的hook,isPenging是当前的过渡状态是否被挂起,如果正在被挂起就返回true,如果结束挂起就返回false
注意 如果不需要获取挂起状态那就不需要使用useTransition,直接使用startTransition,原因如下:
注意上面的demo输出
可以看出 render了两下,这个是为什么呢,我们看看useTransition源码
源码
function mountTransition() {
var _mountState2 = mountState(false),
isPending = _mountState2[0],
setPending = _mountState2[1]; // The `start` method never changes.
var start = startTransition.bind(null, setPending);
var hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [isPending, start];
}
复制代码
用了useState记录了isPending的状态,isPending的状态改变渲染1次,startTransition内部执行setXXX状态渲染1次,所以还是酌情使用useTransition
startTransition解决了什么问题?
- 数据请求:当请求大量数据并且要渲染在界面上,可以用它,在数据被isPending的时候加上loading的ui展示
- 实时搜索:在搜索框输入的时候状态是需要急迫的展示,但是结果列表的呈现是不需要马上显示的
- 复杂渲染:点击链接
全新的SSR
在说React18的SSR之前,我们先来了解一下服务端渲染可能提到的几个名词
下面我们来举个例子
从一只小兔子来说明SSR
首先
一只可爱的小兔子由:骨架+组织(结缔组织上皮组织,大白话就是肉,毛发之类的)—这个是构成一个静态的小兔子的基本要件,但是可爱的小兔子怎么能不会动呢,那么就要给它加上行为能力(能吃草,能睡觉,能蹦蹦跳跳)。所以,一个活泼的小兔子是由骨架+组织+行为构成,现在我们要压缩和复原一个小兔子,首先将骨架和组织压缩把它的行为能力抽出来,现在这个小兔子就被处理成 兔子干+行为能力,如何复原呢,兔子干泡水(hydrate 水化)再加上行为能力,一个活泼的小兔子变回来了
那么对应到页面也是同理,服务端返回的是很干瘪的html,
服务端和客户端分别要做
- 服务器上,获取整个应用需要的数据。
- 服务器上,将整个应用呈现为 HTML 并将其发送到响应报文中。
- 客户端上,加载整个应用的 JavaScript 代码。
- 客户端上,将 JavaScript 逻辑连接到整个应用的服务器生成的 HTML(这就是hydrate “水化”)。
以上的整个操作是要立即执行的,意思是每一步的操作完成后才能进行下一操作,如果遇到某个部分请求数据过程略长那么将会阻塞页面的显示,这个个是React18之前存在的问题,如何去解决优化这个问题呢
<Suspense>
Suspense在React18之前的官方文档是这样子介绍的
Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。目前,Suspense 仅支持的使用场景是:通过 React.lazy 动态加载组件。它将在未来支持其它使用场景,如数据获取等。
Suspense 被用作代码分割 配合上lazy能实现比较好的用户体验,不过React.lazy 和 Suspense 技术还不支持服务端渲染,但是React18支持Suspense应用在服务端的
Suspense应用在客户端渲染
Demo
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
复制代码
React18 Suspense应用在服务端渲染
Suspense的使用和客户端渲染一样的用法只是被包含的子组件有数据加载延时情况,但是还得注意 需要如下操作
- 将
ReactDOM.render
切换成ReactDOM.createRoot
- 18之前是通过
renderToString(React.Node): string
,18最新的api是pipeToNodeWritable(React.Node, Writable, Options): Controls
需要我们使用这个方法
pipeToNodeWritable
使用方式
import {pipeToNodeWritable} from 'react-dom/server';
const {startWriting, abort} = pipeToNodeWritable(
<DataProvider data={data}>
<App assets={assets} />
</DataProvider>,
res,
{
onReadyToStream() {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
res.write('<!DOCTYPE html>');
startWriting();
},
onError(x) {
didError = true;
console.error(x);
},
}
);
复制代码
入参
第一个参数:是要转换的reactcomponent
res:服务端的response
第三个参数:各个周期的回调处理
出参
Controls:处理方法对象对象有的方法为 startWriting开始执行方法,abort错误终止处理方法
源码
function pipeToNodeWritable(children, destination, options) {
var request = createRequestImpl(children, destination, options);
var hasStartedFlowing = false;
startWork(request);
return {
startWriting: function () {
if (hasStartedFlowing) {
return;
}
hasStartedFlowing = true;
startFlowing(request);
destination.on('drain', createDrainHandler(destination, request));
},
abort: function () {
abort(request);
}
};
}
复制代码
这块写的不是很细致大家可以参考下面文档包括水化细节,后面有时间再写写
参考文档:
github.com/reactwg/rea…