React18新特性学习(2)

281 阅读6分钟

(前两天电脑坏了去修了一通)

五、useTransition

React的状态更新可以分为两类:

  • 紧急更新(Urgent updates):比如打字、点击、拖动等,需要立即响应的行为,如果不立即响应会给人很卡的感觉。
  • 过度更新(Transition updates):将UI从一个视图过渡到另一个视图。有些延迟,不过即相应是可以接受的。

并发模式只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。

所以它提供了startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的。

1. startTransition

首先我们先来学习startTransition,被它包裹住的setState()会被认为是低优先级的。例如:

 import {useState,startTransition} from 'react'
 ​
 export default function App(){
     const [text,setText] = useState('')
     const [search,setSearch] = useState('')
     
   return(
     <div>
       <h1>{text}</h1>
       <h2>{search}</h2>
       <button onClick={()=>{
         setText('我会先执行')
         startTransition(()=>{//使用startTransition包裹起来
           console.log('执行')
           setSearch('我会后执行')
         })}
       }>按钮</button>
     </div>
   )
 }

如果说startTransition中包裹的是关系到比较复杂的更新,那么它还能够帮我们做到一个节流效果。这个例子由于动作都很快看不太出来,就简单演示一下写法,下面来一个比较迟吃资源的更新:

 import {useState,startTransition, useEffect} from 'react'
 ​
 function List(props){  //List组件
   const [list,setList] = useState([])
   useEffect(()=>{
     console.log('执行')
     setTimeout(()=>{
       setList(new Array(3000).fill(props.search))
     },100)
   },[props.search])
   return <ul>
     {list.map((item,index)=>{
       return <h3 key={index}>{index}-{item}</h3>
     })}
   </ul>
 }
 ​
 export default function App() {
   const [text,setText] = useState('')
   const [search,setSearch] = useState('')
 ​
   return (
     <div>
       <input value={text} onChange={(e)=>{
         setText(e.target.value) //先更改输入框的内容,防止用户体验不佳
 ​
         startTransition(()=>{
           setSearch(e.target.value)
         })
       }} />
       <List search={search}></List>
     </div>
   );
 }

这样就可以看出来效果啦!

2. useTransition

在上面的代码前提下进行修改:

 import {useState,useTransition, useEffect} from 'react'
 ​
 function List(props){......}  //List组件
 ​
 export default function App() {
   const [text,setText] = useState('')
   const [search,setSearch] = useState('')
 ​
   //isPending若为true说明还在等待中,为false说明可以执行startTransition了
   const [isPending,startTransition] = useTransition()
 ​
   return (
     <div>
       <input value={text} onChange={(e)=>{
         setText(e.target.value) //先更改输入框的内容,防止用户体验不佳
 ​
         startTransition(()=>{
           setSearch(e.target.value)
         })
       }} />
       {isPending?'loading...':<List search={search}></List>}
       //要是这样写就不会有节流的效果如果想要有节流的效果还是只能像下面分开
       //{isPending&&'loading...'}
       //<List search={search}></List>
     </div>
   );
 }

六、useDeferredValue

Deferred: 有延迟、推迟的意思。

一个useState会产生一个 状态state ,和一个用来更新状态的 setState()。上面的useTransition包裹的是setState(),而这里的useDeferredValue包裹的是state。依旧用上面的例子来尝试:

 import {useState,useDeferredValue, useEffect} from 'react'
 ​
 export default function App() {
   const [text,setText] = useState('')
   const [search,setSearch] = useState('')
 ​
   return (
     <div>
       <input value={text} onChange={(e)=>{
         setText(e.target.value)
         setSearch(e.target.value)
       }} />
       //像这样把 传给List组件的search 标记为低优先级的,等input框都渲染完了,再来更新
       <List search={useDeferredValue(search)}></List>
     </div>
   );
 }

来总结一下useTransition和useDeferredValue的特点嗷:

  • 相同:useDeferredValue本质上与useTransition内部实现一样,都是标记成了延迟更新任务。
  • 不同:useTransition是把更新任务变成了延迟更新任务,而useDeferredValue是把一个状态变成了延迟的状态

七、严格模式

之前讲过,在root.render中,如果加上<React.StrictMode>,在生产环境下处于严格模式,它会帮我们渲染两次来测试排错,如果我们在控制台打印东西 这样通常会混淆我们观察。浏览器中安装扩展插件大于4.18版本的React DevTools并且打开就可以让第二次渲染(测试)的打印数据变成暗灰色

八、Suspense组件的变化

1. Suspense组件

常见应用场景:懒加载代码分割请求数据等待时loading的显示

1.1 懒加载

假设在一个页面中我们引入几个js组件,但在首页加载的时候,其实会把它们合并成一个js中下载,一旦组件变多那么这个js就会非常大,看到首页就会非常慢,用户体验很差。下面来做个小栗子:

