全面瞭解 React 新功能: Suspense 和 Hooks

皮小蛋發表於2018-12-23

悄悄的, React v16.7 釋出了。 React v16.7: No, This Is Not The One With Hooks.

clipboard.png

最近我也一直在關注這兩個功能,就花些時間就整理了一下資料, 在此分享給大家, 希望對大家有所幫助。


引子

為什麼不推薦在 componentwillmount 裡最獲取資料的操作呢?

這個問題被過問很多遍了, 前幾天又討論到這個問題, 就以這個作為切入點吧。

有些朋友可能會想, 資料早點獲取回來,頁面就能快點渲染出來呀, 提升使用者體驗, 何樂而為不為?

這個問題, 簡單回答起來就是, 因為是可能會呼叫多次

要深入回答這個問題, 就不得不提到一個React 的核心概念: React Fiber.

一些必須要先了解的背景

React Fiber

React Fiber 是在 v16 的時候引入的一個全新架構, 旨在解決非同步渲染問題。

新的架構使得使得 React 用非同步渲染成為可能,但要注意,這個改變只是讓非同步渲染成為可能

但是React 卻並沒有在 v16 釋出的時候立刻開啟,也就是說,React 在 v16 釋出之後依然使用的是同步渲染

不過,雖然非同步渲染沒有立刻採用,Fiber 架構還是開啟了通向新世界的大門,React v16 一系列新功能幾乎都是基於 Fiber 架構。

說到這, 也要說一下 同步渲染非同步渲染.

同步渲染 和 非同步渲染

同步渲染

我們都知道React 是facebook 推出的, 他們內部也在大量使用這個框架,(個人感覺是很良心了, 內部推動, 而不是丟出去拿使用者當小白鼠), 然後就發現了很多問題, 比較突出的就是渲染問題

他們的應用是比較複雜的, 元件樹也是非常龐大, 假設有一千個元件要渲染, 每個耗費1ms, 一千個就是1000ms, 由於javascript 是單執行緒的, 這 1000ms 裡 CPU 都在努力的幹活, 一旦開始,中間就不會停。 如果這時候使用者去操作, 比如輸入, 點選按鈕, 此時頁面是沒有響應的。 等更新完了, 你之前的那些輸入就會啪啪啪一下子出來了。

這就是我們說的頁面卡頓, 用起來很不爽, 體驗不好。

這個問題和裝置效能沒有多大關係, 歸根結底還是同步渲染機制的問題。

目前的React 版本(v16.7), 當元件樹很大的時候,也會出現這個問題, 逐層渲染, 逐漸深入,不更新完就不會停

函式呼叫棧如圖所示:

clipboard.png

因為JavaScript單執行緒的特點,每個同步任務不能耗時太長,不然就會讓程式不會對其他輸入作出相應,React的更新過程就是犯了這個禁忌,而React Fiber就是要改變現狀。

非同步渲染

Fiber 的做法是:分片。

把一個很耗時的任務分成很多小片,每一個小片的執行時間很短,雖然總時間依然很長,但是在每個小片執行完之後,都給其他任務一個執行的機會,這樣唯一的執行緒就不會被獨佔,其他任務依然有執行的機會。 而維護每一個分片的資料結構, 就是Fiber

用一張圖來展示Fiber 的碎片化更新過程:

clipboard.png

中間每一個波谷代表深入某個分片的執行過程,每個波峰就是一個分片執行結束交還控制權的時機。

更詳細的資訊可以看: Lin Clark - A Cartoon Intro to Fiber - React Conf 2017

在React Fiber中,一次更新過程會分成多個分片完成,所以完全有可能一個更新任務還沒有完成,就被另一個更高優先順序的更新過程打斷,這時候,優先順序高的更新任務會優先處理完,而低優先順序更新任務所做的工作則會完全作廢,然後等待機會重頭再來

因為一個更新過程可能被打斷,所以React Fiber一個更新過程被分為兩個階段: render phase and commit phase.

兩個重要概念: render phase and commit phase

