react18 來了,我 get 到...

大家好!

本文主要是關於即將發佈的 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

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

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

代碼實現,將設置更新樹的 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,顯示暢銷書排行榜。

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

從圖中,可以明顯感到 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,Sibling 不會執行,會等 suspense 包裹的組件都加載完才執行渲染

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

打斷兄弟組件並阻止他們提交。等待提交 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章