「React18 新特性」深入淺出用戶體驗大師—transition

在 React 18 中,引進了一個新的 API —— startTransition 還有二個新的 hooks —— useTransitionuseDeferredValue,本質上它們離不開一個概念 transition

什麼叫做 transition 英文翻譯爲 ‘過渡’,那麼這裏的過渡指的就是在一次更新中,數據展現從無到有的過渡效果。用 ReactWg 中的一句話描述 startTransition 。

在大屏幕視圖更新的時,startTransition 能夠保持頁面有響應,這個 api 能夠把 React 更新標記成一個特殊的更新類型 transitions ,在這種特殊的更新下,React 能夠保持視覺反饋和瀏覽器的正常響應。

單單從上述對 startTransition 的描述,我們很難理解這個新的 api 到底解決什麼問題。不過不要緊,接下來讓我逐步分析這個 api 到底做了什麼,以及它的應用場景。

二 transition 使命

1 transition 的誕生

爲什麼會出現 Transition 呢?Transition 本質上解決了渲染併發的問題,在 React 18 關於 startTransition 描述的時候,多次提到 ‘大屏幕’ 的情況,這裏的大屏幕並不是單純指的是尺寸,而是一種數據量大,DOM 元素節點多的場景,比如數據可視化大屏情況,在這一場景下,一次更新帶來的變化可能是巨大的,所以頻繁的更新,執行 js 事務頻繁調用,瀏覽器要執行大量的渲染工作,所以給用戶感覺就是卡頓。

Transition 本質上是用於一些不是很急迫的更新上,在 React 18 之前,所有的更新任務都被視爲急迫的任務,在 React 18 誕生了 concurrent Mode 模式,在這個模式下,渲染是可以中斷,低優先級任務,可以讓高優先級的任務先更新渲染。可以說 React 18 更青睞於良好的用戶體驗。從  concurrent Modesusponse 再到 startTransition 無疑都是圍繞着更優質的用戶體驗展開。

startTransition 依賴於 concurrent Mode 渲染併發模式。也就是說在 React 18 中使用 startTransition ,那麼要先開啓併發模式,也就是需要通過 createRoot 創建 Root 。我們先來看一下兩種模式下,創建 Root 區別。

傳統 legacy 模式

import ReactDOM from 'react-dom'
/* 通過 ReactDOM.render  */
ReactDOM.render(
    <App />,
    document.getElementById('app')
)

v18 concurrent Mode 併發模式

import ReactDOM from 'react-dom'
/* 通過 createRoot 創建 root */
const root =  ReactDOM.createRoot(document.getElementById('app'))
/* 調用 root 的 render 方法 */
root.render(<App/>)

上面說了 startTransition 使用條件,接下來探討一下 startTransition 到底應用於什麼場景。前面說了 React 18 確定了不同優先級的更新任務,爲什麼會有不同優先級的任務。世界上本來沒有路,走的人多了就成了路,優先級產生也是如此,React 世界裏本來沒有優先級,場景多了就出現了優先級。

如果一次更新中,都是同樣的任務,那麼也就無任務優先級可言,統一按批次處理任務就可以了,可現實恰好不是這樣子。舉一個很常見的場景:就是有一個 input 表單。並且有一個大量數據的列表,通過表單輸入內容,對列表數據進行搜索,過濾。那麼在這種情況下,就存在了多個併發的更新任務。分別爲

第一種類型的更新,在輸入的時候,希望是的視覺上馬上呈現變化,如果輸入的時候,輸入的內容延時顯示,會給用戶一種極差的視覺體驗。第二種類型的更新就是根據數據的內容,去過濾列表中的數據,渲染列表,這個種類的更新,和上一種比起來優先級就沒有那麼高。那麼如果 input 搜索過程中用戶更優先希望的是輸入框的狀態改變,那麼正常情況下,在 input 中綁定 onChange 事件用來觸發上述的兩種類的更新。

const handleChange=(e)=>{
   /* 改變搜索條件 */ 
   setInputValue(e.target.value)
   /* 改變搜索過濾後列表狀態 */
   setSearchQuery(e.target.value)
}

上述這種寫法,那麼 setInputValuesetSearchQuery 帶來的更新就是一個相同優先級的更新。而前面說道,輸入框狀態改變更新優先級要大於列表的更新的優先級。 ,這個時候我們的主角就登場了。用 startTransition 把兩種更新區別開。