首页拥有两个按钮 “1” 和 “2”。点击会分别在下方展示One.js组件和Two.js。默认是One.js。所以我们的懒加载应该是只加载One.js,而点到按钮“2”的时候,才加载Two.js。

 import React, {Suspense, useState} from 'react'
 // import One from './components/One'
 // import Two from './components/Two'
 //不用以上引入方式,而使用lazy的引入方式
 const One = React.lazy(()=>import('./components/One.js'))
 const Two = React.lazy(()=>import('./components/Two.js'))
 ​
 export default function App() {
   const [num,setNum] = useState(1)
 ​
   return (
     <div>
       <button onClick={()=>{setNum(1)}}>one</button>
       <button onClick={()=>{setNum(2)}}>two</button>
       //用Suspense包住
       //其中 fallback 是当中间的组件没有加载出来的时候,可以拿来代替的loading组件(边境设置)
       <Suspense fallback={<div>loading...</div>}>
         //如果在React17没有fallback就会报错了,但18中不写fallback它就默认为null了
         {
           num===1?<One></One>:<Two></Two>
         }
       </Suspense>
     </div>
   );
 }

实现后我们可以在控制台的网络栏看到,一开始只加载了One组件,而当我们点击按钮2的时候,才会加载Two组件。

1.2 异步请求数据

在异步请求数据的时候,使用Suspense会有所不同:

 //App.js
 import {Suspense} from 'react'
 ​
 function Title(){
   //获取数据
   return <div></div>
 }
 ​
 function Content(){
   //获取数据
   return <div></div>
 }
 ​
 export default function App() {
 ​
   return (
     <div>
       <Suspense fallback={<div>title-loading</div>}>
         <Title></Title>
       </Suspense>
       <Suspense fallback={<div>content-loading</div>}>
         <Content></Content>
       </Suspense>
     </div>
   );
 }

上面先写了一个大概的框架,重点在于 组件的获取数据部分。如果简单地用useEffect放在里面,Suspense是监察不到的,触发不了fallback。所以我们模拟一下从后台获取的数据的样子,还是边看代码边学习:

我们创建一个假装从后台获取数据的一个js:fakeApi.js:(基于官方demo修改)

 //fakeApi.js
 export function fetchProfileData(){
   let titlePromise = fetchTitle()
   let contentPromise = fetchContent()
   return {
     title: wrapPromise(titlePromise),
     content: wrapPromise(contentPromise)
   }
 }
 ​
 function wrapPromise(promise){
   let status = 'pending'
   let result
   let suspender = promise.then(
     (r)=>{
       status = "success"
       result = r
     },
     (e)=>{
       status = "error"
       result = e
     })
     return {  //返回函数,为了构建闭包,为的是重复调用时status、result还在。
       read(){  
         if(status==="pending"){ //一开始promise.then还未拿到数据,status为pending
           throw suspender
         }else if(status==="error"){
           throw result
         }else if(status==="success"){
           return result
         }
       }
     }
 }
 ​
 function fetchTitle(){
   return new Promise((resolve)=>{
     setTimeout(()=>{
       resolve({title:'高数A'})
     },2000)  //模拟延迟2秒获得数据
   })
 }
 ​
 function fetchContent(){
   return new Promise((resolve)=>{
     setTimeout(()=>{
       resolve({content:'函数与极限...'})
     },4000)  //模拟延迟4秒获得数据
   })
 }

为什么要这么写呢,因为在Suspense的源码中,可以看到,当它包裹的数据还是Promise的时候(也就是还在请求中),会被它的componentDidCatch(e)捕获到,在最后render( )的时候他就会返回fallback:

 return <>
     { promise ? fallback : children }//如果在promise,就会返回fallback来渲染
 </>

这样子就能够达到在请求数据的时候,让Suspense能够知道需要渲染fallback中的loading。那么接着就是在App.js中调用:

 //App.js
 import {Suspense} from 'react'
 import {fetchProfileData} from './components/fakeApi'
 ​
 const source = fetchProfileData()
 function Title(){
   //记得还需要read()
   const data = source.title.read()
   return <div>{data.title}</div>
 }
 ​
 function Content(){
   const data = source.content.read()
   return <div>{data.content}</div>
 }
 ​
 export default function App() {
   return (
     <div>
       <Suspense fallback={<div>title-loading</div>}>
         <Title></Title>
       </Suspense>
       <Suspense fallback={<div>content-loading</div>}>
         <Content></Content>
       </Suspense>
     </div>
   );
 }

在页面中能看到效果了,在等待的时候展示的是fallback中的内容。

而且呢,Suspense也是可以嵌套的,比如上面可以写成:

 return (
     <div>
       <Suspense fallback={<div>title-loading</div>}>
         <Title></Title>
         <Suspense fallback={<div>content-loading</div>}>
           <Content></Content>
         </Suspense>
       </Suspense>
     </div>
   );

这样呢,就一定会等到Title信息到了,才会显示内部的信息。但是呢,在这种嵌套中,React17和React18有所不同

  • React17:如果内层Suspense没写fallback,那么它会自动找外层的fallback,所以整个title-loading会持续到Content也读到数据。
  • React18:如果内层Suspense没写fallback,其实之前讲过,它会自动把fallback设置为null,只要Title拿到数据了,就会立即展示,而不会等待Content拿数据,不过Content还在拿数据的途中就会判定为null。

九、其他更新

1. React 空组件的返回值

如果React返回一个空组件,React17只允许返回null。React18也允许返回undefined了。

2. 对于已卸载组件的状态更新的警告删除

在React17时,如果我们对一个已经被卸载的组件的状态进行更新的话,会报错,但报错的描述有一定误导性。也是之前比较多的人讨论的问题,所以官方就把这个报错给删除了。但其实作者原意是想提醒是否有订阅来更改状态,记得取消订阅等等