React18带来了几个非常实用的新特性,同时也没有额外的升级成本,值得仔细看一看。
下面是几个关键信息:
React18工作小组。利用社区讨论React18发布节奏与新特性。发布计划。目前还没有正式发布,不过alpha版已经可用了,安装alpha版。React18新特性介绍。虽然还未正式发布,但特性介绍可以先行,本周精读主要就是解读这篇文档。精读总的来说,React18带来了3大新特性:
Automaticbatching。ConcurrentAPIS。SSRforSuspense。同时为了开启新的特性,需要进行简单的render函数升级。
Automaticbatchingbatching是指,React可以将回调函数中多个setState事件合并为一次渲染。
也就是说,setState并不是实时修改State的,而将多次setState调用合并起来仅触发一次渲染,既可以减少程序数据状态存在中间值导致的不稳定性,也可以提升渲染性能。可以理解为如下代码所示:
functionhandleClick(){setCount((c)=c+1);setFlag((f)=!f);//仅触发一次渲染}
但可惜的是,React18以前,如果在回调函数的异步调用中执行setState,由于丢失了上下文,无法做合并处理,所以每次setState调用都会立即触发一次重渲染:
functionhandleClick(){//React18以前的版本fetch(/*...*/).then(()={setCount((c)=c+1);//立刻重渲染setFlag((f)=!f);//立刻重渲染});}
而React18带来的优化便是,任何情况都可以合并渲染了!即使在promise、timeout或者event回调中调用多次setState,也都会合并为一次渲染:
functionhandleClick(){//React18+fetch(/*...*/).then(()={setCount((c)=c+1);setFlag((f)=!f);//仅触发一次渲染});}
当然如果你非要setState调用后立即重渲染也行,只需要用flushSync包裹:
functionhandleClick(){//React18+fetch(/*...*/).then(()={ReactDOM.flushSync(()={setCount((c)=c+1);//立刻重渲染setFlag((f)=!f);//立刻重渲染});});}
开启这个特性的前提是,将ReactDOM.render替换为ReactDOM.createRoot调用方式。
新的ReactDOMRenderAPI升级方式很简单:
constcontainer=document.getElementById("app");//旧renderAPIReactDOM.render(Apptab="home"/,container);//新createRootAPIconstroot=ReactDOM.createRoot(container);root.render(Apptab="home"/);
API修改的主要原因还是语义化,即当我们多次调用render时,不再需要重复传入container参数,因为在新的API中,container已经提前绑定到root了。
ReactDOM.hydrate也被ReactDOM.hydrateRoot代替:
constroot=ReactDOM.hydrateRoot(container,Apptab="home"/);//注意这里不用调用root.render()
这样的好处是,后续如果再调用root.render(Appx/)进行重渲染,我们不用关心这个root来自createRoot或者hydrateRoot,因为后续API行为表现都一样,减少了理解成本。
ConcurrentAPIS首先要了解ConcurrentMode是什么。
简单来说,ConcurrentMode就是一种可中断渲染的设计架构。什么时候中断渲染呢?当一个更高优先级渲染到来时,通过放弃当前的渲染,立即执行更高优先级的渲染,换来视觉上更快的响应速度。
有人可能会说,不对啊,中断渲染后,之前渲染的CPU执行不就浪费了吗,换句话说,整体执行时常增加了。这句话是对的,但实际上用户对页面交互及时性的感知是分为两种的,第一种是即时输入反馈,第二种是这个输入带来的副作用反馈,比如更新列表。其中,即使输入反馈只要能优先满足,即便副作用反馈更慢一些,也会带来更好的体验,更不用说副作用反馈大部分情况会因为即使输入反馈的变化而作废。
由于React将渲染DOM树机制改为两个双向链表,并且渲染树指针只有一个,指向其中一个链表,因此可以在更新完全发生后再切换指针指向,而在指针切换之前,随时可以放弃对另一颗树的修改。
以上是背景输入。React18提供了三个新的API支持这一模式,分别是:
startTransition。useDeferredValue。。后两个文档还未放出,所以本文只介绍第一个API:startTransition。首先看一下用法:
import{startTransition}from"react";//紧急更新:setInputValue(input);//标记回调函数内的更新为非紧急更新:startTransition(()={setSearchQuery(input);});
简单来说,就是被startTransition回调包裹的setState触发的渲染被标记为不紧急的渲染,这些渲染可能被其他紧急渲染所抢占。
比如这个例子,当setSearchQuery更新的列表内容很多,导致渲染时CPU占用%时,此时用户又进行了一个输入,即触发了由setInputValue引起的渲染,此时由setSearchQuery引发的渲染会立刻停止,转而对setInputValue渲染进行支持,这样用户的输入就能快速反映在UI上,代价是搜索列表响应稍慢了一些。而一个transition被打断的状态可以通过isPending访问到:
import{useTransition}from"react";const[isPending,startTransition]=useTransition();
其实这比较符合操作系统的设计理念,我们知道在操作系统是通过中断响应底层硬件事件的,中断都非常紧急(因为硬件能存储的消息队列非常有限,操作系统不能即使响应,硬件的输入可能就丢失了),因此要支持抢占式内核,并在中断到来时立刻执行中断(可能把不太紧急的操作放到下半部执行)。
对前端交互来说,用户角度发出的“中断”一般来自键盘或鼠标的操作,但不幸的是,前端框架甚至是JS都过于上层,它们无法自动识别:
哪些代码是紧急中断产生的。比如onClick就一定是用户鼠标点击产生的吗?不一定,可能是xxx.onClick主动触发的,而非用户触发。用户触发的就一定是紧急中断吗?不一定,比如键盘输入后,setInputValue是紧急的,而更新查询列表的setSearchQuery就是非紧急的。我们要理解到前端场景对用户操作感知的局限性,才能理解为什么必须手动指定更新的紧急程度,而不能像操作系统一样,上层程序无需感知中断的存在。
SSRforSuspense完整名称是:StreamingSSRwithselectivehydration。
即像水流一样,打造一个从服务端到客户端持续不断的渲染管线,而不是renderToString那样一次性渲染机制。selectivehydration表示选择性水合,水合指的是后端内容打到前端后,JS需要将事件绑定其上,才能响应用户交互或者DOM更新行为,而在React18之前,这个操作必须是整体性的,而水合过程可能比较慢,会引起全局的卡顿,所以选择性水合可以按需优先进行水合。
所以这个特性其实是转为SSR准备的,而功能启用载体就是Suspense(所以以后不要再认为Suspense只是一个loading作用)。其实在Suspense设计之初,就是为了解决服务端渲染问题,只是一开始只实装了客户端测的按需加载功能,后面你会逐渐发现React团地逐渐赋予了Suspense更多强大能力。
SSRforSuspense解决三个主要问题:
SSR模式下,如果不同模块取数效率不同,会因为最慢的一个模块拖慢整体HTML吞吐时间,这可能导致体验还不如非SSR来的好。举一个极端情况,假设报表中一个组件依赖了慢查询,需要五分钟数据才能出来,那么SSR的后果就是白屏时间拉长到5分钟。即便SSR内容打到了页面上,由于JS没有加载完毕,所以根本无法进行hydration,整个页面处于无法交互状态。即便JS加载完了,由于React18之前只能进行整体hydration,可能导致卡顿,导致首次交互响应不及时。在React18的serverrender中,只要使用pipeToNodeWritable代替renderToString并配合Suspense就能解决上面三个问题。
使用pipeToNodeWriteable可以看这个例子。
最大的区别在于,服务端渲染由简单的res.send改成了res.socket,这样渲染就从单次行为变成了持续性的行为。
那么React18的SSR到底有怎样的效果呢?这篇介绍文档的图建议看一看,非常直观,这里我简要描述一下:
被Suspense包裹的区块,在服务端渲染时不会阻塞首次吞吐,而且在这个区块准备完毕后(包括异步取数)再实时打到页面中(以HTML模式,此时还没有hydration),在此之前返回的是fallback的内容。hydration的过程也是逐步的,这样不会导致一下执行所有完整的js导致页面卡顿(hydration其实就是React里写的回调注册、各类Hooks,整个应用的量非常庞大)。hydration因为被拆成多部,React还会提前监听鼠标点击,并提前对点击区域优先级进行hydration,甚至能抢占已经在其他区域正在进行中的hydration。那么总结一下,新版SSR性能提高的秘诀在于两个字:按需。
而这个难点在于,SSR需要后端到前端的配合,在React18之前,后端到前端的过程完全没有优化,而现在将SSRHTML的吞吐改成多次,按需,并且水合过程中还支持抢占,因此性能得到进一步提升。
总结结合起来看,React18