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

在 React 18 中,引進了一個新的 API —— startTransition 還有二個新的 hooks —— useTransition 和 useDeferredValue,本質上它們離不開一個概念 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 Mode 到 susponse 再到 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)
}

上述這種寫法,那麼 setInputValue 和 setSearchQuery 帶來的更新就是一個相同優先級的更新。而前面說道,輸入框狀態改變更新優先級要大於列表的更新的優先級。 ,這個時候我們的主角就登場了。用 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 ()

那麼當任務處於懸停狀態的時候,isPending 爲 true,可以作爲用戶等待的 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