有了Fiber 之後, react 的渲染過程不再是一旦開始就不能終止的模式了, 而是劃分成為了兩個過程: 第一階段和第二階段, 也就是官網所謂的 render phase and commit phase

在 Render phase 中, React Fiber會找出需要更新哪些DOM,這個階段是可以被打斷的, 而到了第二階段commit phase, 就一鼓作氣把DOM更新完,絕不會被打斷。

兩個階段的分界點

這兩個階段, 分界點是什麼呢?

其實是 render 函式。 而且, render 函式 也是屬於 第一階段 render phase 的

那這兩個 phase 包含的的生命週期函式有哪些呢?

render phase:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

commit phase:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

clipboard.png

因為第一階段的過程會被打斷而且“重頭再來”,就會造成意想不到的情況。

比如說,一個低優先順序的任務A正在執行,已經呼叫了某個元件的componentWillUpdate函式,接下來發現自己的時間分片已經用完了,於是冒出水面,看看有沒有緊急任務,哎呀,真的有一個緊急任務B,接下來React Fiber就會去執行這個緊急任務B,任務A雖然進行了一半,但是沒辦法,只能完全放棄,等到任務B全搞定之後,任務A重頭來一遍,注意,是重頭來一遍,不是從剛才中段的部分開始,也就是說,componentWillUpdate函式會被再呼叫一次。

在現有的React中,每個生命週期函式在一個載入或者更新過程中絕對只會被呼叫一次;在React Fiber中,不再是這樣了,第一階段中的生命週期函式在一次載入和更新過程中可能會被多次呼叫!

這裡也可以回答文行開頭的那個問題了, 當然, 在非同步渲染模式沒有開啟之前, 你可以在 willMount 裡做ajax (不建議)。 首先,一個元件的 componentWillMount 比 componentDidMount 也早呼叫不了幾微秒,效能沒啥提高,而且如果開啟了非同步渲染, 這就難受了。 React 官方也意識到了這個問題,覺得有必要去勸告(威脅, 阻止)開發者不要在render phase 裡寫有副作用的程式碼了(副作用:簡單說就是做本函式之外的事情,比如改一個全域性變數, ajax之類)。

static getDerivedStateFromProps(nextProps, prevState) {
  //根據nextProps和prevState計算出預期的狀態改變,返回結果會被送給setState
}

新的靜態方法

為了減少(避免?)一些開發者的騷操作,React v16.3,乾脆引入了一個新的生命週期函式 getDerivedStateFromProps, 這個函式是一個 static 函式,也是一個純函式,裡面不能通過 this 訪問到當前元件(強制避免一些有副作用的操作),輸入只能通過引數,對元件渲染的影響只能通過返回值。目的大概也是讓開發者逐步去適應非同步渲染。

我們再看一下 React v16.3 之前的的生命週期函式 示意圖:

clipboard.png

再看看16.3的示意圖:

clipboard.png

上圖中幷包含全部React生命週期函式,另外在React v16釋出時,還增加了一個componentDidCatch,當異常發生時,一個可以捕捉到異常的componentDidCatch就排上用場了。不過,很快React覺著這還不夠,在v16.6.0又推出了一個新的捕捉異常的生命週期函式getDerivedStateFromError

如果異常發生在render階段,React就會呼叫getDerivedStateFromError,如果異常發生在第commit階段,React會呼叫componentDidCatch。 這個異常可以是任何型別的異常, 捕捉到這個異常之後呢, 可以做一些補救之類的事情。

componentDidCatchgetDerivedStateFromError 的 區別

componentDidCatch 和 getDerivedStateFromError 都是能捕捉異常的,那他們有什麼區別呢?

我們之前說了兩個階段, render phasecommit phase.

render phase 裡產生異常的時候, 會呼叫 getDerivedStateFromError;

在 commit phase 裡產生異常大的時候, 會呼叫 componentDidCatch

嚴格來說, 其實還有一點區別:

componentDidCatch 是不會在伺服器端渲染的時候被呼叫的 而 getDerivedStateFromError 會。

背景小結

囉裡八嗦一大堆, 關於背景的東西就說到這, 大家只需要瞭解什麼是Fiber: ‘ 哦, 這個這個東西是支援非同步渲染的, 雖然這個東西還沒開啟’。

