react18 來了,我 get 到...

ESnail發表於2021-12-12

大家好!

本文主要是關於即將釋出的 react 18 的新特性。那麼 react18 帶來了什麼呢?

詳情可以關注 github React 18 工作組倉庫

1. automatic batching:自動批處理。

batching 批處理,說的是,可以將回撥函式中多個 setState 事件合併為一次渲染,因此是非同步的。

解決的問題是多次同值、不同值 setState, 期望最後顯示的是最後一次 setState 的結果,減少渲染。

  const Index = () => {
    const [name, setName] = useState('')
    const [age, setAge] = useState(0)
    
    const change = () => {
      setName('a')
      setAge(1) 
      // 僅觸發一次渲染,批處理,2次setState合併為一次渲染

      // 需需要立即重渲染,需要手動呼叫
      // ReactDOM.flushSync(() => {
      //   setName('a') // 立即執行渲染
      //   setAge(1) // 立即執行渲染
      //   // 不會合並處理,即沒有批處理,觸發2次
      // });
    }

    console.log(1) // 只列印一次

    return (
      <div>
        <p>name: {name}</p>
        <p>age: {age}</p>
        <button onClick={change}>更改</button>
      </div>
    )
  }

但是 react 18 之前,在 promise、timeout 或者 event 回撥中呼叫多次 setState,由於丟失了上下文,無法做合併處理,所以每次 setState 呼叫都會立即觸發一次重渲染:

 const Index = () => {
   const [name, setName] = useState('')
   const [age, setAge] = useState(0)
   
   const change = () => {
     setTimeout(() => {
       setName('a') // 立即執行渲染
       setAge(1) // 立即執行渲染
       // 不會合並處理,即沒有批處理,觸發2次

       // 若需要批處理,需要手動呼叫
       // ReactDom.unstable_batchedUpdates(() => {
       //   setName('a')
       //   setAge(1) 
       //   // 合併處理
       // })
       // 並且將 ReactDOM.render 替換為 ReactDOM.createRoot 呼叫方式
       // 舊 ReactDOM.render(<App tab="home" />, container);
       // 新 ReactDOM.createRoot(container).render(<App tab="home" />)
     }, 0);
   }

   console.log(1) // 列印2次

   return (
     <div>
       <p>name: {name}</p>
       <p>age: {age}</p>
       <button onClick={change}>更改</button>
     </div>
   )
 }

react18,在 promise、timeout 或者 event 回撥中呼叫多次 setState,會合併為一次渲染。提升渲染效能。

v18實現「自動批處理」的關鍵在於兩點:

  • 增加排程的流程
  • 不以全域性變數 executionContext 為批處理依據,而是以更新的「優先順序」為依據

參考:

2. concurrent apis:全新的併發 api。比如:startTransition

Concurrent:併發,採用可中斷的遍歷方式更新 Fiber Reconciler。是漸進升級策略的產物。

不同更新觸發的檢視變化是有輕重緩急的,讓高優更新對應的檢視變化先渲染,那麼就能在裝置效能不變的情況下,讓使用者更快看到他們想看到的UI。

案例:使用者操作滑塊,然後響應樹的變化。滑塊響應是高優先順序的,而樹的變化可以認為是低優先順序的。

demo

未開啟:可以看到滑塊的拖動有卡頓

react18 來了,我 get 到...

開啟:可以看到滑塊的拖動,非常的絲滑順暢

react18 來了,我 get 到...

程式碼實現,將設定更新樹的 setState,放到 startTransition 中。而更新滑塊的不變,認為是高優先順序,優先響應。

2部分:

  • 緊急響應:滑塊。
  • 過渡更新:根據滑塊,呈現結果內容。
  import { useTransition } from 'react';
  const [isPending, startTransition] = useTransition();

  // 更改滑塊觸發
  function changeTreeLean(event) {
      const value = Number(event.target.value);
      setTreeLeanInput(value); // 更新滑塊

      // 是否開啟startTransition
      if (enableStartTransition) {
        startTransition(() => {
          setTreeLean(value); // 這個變慢,根據滑塊,呈現結果內容。
        });

        // react18之前,想要有類似功能。變體,setTimeout,防抖節流
        // setTimeout(() => {
        //   setTreeLean(value)
        // }, 0)

      } else {
        setTreeLean(value);
      }
  }

  // 過渡期間可以這麼處理
  {isPending ? <Spinner /> : <Con>}

setTimeout 更好,能有狀態 isPending,且更早更快的呈現更新到介面上(微任務裡處理)。而且 setTimeout 是不可中斷的,而 startTransition 是可中斷的,不會影響頁面互動響應。

依賴於React底層實現的優先順序排程模型,被 startTransition 包含的 setState 的優先順序會被設定為低優先順序的過渡更新。

參考:

3. suspense:更好的 suspense。更好的支援在 ssr 和 非同步資料 場景下使用 suspense。

1. ssr 下支援,可參考:React18 中的新 Suspense SSR 架構

2.透明的非同步資料處理(未來18.x支援)