const handleChange=()=>{
    /* 高優先級任務 —— 改變搜索條件 */
    setInputValue(e.target.value)
    /* 低優先級任務 —— 改變搜索過濾後列表狀態  */
    startTransition(()=>{
        setSearchQuery(e.target.value)
    })
}

2 模擬場景

接下來我們模擬一下上述場景。流程大致是這樣的:

/*  模擬數據  */
const mockDataArray = new Array(10000).fill(1)
/* 高量顯示內容 */
function ShowText({ query }){
   const text = 'asdfghjk'
   let children
   if(text.indexOf(query) > 0 ){
       /* 找到匹配的關鍵詞 */
       const arr = text.split(query)
       children = <div>{arr[0]}<span style={{ color:'pink' }} >{query}</span>{arr[1]} </div>
   }else{
      children = <div>{text}</div>
   }
   return <div>{children}</div>
}
/* 列表數據 */
function List ({ query }){
    console.log('List渲染')
    return <div>
        {
           mockDataArray.map((item,index)=><div key={index} >
              <ShowText query={query} />
           </div>)
        }
    </div>
}
/* memo 做優化處理  */
const NewList = memo(List)

接下來就是 App 組件編寫。

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const [ isTransition , setTransion ] = React.useState(false)
    const [ query ,setSearchQuery  ] = React.useState('')
    const handleChange = (e) ={
        /* 高優先級任務 —— 改變搜索條件 */
        setInputValue(e.target.value)
        if(isTransition){ /* transition 模式 */
            React.startTransition(()=>{
                /* 低優先級任務 —— 改變搜索過濾後列表狀態  */
                setSearchQuery(e.target.value)
            })
        }else{ /* 不加優化,傳統模式 */
            setSearchQuery(e.target.value)
        }
    }
    return <div>
        <button onClick={()=>setTransion(!isTransition)} >{isTransition ? 'transition' : 'normal'} </button>
        <input onChange={handleChange}
            placeholder="輸入搜索內容"
            value={value}
        />
       <NewList  query={query} />
    </div>
}

我們看一下 App 做了哪些事情。

常規模式下效果:

transtion 模式下效果:

整體效果:

總結: 通過上面可以直觀的看到 startTransition 在處理過渡任務,優化用戶體驗上起到了舉足輕重的作用。

3 爲什麼不是 setTimeout

上述的問題能夠把 setSearchQuery 的更新包裝在 setTimeout 內部呢,像如下這樣。

const handleChange=()=>{
    /* 高優先級任務 —— 改變搜索條件 */
    setInputValue(e.target.value)
    /* 把 setSearchQuery 通過延時器包裹  */
    setTimeout(()=>{
        setSearchQuery(e.target.value)
    },0)
}

4.gif

綜上所述,startTransition 相比 setTimeout 的優勢和異同是:

4 爲什麼不是節流防抖

那麼我們再想一個問題,爲什麼不是節流和防抖。首先節流和防抖能夠解決卡頓的問題嗎?答案是一定的,在沒有 transition 這樣的 api 之前,就只能通過防抖節流來處理這件事,接下來用防抖處理一下。

const SetSearchQueryDebounce = useMemo(()=> debounce((value)=> setSearchQuery(value),1000)  ,[])
const handleChange = (e) ={
    setInputValue(e.target.value)
    /* 通過防抖處理後的 setSearchQuery 函數。  */
    SetSearchQueryDebounce(e.target.value)
}

通過上面可以直觀感受到通過防抖處理後,基本上已經不影響 input 輸入了。但是面臨一個問題就是 list 視圖改變的延時時間變長了。那麼 transition 和節流防抖 本質上的區別是:

5 受到計算機性能影響

transition 在處理慢的計算機上效果更加明顯,我們來看一下 Real world example

注意看滑塊速度

三 transition 特性

既然已經講了 transition 的產生初衷,接下來看 transition 的功能介紹 。

1 什麼是過度任務。

一般會把狀態更新分爲兩類:

2 什麼是 startTransition

上邊已經用了 startTransition 開啓過度任務,對於 startTransition 的用法,相信很多同學已經清楚了。

startTransition(scope)

使用

startTransition(()=>{
   /* 更新任務 */
   setSearchQuery(value)
})

3 什麼是 useTranstion