然後就是渲染的兩個階段:renderphasecommit phase.

  • render phase 可以被打斷, 大家不要在此階段做一些有副作用的操作,可以放心在commit phase 裡做。
  • 然後就是生命週期的調整, react 把你有可能在render phase 裡做的有副作用的函式都改成了static 函式, 強迫開發者做一些純函式的操作。

現在我們進入正題: SuspenseHooks

正題


suspense

Suspense要解決的兩個問題:

  1. 程式碼分片;
  2. 非同步獲取資料。

剛開始的時候, React 覺得自己只是管檢視的, 程式碼打包的事不歸我管, 怎麼拿資料也不歸我管。 程式碼都打到一起, 比如十幾M, 下載就要半天,體驗顯然不會好到哪裡去。

可是後來呢,這兩個事情越來越重要, React 又覺得, 嗯,還是要摻和一下,是時候站出來展現真正的技術了。

Suspense 在v16.6的時候 已經解決了程式碼分片的問題,非同步獲取資料還沒有正式釋出。

先看一個簡單的例子:

import React from "react";
import moment from "moment";
 
const Clock = () => <h1>{moment().format("MMMM Do YYYY, h:mm:ss a")}</h1>;

export default Clock;

假設我們有一個元件, 是看當前時間的, 它用了一個很大的第三方外掛, 而我想只在用的時候再載入資源,不打在總包裡。

再看一段程式碼:

// Usage of Clock
const Clock = React.lazy(() => {
  console.log("start importing Clock");
  return import("./Clock");
});

這裡我們使用了React.lazy, 這樣就能實現程式碼的懶載入。 React.lazy 的引數是一個function, 返回的是一個promise. 這裡返回的是一個import 函式, webpack build 的時候, 看到這個東西, 就知道這是個分界點。 import 裡面的東西可以打包到另外一個包裡。

真正要用的話, 程式碼大概是這個樣子的:

<Suspense fallback={<Loading />}>
  { showClock ? <Clock/> : null}
</Suspense>

showClock 為 true, 就嘗試render clock, 這時候, 就觸發另一個事件: 去載入clock.js 和它裡面的 lib momment。

看到這你可能覺得奇怪, 怎麼還需要用個<Suspense> 包起來, 有啥用, 不包行不行。

哎嗨, 不包還真是不行。 為什麼呢?

前面我們說到, 目前react 的渲染模式還是同步的, 一口氣走到黑, 那我現在畫到clock 這裡, 但是這clock 在另外一個檔案裡, 伺服器就需要去下載, 什麼時候能下載完呢, 不知道。 假設你要花十分鐘去下載, 那這十分鐘你讓react 去幹啥, 總不能一直等你吧。 Suspens 就是來解決這個問題的, 你要畫clock, 現在沒有,那就會拋一個異常出來,我們之前說
componentDidCatch 和 getDerivedStateFromProps, 這兩個函式就是來抓子元件 或者 子子元件丟擲的異常的。

子元件有異常的時候就會往上拋,直到某個元件的 getDerivedStateFromProps 抓住這個異常,抓住之後幹嘛呢, 還能幹嘛呀, 忍著。 下載資源的時候會丟擲一個promise, 會有地方(這裡是suspense)捕捉這個promise, suspense 實現了getDerivedStateFromProps, getDerivedStateFromProps 捕獲到異常的時候, 一看, 哎, 小老弟,你來啦,還是個promise, 然後就等這個promise resole, resolve 完成之後呢,它會嘗試重新畫一下子元件。這時候資源已經到本地了, 也就能畫成功了。

用虛擬碼 大致實現一下:

getDerivedStateFromError(error) {
   if (isPromise(error)) {
      error.then(reRender);
   }
}

以上大概就是Suspense 的原理, 其實也不是很複雜,就是利用了 componentDidCatch 和 getDerivedStateFromError, 其實剛開始在v16的時候, 是要用componentDidCatch 的, 但它畢竟是commit phase 裡的東西, 還是分出來吧, 所以又加了個getDerivedStateFromError來實現 Suspense 的功能。