和寫同步邏輯程式碼一樣,寫非同步程式碼邏輯。大大的簡化了程式碼邏輯的書寫。把代數效應應用到極致了,把非同步的副作用剝離了。

代數效應是函數語言程式設計中的一個概念,用於將副作用從函式呼叫中分離。

場景案例:demo,顯示暢銷書排行榜。

react18 來了,我 get 到...

其中,名稱和日期是一個介面獲取,而下面的列表是另一個介面獲取。

從圖中,可以明顯感到 with suspense 的效果更絲滑,使用者體驗更好。而程式碼也非常簡潔。部分程式碼如下:

```js
// 介面部分
import { fetch } from "react-fetch"

export function fetchBookLists() {
  const res = fetch(`
  https://api.nytimes.com/svc/books/v3/lists/names.json?api-key=${API_KEY}`)

  const json = res.json()

  if (json.status === "OK") {
    return json.results
  } else {
    console.log(json)
    throw new Error("Loading failed, likely rate limit")
  }
}

// 元件部分
// 沒有處理 loading 狀態等的非同步處理,和同步已經完全一致的程式碼書寫
const Content = () => {
  const list = fetchBookLists()[0]

  return (
    <>
      <h4>From {list.display_name}</h4>
      <Paragraph sx={{ mt: -3 }}>
        Published on {list.newest_published_date}
      </Paragraph>
      <BookList list={list} />
    </>
  )
}

export const BestSellers = () => {
  return (
    <Suspense fallback={<Spinner />}>
      {/* loading must happen inside a <Suspense> */}
      <Content />
    </Suspense>
  )
}
```

而在 react18 之前,你得這麼寫:

```js
// 介面部分
import { fetch } from "react-fetch"
export async function fetchBookLists() {
  const res = await fetch(`
  https://api.nytimes.com/svc/books/v3/lists/names.json?api-key=${API_KEY}`)

  const json = await res.json()

  if (json.status === "OK") {
    return json.results
  } else {
    console.log(json)
    throw new Error("Loading failed, likely rate limit")
  }
}

// 元件部分,按照非同步的邏輯寫,寫loading,對非同步結果的處理等
function useNYTBestSellerLists() {
  // poor man's useQuery implementation
  const [isLoading, setIsLoading] = useState(false)
  const [lists, setLists] = useState(null)

  useEffect(() => {
    setIsLoading(true)

    fetchBookLists()
      .then((lists) => {
        setLists(lists)
        setIsLoading(false)
      })
      .catch(() => setIsLoading(false))
  }, [])

  return { isLoading, lists }
}

export const BestSellers = () => {
  const { isLoading, lists } = useNYTBestSellerLists();

  if (isLoading) {
    return <Spinner />;
  }

  if (!lists) {
    return "not loading or error";
  }

  const list = lists[0];

  return (
    <>
      <h4>From {list.display_name}</h4>
      <Paragraph sx={{ mt: -3 }}>
        Published on {list.newest_published_date}
      </Paragraph>
      <BookList list={list} />
    </>
  );
}
```

參考:

3.優化 suspense 的行為表現。

場景舉例:

    <Suspense fallback={<h3>loading...</h3>}>
      <LazyCpn /> // 為 React.lazy 包裹的非同步載入元件
      <Sibling /> // 普通元件
    </Suspense>

由於 Suspense 會等待子孫元件中的非同步請求完畢後再渲染,所以當程式碼執行時頁面首先會渲染 fallback:loading。而在loading這個過程中,頁面表現是一致的,但是背後的行為是不一致的:

  • react18 之前:即在 Legacy Suspense 中,Sibling 元件會立即安裝到 DOM 並觸發其效果/生命週期。頁面上隱藏。
  • react18:即在 Concurrent Suspense 中,Sibling 元件沒有掛載到 DOM。它的效果/生命週期也不會在 ComponentThatSuspends 解決之前觸發。
react18 來了,我 get 到...

react18,Sibling 不會執行,會等 suspense 包裹的元件都載入完才執行渲染

react18 來了,我 get 到...

優化的是提交渲染的流程:

打斷兄弟元件並阻止他們提交。等待提交 Suspense 邊界內的所有內容- 掛起的元件及其所有兄弟元件 - 直到掛起的資料解決。然後在一個單一的、一致的批次中同時提交整個樹渲染。

參考:

4. 其他

比如:新 Hook —— useId

解決問題:ssr 場景下,客戶端、服務端生成的id不匹配!官方推出 Hook——useId解決,每個 id 代表該元件在元件樹中的層級結構。

function Checkbox() {
  // 生成唯一、穩定id
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input type="checkbox" name="react" id={id} />
    </>
  );
);

參考:為了生成唯一id,React18專門引入了新Hook:useId

最後

這幾個重大的更新,目的都是較少渲染、根據優先順序響應、提升效能、擁有更好的體驗。非常值得期待。

想嚐鮮的可安裝 react18 beta 版(2021-11-16釋出的)

# npm
npm install react@beta react-dom@beta
# yarn
yarn add react@beta react-dom@beta

相關文章