上面介紹了 startTransition ,又講到了過渡任務,本質上過渡任務有一個過渡期,在這個期間當前任務本質上是被中斷的,那麼在過渡期間,應該如何處理呢,或者說告訴用戶什麼時候過渡任務處於 pending 狀態,什麼時候 pending 狀態完畢。

爲了解決這個問題,React 提供了一個帶有 isPending 狀態的 hooks —— useTransition 。useTransition 執行返回一個數組。數組有兩個狀態值:

import { useTransition } from 'react' 

/* 使用 */
const  [ isPending , startTransition ] = useTransition ()

那麼當任務處於懸停狀態的時候,isPendingtrue,可以作爲用戶等待的 UI 呈現。比如:

{ isPending  &&  < Spinner  / > }

useTranstion 實踐

接下來我們做一個 useTranstion 的實踐,還是複用上述 demo 。對上述 demo 改造。

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const [ query ,setSearchQuery  ] = React.useState('')
    const [ isPending , startTransition ] = React.useTransition()
    const handleChange = (e) ={
        setInputValue(e.target.value)
        startTransition(()=>{
            setSearchQuery(e.target.value)
        })
    }
    return  <div>
    {isPending && <span>isTransiton</span>}
    <input onChange={handleChange}
        placeholder="輸入搜索內容"
        value={value}
    />
   <NewList  query={query} />
</div>
}

接下來看一下效果:

可以看到能夠準確捕獲到過渡期間的狀態。

4 什麼是 useDeferredValue

如上場景我們發現,本質上 query 也是 value ,不過 query 的更新要滯後於 value 的更新。那麼 React 18 提供了 useDeferredValue 可以讓狀態滯後派生。useDeferredValue 的實現效果也類似於 transtion,當迫切的任務執行後,再得到新的狀態,而這個新的狀態就稱之爲 DeferredValue 。

useDeferredValue 和上述 useTransition 本質上有什麼異同呢?

相同點:

不同點:

那麼回到 demo 上來,似乎 query 變成 DeferredValue 更適合現實情況,那麼對 demo 進行修改。

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const query = React.useDeferredValue(value)
    const handleChange = (e) ={
        setInputValue(e.target.value)
    }
    return  <div>
     <button>useDeferredValue</button>
    <input onChange={handleChange}
        placeholder="輸入搜索內容"
        value={value}
    />
   <NewList  query={query} />
   </div>
}

效果:

7.gif

四 原理

接下來又到了原理環節,從 startTransition 到 useTranstion 再到 useDeferredValue 原理本質上很簡單,

1 startTransition

首先看一下最基礎的 startTransition 是如何實現的。

react/src/ReactStartTransition.js -> startTransition

export function startTransition(scope) {
  const prevTransition = ReactCurrentBatchConfig.transition;
  /* 通過設置狀態 */
  ReactCurrentBatchConfig.transition = 1;
  try {  
      /* 執行更新 */
    scope();
  } finally {
    /* 恢復狀態 */  
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

其原理圖如下所示。

9.jpg

2 useTranstion

接下來看一下 useTranstion 的內部實現。

react-reconciler/src/ReactFiberHooks.new.js -> useTranstion

function mountTransition(){
    const [isPending, setPending] = mountState(false);
    const start = (callback)=>{
        setPending(true);
        const prevTransition = ReactCurrentBatchConfig.transition;
        ReactCurrentBatchConfig.transition = 1;
        try {
            setPending(false);
            callback();
        } finally {
            ReactCurrentBatchConfig.transition = prevTransition;
        }
    }
     return [isPending, start];
}

這段代碼不是源碼,我把源碼裏面的內容進行組合,壓縮。

其原理圖如下所示。

10.jpg

3 useDeferredValue

最後,讓我們看一下 useDeferredValue 的內部實現原理。

react-reconciler/src/ReactFiberHooks.new.js -> useTranstion

function updateDeferredValue(value){
  const [prevValue, setValue] = updateState(value);
  updateEffect(() ={
    const prevTransition = ReactCurrentBatchConfig.transition;
    ReactCurrentBatchConfig.transition = 1;
    try {
      setValue(value);
    } finally {
      ReactCurrentBatchConfig.transition = prevTransition;
    }
  }[value]);
  return prevValue;
}

useDeferredValue 處理流程是這樣的。

其原理圖如下所示。

11.jpg

四 總結

本章節講到的知識點如下:

感興趣的同學可以是一下

參考文檔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iZ1unVR4OI_AYNsQOn6UfQ