這裡需要注意的是 reRender 會渲染suspense 下面的所有子元件。

非同步渲染什麼時候開啟呢, 根據介紹說是在19年的第二個季度隨著一個小版本的升級開啟, 讓我們提前做好準備。

做些什麼準備呢?

  • render 函式之前的程式碼都檢查一邊, 避免一些有副作用的操作

到這, 我們說完了Suspense 的一半功能, 還有另一半: 非同步獲取資料。

目前這一部分功能還沒正式釋出。 那我們獲取資料還是隻能在commit phase 做, 也就是在componentDidMount 裡 或者 didUpdate 裡做。

就目前來說, 如果一個元件要自己獲取資料, 就必須實現為一個類元件, 而且會畫兩次, 第一次沒有資料, 是空的, 你可以畫個loading, didMount 之後發請求, 資料回來之後, 把資料setState 到元件裡, 這時候有資料了, 再畫一次,就畫出來了。

雖然是一個很簡答的功能, 我就想請求個資料, 還要寫一堆東西, 很麻煩, 但在目前的正式版裡, 不得不這麼做。

但以後這種情況會得到改善, 看一段示例:

import {unstable_createResource as createResource} from 'react-cache';

const resource = createResource(fetchDataApi);

const Foo = () => {
  const result = resource.read();
  return (
    <div>{result}</div>
  );

// ...

<Suspense>
   <Foo />
</Suskpense>};

程式碼裡我們看不到任何譬如 async await 之類的操作, 看起來完全是同步的操作, 這是什麼原理呢。

上面的例子裡, 有個 resource.read(), 這裡就會調api, 返回一個promise, 上面會有suspense 抓住, 等resolve 的時候,再畫一下, 就達到目的了。

到這,細心的同學可能就發現了一個問題, resource.read(); 明顯是一個有副作用的操作, 而且 render 函式又屬於render phase, 之前又說, 不建議在 render phase 裡做有副作用的操作, 這麼矛盾, 不是自己打臉了嗎。

這裡也能看出來React 團隊現在還沒完全想好, 目前放出來測試api 也是以unstable_開頭的, 不用用意還是跟明顯的: 讓大家不要寫class的元件,Suspense 能很好的支援函式式元件。

hooks

React v16.7.0-alpha 中第一次引入了 Hooks 的概念, 為什麼要引入這個東西呢?

有兩個原因:

  1. React 官方覺得 class元件太難以理解,OO(物件導向)太難懂了
  2. React 官方覺得 , React 生命週期太難理解。

最終目的就是, 開發者不用去理解class, 也不用操心生命週期方法。

但是React 官方又說, Hooks的目的並不是消滅類元件。此處應手動滑稽。

迴歸正題, 我們繼續看Hooks, 首先看一下官方的API

clipboard.png

乍一看還是挺多的, 其實有很多的Hook 還處在實驗階段,很可能有一部分要被砍掉, 目前大家只需要熟悉的, 三個就夠了:

  • useState
  • useEffect
  • useContext

useState

舉個例子來看下, 一個簡單的counter :

// 有狀態類元件
class Counter extends React.Component {
   state = {
      count: 0
   }
   
   increment = () => {
       this.setState({count: this.state.count + 1});
   }
   
   minus = () => {
       this.setState({count: this.state.count - 1});
   }
   
   render() {
       return (
           <div>
               <h1>{this.state.count}</h1>
               <button onClick={this.increment}>+</button>
               <button onClick={this.minus}>-</button>
           </div>
       );
   }
}
// 使用useState Hook
const Counter = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  
  return (
    <div>
        <h1>{count}</h1>
        <button onClick={increment}>+</button>
    </div>
  );
};

這裡的Counter 不是一個類了, 而是一個函式。

進去就呼叫了useState, 傳入 0,對state 進行初始化,此時count 就是0, 返回一個陣列, 第一個元素就是 state 的值,第二個元素是更新 state 的函式。

// 下面程式碼等同於: const [count, setCount] = useState(0);
  const result = useState(0);
  const count = result[0];
  const setCount = result[1];

利用 count 可以讀取到這個 state,利用 setCount 可以更新這個 state,而且我們完全可以控制這兩個變數的命名,只要高興,你完全可以這麼寫:

 const [theCount, updateCount] = useState(0);

因為 useState 在 Counter 這個函式體中,每次 Counter 被渲染的時候,這個 useState 呼叫都會被執行,useState 自己肯定不是一個純函式,因為它要區分第一次呼叫(元件被 mount 時)和後續呼叫(重複渲染時),只有第一次才用得上引數的初始值,而後續的呼叫就返回“記住”的 state 值。

讀者看到這裡,心裡可能會有這樣的疑問:如果元件中多次使用 useState 怎麼辦?React 如何“記住”哪個狀態對應哪個變數?

React 是完全根據 useState 的呼叫順序來“記住”狀態歸屬的,假設元件程式碼如下:

const Counter = () => {
  const [count, setCount] = useState(0);
  const [foo, updateFoo] = useState('foo');
  
  // ...
}

每一次 Counter 被渲染,都是第一次 useState 呼叫獲得 count 和 setCount,第二次 useState 呼叫獲得 foo 和 updateFoo(這裡我故意讓命名不用 set 字首,可見函式名可以隨意)。

React 是渲染過程中的“上帝”,每一次渲染 Counter 都要由 React 發起,所以它有機會準備好一個記憶體記錄,當開始執行的時候,每一次 useState 呼叫對應記憶體記錄上一個位置,而且是按照順序來記錄的。React 不知道你把 useState 等 Hooks API 返回的結果賦值給什麼變數,但是它也不需要知道,它只需要按照 useState 呼叫順序記錄就好了。

你可以理解為會有一個槽去記錄狀態。

正因為這個原因,Hooks,千萬不要在 if 語句或者 for 迴圈語句中使用!

像下面的程式碼,肯定會出亂子的:

const Counter = () => {
    const [count, setCount] = useState(0);
    if (count % 2 === 0) {
        const [foo, updateFoo] = useState('foo');
    }
    const [bar, updateBar] = useState('bar');
 // ...
}

因為條件判斷,讓每次渲染中 useState 的呼叫次序不一致了,於是 React 就錯亂了。

useEffect

除了 useState,React 還提供 useEffect,用於支援元件中增加副作用的支援。

在 React 元件生命週期中如果要做有副作用的操作,程式碼放在哪裡?

當然是放在 componentDidMount 或者 componentDidUpdate 裡,但是這意味著元件必須是一個 class。

在 Counter 元件,如果我們想要在使用者點選“+”或者“-”按鈕之後把計數值體現在網頁標題上,這就是一個修改 DOM 的副作用操作,所以必須把 Counter 寫成 class,而且新增下面的程式碼:

componentDidMount() {
  document.title = `Count: ${this.state.count}`;
}

componentDidUpdate() {
  document.title = `Count: ${this.state.count}`;
}

而有了 useEffect,我們就不用寫一個 class 了,對應程式碼如下:

import { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${this.state.count}`;
  });

  return (
    <div>
       <div>{count}</div>
       <button onClick={() => setCount(count + 1)}>+</button>
       <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
};

useEffect 的引數是一個函式,元件每次渲染之後,都會呼叫這個函式引數,這樣就達到了 componentDidMount 和 componentDidUpdate 一樣的效果。

雖然本質上,依然是 componentDidMountcomponentDidUpdate 兩個生命週期被呼叫,但是現在我們關心的不是 mount 或者 update 過程,而是“after render”事件,useEffect 就是告訴元件在“渲染完”之後做點什麼事。

讀者可能會問,現在把 componentDidMountcomponentDidUpdate 混在了一起,那假如某個場景下我只在 mount 時做事但 update 不做事,用 useEffect 不就不行了嗎?

其實,用一點小技巧就可以解決。useEffect 還支援第二個可選引數,只有同一 useEffect 的兩次呼叫第二個引數不同時,第一個函式引數才會被呼叫. 所以,如果想模擬 componentDidMount,只需要這樣寫:

  useEffect(() => {
    // 這裡只有mount時才被呼叫,相當於componentDidMount
  }, [123]);

在上面的程式碼中,useEffect 的第二個引數是 [123],其實也可以是任何一個常數,因為它永遠不變,所以 useEffect 只在 mount 時呼叫第一個函式引數一次,達到了 componentDidMount 一樣的效果。

useContext

在前面介紹“提供者模式”章節我們介紹過 React 新的 Context API,這個 API 不是完美的,在多個 Context 巢狀的時候尤其麻煩。

比如,一段 JSX 如果既依賴於 ThemeContext 又依賴於 LanguageContext,那麼按照 React Context API 應該這麼寫:

<ThemeContext.Consumer>
    {
        theme => (
            <LanguageContext.Cosumer>
                language => {
                    //可以使用theme和lanugage了
                }
            </LanguageContext.Cosumer>
        )
    }
</ThemeContext.Consumer>

因為 Context API 要用 render props,所以用兩個 Context 就要用兩次 render props,也就用了兩個函式巢狀,這樣的縮格看起來也的確過分了一點點。

使用 Hooks 的 useContext,上面的程式碼可以縮略為下面這樣:

const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// 這裡就可以用theme和language了

這個useContext把一個需要很費勁才能理解的 Context API 使用大大簡化,不需要理解render props,直接一個函式呼叫就搞定。

但是,useContext也並不是完美的,它會造成意想不到的重新渲染,我們看一個完整的使用useContext的元件。

const ThemedPage = () => {
    const theme = useContext(ThemeContext);
    
    return (
       <div>
            <Header color={theme.color} />
            <Content color={theme.color}/>
            <Footer color={theme.color}/>
       </div>
    );
};

因為這個元件ThemedPage使用了useContext,它很自然成為了Context的一個消費者,所以,只要Context的值發生了變化,ThemedPage就會被重新渲染,這很自然,因為不重新渲染也就沒辦法重新獲得theme值,但現在有一個大問題,對於ThemedPage來說,實際上只依賴於theme中的color屬性,如果只是theme中的size發生了變化但是color屬性沒有變化,ThemedPage依然會被重新渲染,當然,我們通過給Header、Content和Footer這些元件新增shouldComponentUpdate實現可以減少沒有必要的重新渲染,但是上一層的ThemedPage中的JSX重新渲染是躲不過去了。

說到底,useContext 需要一種表達方式告訴React:“我沒有改變,重用上次內容好了。”

希望Hooks正式釋出的時候能夠彌補這一缺陷。

Hooks 帶來的程式碼模式改變

上面我們介紹了 useStateuseEffectuseContext 三個最基本的 Hooks,可以感受到,Hooks 將大大簡化使用 React 的程式碼。

首先我們可能不再需要 class了,雖然 React 官方表示 class 型別的元件將繼續支援,但是,業界已經普遍表示會遷移到 Hooks 寫法上,也就是放棄 class,只用函式形式來編寫元件。

對於 useContext,它並沒有為消除 class 做貢獻,卻為消除 render props 模式做了貢獻。很長一段時間,高階元件和 render props 是元件之間共享邏輯的兩個武器,但如同我前面章節介紹的那樣,這兩個武器都不是十全十美的,現在 Hooks 的出現,也預示著高階元件和 render props 可能要被逐步取代。

但讀者朋友,不要覺得之前學習高階元件和 render props 是浪費時間,相反,你只有明白 React 的使用歷史,才能更好地理解 Hooks 的意義。

可以預測,在 Hooks 興起之後,共享程式碼之間邏輯會用函式形式,而且這些函式會以 use- 字首為約定,重用這些邏輯的方式,就是在函式形式元件中呼叫這些 useXXX 函式。

例如,我們可以寫這樣一個共享 Hook useMountLog,用於在 mount 時記錄一個日誌,程式碼如下:

const useMountLog = (name) => {
    useEffect(() => {
        console.log(`${name} mounted`);    
    }, [123]);
}

任何一個函式形式元件都可以直接呼叫這個 useMountLog 獲得這個功能,如下:

const Counter = () => {
    useMountLog('Counter');
    
    ...
}

對了,所有的 Hooks API 都只能在函式型別元件中呼叫,class 型別的元件不能用,從這點看,很顯然,class 型別元件將會走向消亡。

如何用Hooks 模擬舊版本的生命週期函式

Hooks 未來正式釋出後, 我們自然而然的會遇到這個問題, 如何把寫在舊生命週期內的邏輯遷移到Hooks裡面來。下面我們就簡單說一下,

模擬整個生命週期中只執行一次的方法

useMemo(() => {
  // execute only once
}, []);

我們可以看到useMemo 接收兩個引數, 第一個引數是一個函式, 第二個引數是一個陣列。

這裡有個地方要注意, 就是, 第二個引數的陣列裡的元素和上一次執行useMemo的第二個引數的陣列的元素 完全一樣的話,那就表示沒有變化, 就不用執行第一個引數裡的函式了。 如果有不同, 說明有變化, 就執行。

上面的例子裡, 我們只傳入了一個空陣列, 不會有變化, 也就是隻會執行一次。

模擬shouldComponentUpdate

const areEqual = (prevProps, nextProps) => {
   // 返回結果和shouldComponentUpdate正好相反
   // 訪問不了state
}; 
React.memo(Foo, areEqual);

模擬componentDidMount

useEffect(() => {
    // 這裡在mount時執行一次
}, []);

模擬componentDidUpdate

const mounted = useRef();
useEffect(() => {
  if (!mounted.current) {
    mounted.current = true;
  } else {
    // 這裡只在update是執行
  }
});

模擬componentDidUnmount

useEffect(() => {
    // 這裡在mount時執行一次
    return () => {
       // 這裡在unmount時執行一次
    }
}, []);

未來的程式碼形勢

Hooks 未來發布之後, 我們的程式碼會寫成什麼樣子呢? 簡單設想一下:

// Hooks之後的元件邏輯重用形態

const XXXX = () => {
  const [xx, xxx, xxxx] = useX();
  
  useY();
  
  const {a, b} = useZ();
  

  return (
    <>
     //JSX
    </>
  );
};

內部可能用各種Hooks, 也可能包含第三方的Hooks。 分享Hooks 就是實現程式碼重用的一種形勢。 其實現在已經有人在做這方面的工作了: useHooks.com, 有興趣的朋友可以去看下。

Suspense 和 Hooks 帶來的改變

Suspense 和 Hooks 釋出後, 會帶來什麼樣的改變呢? 毫無疑問, 未來的元件, 更多的將會是函式式元件。

原因很簡單, 以後大家分享出來的都是Hooks,這東西只能在函式元件裡用啊, 其他地方用不了,後面就會自然而然的發生了。

但函式式元件和函數語言程式設計還不是同一個概念。 函數語言程式設計必須是純的, 沒有副作用的, 函式式元件裡, 不能保證, 比如那個resource.read(), 明顯是有副作用的。

關於好壞

既然這兩個東西是趨勢, 那這兩個東西到底好不好呢 ?

個人理解, 任何東西都不是十全十美。 既然大勢所趨, 我們就努力去了解它,學會它, 努力用它好的地方, 避免用不好的地方。

React 釋出路線圖

最新的訊息: https://reactjs.org/blog/2018...

  • React 16.6 with Suspense for Code Splitting (already shipped)
  • A minor 16.x release with React Hooks (~Q1 2019)
  • A minor 16.x release with Concurrent Mode (~Q2 2019)
  • A minor 16.x release with Suspense for Data Fetching (~mid 2019)

明顯能夠看到資源在往 Suspense 和 Hooks 傾斜。

結語

看到這, 相信大家都Suspense 和 Hooks 都有了一個大概的瞭解了。

收集各種資料花費了挺長時間,大概用了兩三天寫出來,中間參考了很多資料, 一部分是摘錄到了上面的內容裡。

在這裡整理分享一下, 希望對大家有所幫助。

才疏學淺, 難免會有紕漏, 歡迎指正:)。

參考資料

相關文章