深刻理解React Router:Context、Hooks、Refs、Memo特性講解

前言

鑑於讀者對React有必定的認識,且本書全部案例均使用React Hooks編寫,以及在React Router源碼中使用了Context等React特性,所以本章僅對React的Context、Hooks等部分特性進行介紹。對於其餘React相關特性,讀者可查閱相關資料進行學習。html

Context

在React中,父組件一般將數據做爲props傳遞給子組件。若是須要跨層級傳遞數據,那麼使用props逐層傳遞數據將會使開發變得複雜。同時,在實際開發中,許多組件須要一些相同的東西,如國際化語言配置、應用的主題色等,在開發組件的過程當中也不但願逐級傳遞這些配置信息。前端

在這種狀況下,可使用React的Context特性。Context被翻譯爲上下文,如同字面意思,其包含了跨越當前層級的信息。node

Context在許多組件或者開發庫中有着普遍的應用,如react-redux使用Context做爲Provider,提供全局的store,以及React Router經過Context提供路由狀態。掌握Context將會對理解React Router起到極大的幫助做用。這裏以圖3-1來講明Context如何跨組件傳遞數據。react

在圖3-1中,左側組件樹使用了逐層傳遞props的方式來傳遞數據,即便組件B、組件C不須要關心某個數據項,也被迫須要將該數據項做爲props傳遞給子組件。而使用Context來實現組件間跨層級的數據傳遞,數據可直接從組件A傳遞到組件D中。git

圖片

在React v16.3及以上版本中,可以使用React.createContext接口建立Context容器。基於生產者-消費者模式,建立容器後可以使用容器提供方(通常稱爲Provider)提供某跨層級數據,同時使用容器消費方(通常稱爲Consumer)消費容器提供方所提供的數據。示例以下:github

// 傳入defaultValue
// 若是Consumer沒有對應的Provider,則Consumer所得到的值爲傳入的1
const CountContext = React.createContext(1);
class App extends React.Component {
    state = { count: 0 };
    render() {
        console.log('app render');
        return (
            <CountContext.Provider value={this.state.count}>
                
                <Toolbar />
                <button onClick={() => this.setState(state => ({ count: state.count + 1 }))}>
                    
                    更新
                </button>
            </CountContext.Provider>
        );
    }
}

經過setState改變count的值,觸發render渲染,Context.Provider會將最新的value值傳遞給全部的Context.Consumer。redux

class Toolbar extends React.Component {
    render() {
        console.log('Toolbar render');
        return (
            <div>
                
                <Button />
            </div>
        );
    }
}
class Button extends React.Component {
    render() {
        console.log('Button outer render');
        return (
            // 使用Consumer跨組件消費數據
            <CountContext.Consumer>
                
                {count => {
                    // 在Consumer中,受到Provider提供數據的影響
                    console.log('Button render');
                    return <div>{count}</div>;
                }}
            </CountContext.Consumer>
        );
    }
}

在上例中,頂層組件App使用 CountContext.Provider將this.state.count的值提供給後代組件。App的子組件Toolbar不消費Provider所提供的數據,Toolbar的子組件Button使用CountContext.Consumer得到App所提供的數據count。中間層的Toolbar組件對數據跨層級傳遞沒有任何感知。在單擊「更新」按鈕觸發數據傳遞時,Toolbar中的「Toolbar render」信息不會被打印。每次單擊「更新」按鈕時,僅會打印「app render」與「Button render」,這是由於在Provider所提供的值改變時,僅Consumer會渲染,因此Toolbar中的「Toolbar render」不會被打印。數組

若是在Toolbar中也使用Provider提供數據,如提供的value爲500:promise

class Toolbar extends React.Component {
    render() {
        console.log('Toolbar render');
        return (
            <CountContext.Provider value={500}>
                
                <Button />
            </CountContext.Provider>
        );
    }
}

則Button中的Consumer獲得的值將爲500。緣由在於當有多個Provider時,Consumer將消費組件樹中最近一級的Provider所提供的值。這做爲React的一個重要特性,在React Router源碼中被大量應用。瀏覽器

注意,若是不設置Context.Provider的value,或者傳入undefined,則Consumer並不會得到建立Context時的defaultValue數據。建立Context時的defaultValue數據主要提供給沒有匹配到Provider的Consumer,若是去掉App中的Provider,則Consumer所得到的值爲1。

若是但願使用this.context方式獲取Provider所提供的值,則可聲明類的靜態屬性contextType (React v16.6.0)。contextType的值爲建立的Context,如:

const MyContext = React.createContext();
class MyClass extends React.Component {
    static contextType = MyContext;
    render() {
        // 獲取Context的值
        let value = this.context;
    }
}

在React v16.3之前,不支持經過createContext的方式建立上下文,可以使用社區的polyfill方案,如create-react-context等。

注意,組件的優化方法如shouldComponentUpdate或者React.memo不能影響Context值的傳遞。若在Button中引入shouldComponentUpdate,則會阻止Button更新:

shouldComponentUpdate() {
    // 返回false 阻止了Button組件的渲染,可是Provider提供的數據依然會提供到
    //Consumer中
    // 不受此影響
    return false;
};

改變Provider所提供的值後,依然會觸發Consumer的從新渲染,結果與未引入shouldComponentUpdate時一致。

Hooks

React Hooks是React v16.8正式引入的特性,旨在解決與狀態有關的邏輯重用和共享等問題。

在React Hooks誕生前,隨着業務的迭代,在組件的生命週期函數中,充斥着各類互不相關的邏輯。一般的解決辦法是使用Render Props動態渲染所需的部分,或者使用高階組件提供公共邏輯以解耦各組件間的邏輯關聯。可是,不管是哪種方法,都會形成組件數量增多、組件樹結構修改或者組件嵌套層數過多的問題。在Hooks誕生後,它將本來分散在各個生命週期函數中處理同一業務的邏輯封裝到了一塊兒,使其更具移植性和可複用性。使用Hooks不只使得在組件之間複用狀態邏輯更加容易,也讓複雜組件更易於閱讀和理解;而且因爲沒有類組件的大量polyfill代碼,僅須要函數組件就可運行,Hooks將用更少的代碼實現一樣的效果。

React提供了大量的Hooks函數支持,如提供組件狀態支持的useState、提供反作用支持的useEffect,以及提供上下文支持的useContext等。

在使用React Hooks時,須要遵照如下準則及特性要求。

  • 只在頂層使用Hooks。不要在循環、條件或嵌套函數中調用Hooks,確保老是在React函數組件的頂層調用它們。
  • 不要在普通的JavaScript函數中調用Hooks。僅在React的函數組件中調用Hooks,以及在自定義Hook中調用其餘Hooks。

useState

useState相似於React類組件中的state和setState,可維護和修改當前組件的狀態。

useState是React自帶的一個Hook函數,使用useState可聲明內部狀態變量。useState接收的參數爲狀態初始值或狀態初始化方法,它返回一個數組。數組的第一項是當前狀態值,每次渲染其狀態值可能都會不一樣;第二項是可改變對應狀態值的set函數,在useState初始化後該函數不會變化。

useState的類型爲:

function useState<S>(initialState: S | (() => S)): [S, Dispatch <SetStateAction <S>>];

initialState僅在組件初始化時生效,後續的渲染將忽略initialState:

const [inputValue, setValue] = useState("react");const [react, setReact] = useState(inputValue);

如上例中的inputValue,當初始值傳入另外一個狀態並初始化後,另外一個狀態函數將再也不依賴inputValue的值。

使用Hooks的方式很是簡單,引入後在函數組件中使用:

import { useState } from 'react';
function Example() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>您點擊了 {count} 次</p>
            <button onClick={() => setCount(count + 1)}> 單擊觸發更新 </button>
        </div>
    );
}

相似於setState,單擊按鈕時調用setCount更新了狀態值count。當調用setCount後,組件會從新渲染,count的值會獲得更新。

當傳入初始狀態爲函數時,其僅執行一次,相似於類組件中的構造函數:

const [count, setCount] = useState(() => {
    // 可執行初始化邏輯
    return 0;
});

此外,useState返回的更新函數也可以使用函數式更新:

setCount(preCount => preCount + 1)

若是新的state須要依賴先前的 state 計算得出,那麼能夠將回調函數看成參數傳遞給setState。該回調函數將接收先前的state,並將返回的值做爲新的state進行更新。

注意,React規定Hooks需寫在函數的最外層,不能寫在if…else等條件語句中,以此來確保Hooks的執行順序一致。

useEffect

反作用

在計算機科學中,若是某些操做、函數或表達式在其局部環境以外修改了一些狀態變量值,則稱其具備反作用(side effect)。反作用能夠是一個與第三方通訊的網絡請求,或者是外部變量的修改,或者是調用具備反作用的任何其餘函數。反作用並沒有好壞之分,其存在可能影響其餘環境的使用,開發者須要作的是正確處理反作用,使得反作用操做與程序的其他部分隔離,這將使得整個軟件系統易於擴展、重構、調試、測試和維護。在大多數前端框架中,也鼓勵開發者在單獨的、鬆耦合的模塊中管理反作用和組件渲染。

對於函數來講,無反作用執行的函數稱爲純函數,它們接收參數,並返回值。純函數是肯定性的,意味着在給定輸入的狀況下,它們老是返回相同的輸出。但這並不意味着全部非純函數都具備反作用,如在函數內生成隨機值會使純函數變爲非純函數,但不具備反作用。

React是關於純函數的,它要求render純淨。若render不純淨,則會影響其餘組件,影響渲染。但在瀏覽器中,反作用無處不在,若是但願在React中處理反作用,則可以使用 useEffect。 useEffect,顧名思義,就是執行有反作用的操做,其聲明以下:

useEffect(effect: React.EffectCallback, inputs?: ReadonlyArray<any> | undefined)

函數的第一個參數爲反作用函數,第二個參數爲執行反作用的依賴數組,這將在下面的內容中介紹。 示例以下:

const App = () => {
    const [value, setValue] = React.useState(0);
    // 引入useEffect
    React.useEffect(function useEffectCallBack() {
        // 可執行反作用
        // 在此進行數據請求、訂閱事件或手動更改 DOM等操做
        const nvDom = document.getElementById('content');
        console.log('color effect', nvDom.style.color);
    });
    console.log('render');
    return (
        <div
            id="content"
            style={{ color: value === 1 ? 'red' : '' }}
            onClick={() => setValue(c => c + 1)}
        >
            {' '}
            value: {value}{' '}
        </div>
    );
};

當上述組件初始化後,在打印render後會打印一次color effect,代表組件渲染以後,執行了傳入的effect。而在單擊ID爲content的元素後,將更新value狀態,觸發一次渲染,打印render以後會打印color effect red。這一流程代表React的DOM已經更新完畢,並將控制權交給開發者的反作用函數,反作用函數成功地獲取到了DOM更新後的值。事實上,上述流程與React的componentDidMount、componentDidUpdate生命週期相似,React首次渲染和以後的每次渲染都會調用一遍傳給useEffect的函數,這也是useEffect與傳統類組件能夠類比的地方。通常來講,useEffect可類比爲componentDidMount、componentDidUpdate、componentWillUnmount三者的集合,但要注意它們不徹底等同,主要區別在於componentDidMount或componentDidUpdate中的代碼是「同步」執行的。這裏的「同步」指的是反作用的執行將阻礙瀏覽器自身的渲染,若有時候須要先根據DOM計算出某個元素的尺寸再從新渲染,這時候生命週期方法會在瀏覽器真正繪製前發生。

而useEffect中定義的反作用函數的執行不會阻礙瀏覽器更新視圖,也就是說這些函數是異步執行的。所謂異步執行,指的是傳入useEffect的回調函數是在瀏覽器的「繪製」階段以後觸發的,不「同步」阻礙瀏覽器的繪製。在一般狀況下,這是比較合理的,由於大多數的反作用都沒有必要阻礙瀏覽器的繪製。對於useEffect,React使用了一種特殊手段保證effect函數在「繪製」階段後觸發:

const channel = new MessageChannel();
channel.port1.onmessage = function () {
    // 此時繪製結束,觸發effect函數
    console.log('after repaint');
};
requestAnimationFrame(function () {
    console.log('before repaint');
    channel.port2.postMessage(undefined);
});

requestAnimationFrame與postMessage結合使用以達到這一類目的。

簡而言之,useEffect會在瀏覽器執行完reflow/repaint流程以後觸發,effect函數適合執行無DOM依賴、不阻礙主線程渲染的反作用,如數據網絡請求、外部事件綁定等。

清除反作用

當反作用對外界產生某些影響時,在再次執行反作用前,應先清除以前的反作用,再從新更新反作用,這種狀況能夠在effect中返回一個函數,即cleanup(清除)函數。

每一個effect均可以返回一個清除函數。做爲useEffect可選的清除機制,其能夠將監聽和取消監聽的邏輯放在一個effect中。

那麼,React什麼時候清除effect?effect的清除函數將會在組件從新渲染以後,並先於反作用函數執行。以一個例子來講明:

const App = () => {
    const [value, setValue] = useState(0);
    useEffect(function useEffectCallBack() {
        expensive();
        console.log('effect fire and value is', value);
        return function useEffectCleanup() {
            console.log('effect cleanup and value is ', value);
        };
    });
    return <div onClick={() => setValue(c => c + 1)}>value: {value}</div>;
};

每次單擊div元素,都會打印:

// 第一次單擊
effect cleanup and value is  0
effect fire and value is 1
// 第二次單擊
effect cleanup and value is  1
effect fire and value is 2
// 第三次單擊
effect cleanup and value is  2
effect fire and value is 3
// ……

如上例所示,React會在執行當前 effect 以前對上一個 effect 進行清除。清除函數做用域中的變量值都爲上一次渲染時的變量值,這與Hooks的Caputure Value特性有關,將在下面的內容中介紹。

除了每次更新會執行清除函數,React還會在組件卸載的時候執行清除函數。

減小沒必要要的effect

如上面內容所說,在每次組件渲染後,都會運行effect中的清除函數及對應的反作用函數。若每次從新渲染都執行一遍這些函數,則顯然不夠經濟,在某些狀況下甚至會形成反作用的死循環。這時,可利用useEffect參數列表中的第二個參數解決。useEffect參數列表中的第二個參數也稱爲依賴列表,其做用是告訴React只有當這個列表中的參數值發生改變時,才執行傳入的反作用函數:

useEffect(() => {
    document.title = `You clicked ${count} times`;
}, [count]);
// 只有當count的值發生變化時,纔會從新執行document.title這一行

那麼,React是如何判斷依賴列表中的值發生了變化的呢?事實上,React對依賴列表中的每一個值,將經過Object.is進行元素先後之間的比較,以肯定是否有任何更改。若是在當前渲染過程當中,依賴列表中的某一個元素與該元素在上一個渲染週期的不一樣,則將執行effect反作用。

注意,若是元素之一是對象或數組,那麼因爲Object.is將比較對象或數組的引用,所以可能會形成一些疑惑:

function App({ config }) {
    React.useEffect(() => {}, [config]);
    return <div>{/* UI */}</div>;
}
// 每次渲染都傳入config新對象
<App config={{ a: 1 }} />;

若是config每次都由外部傳入,那麼儘管config對象的字段值都不變,但因爲新傳入的對象與以前config對象的引用不相等,所以effect反作用將被執行。要解決此種問題,能夠依賴一些社區的解決方案,如use-deep-compare-effect。

在一般狀況下,若useEffect的第二個參數傳入一個空數組[](這並不屬於特殊狀況,它依然遵循依賴列表的工做方式),則React將認爲其依賴元素爲空,每次渲染比對,空數組與空數組都沒有任何變化。React認爲effect不依賴於props或state中的任何值,因此effect反作用永遠都不須要重複執行,可理解爲componentDidUpdate永遠不會執行。這至關於只在首次渲染的時候執行effect,以及在銷燬組件的時候執行cleanup函數。要注意,這僅是便於理解的類比,對於第二個參數傳入一個空數組[]與這類生命週期的區別,可查看下面的注意事項。

注意事項

1)Capture Value特性

注意,React Hooks有着Capture Value的特性,每一次渲染都有它本身的props和state:

function Counter() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        const id = setInterval(() => {
            console.log('count is', count);
            setCount(count + 1);
        }, 1000);
        return () => clearInterval(id);
    }, []);
    return <h1>{count}</h1>;
}

在useEffect中,得到的永遠是初始值0,將永遠打印「count is 0」;h1中的值也將永遠爲setCount(0+1)的值,即「1」。若但願count能依次增長,則可以使用useRef保存count,useRef將在3.2.4節介紹。

2)async函數

useEffect不容許傳入async函數,如:

useEffect(async () => {
    // return函數將不會被調用
}, []);

緣由在於async函數返回了promise,這與useEffect的cleanup函數容易混淆。在async函數中返回cleanup函數將不起做用,若要使用async函數,則可進行以下改寫:

useEffect(() => {
    (async () => {
        // 一些邏輯
    })(); // 可返回cleanup函數
}, []);

3)空數組依賴

注意,useEffect傳遞空數組依賴容易產生一些問題,這些問題一般容易被忽視,如如下示例:

function ChildComponent({ count }) {
    useEffect(() => {
        console.log('componentDidMount', count);
        return () => {
            // 永遠爲0,由Capture Value特性所致使
            alert('componentWillUnmount and count is ' + count);
        };
    }, []);
    console.log('count', count);
    return <>count:{count}</>;
}
const App = () => {
    const [count, setCount] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div onClick={() => setCount(c => c + 1)}>
            {' '}
            <button onClick={() => setChild(false)}>銷燬Child組件</button>{' '}
            {childShow && <ChildComponent count={count} />}{' '}
        </div>
    );
};

單擊「銷燬Child組件」按鈕,瀏覽器將彈出「componentWillUnmount and count is 0」提示框,不管setCount被調用多少次,都將如此,這是由Capture Value特性所致使的。而類組件的componentWillUnmount生命週期可從this.props.count中獲取到最新的count值。

在使用useEffect時,注意其不徹底與componentDidUpdate、componentWillUnmount等生命週期等同,應該以「反作用」或狀態同步的方式去思考useEffect。但這也不表明不建議使用空數組依賴,須要結合上下文場景決定。與其將useEffect視爲一個功能來經歷3個單獨的生命週期,不如將其簡單地視爲一種在渲染後運行反作用的方式,可能會更有幫助。

useEffect的設計意圖是關注數據流的改變,而後決定effect該如何執行,與生命週期的思考模型須要區分開。

useLayoutEffect

React還提供了與useEffect同等地位的useLayoutEffect。useEffect和useLayoutEffect在反作用中均可得到DOM變動後的屬性:

const App = () => {
    const [value, setValue] = useState(0);
    useEffect(function useEffectCallBack() {
        const nvDom = document.getElementById('content');
        console.log('color effect', nvDom.style.color);
    });
    useLayoutEffect(function useLayoutEffectCallback() {
        const nvDom = document.getElementById('content');
        console.log('color layout effect', nvDom.style.color);
    });
    return (
        <div
            id="content"
            style={{ color: value === 1 ? 'red' : '' }}
            onClick={() => setValue(c => c + 1)}
        >
            {' '}
            value: {value}{' '}
        </div>
    );
};

單擊按鈕後會打印「color layout effect red」「color effect red」。可見useEffect與useLayoutEffect均可從DOM中得到其變動後的屬性。

從表面上看,useEffect與useLayoutEffect並沒有區別,但事實上釐清它們的區別須要從反作用的「同步」「異步」入手。3.2.2節曾介紹過useEffect的運行過程是異步進行的,即useEffect不阻礙瀏覽器的渲染;useLayoutEffect與useEffect的區別是useLayoutEffect的運行過程是「同步」的,其阻礙瀏覽器的渲染。

簡而言之,useEffect發生在瀏覽器reflow/repaint操做以後,若是某些effect是從DOM中得到值的,如獲取clientHeight、clientWidth,並須要對DOM進行變動,則能夠改用useLayoutEffect,使得這些操做在reflow/repaint操做以前完成,這樣有機會避免瀏覽器花費大量成本,屢次進行reflow/repaint操做。以一個例子來講明:

const App = () => {
    const [value, setValue] = useState(0);
    useEffect(function useEffectCallBack() {
        console.log('effect');
    }); // 在下一幀渲染前執行
    window.requestAnimationFrame(() => {
        console.log('requestAnimationFrame');
    });
    useLayoutEffect(function useLayoutEffectCallback() {
        console.log('layoutEffect');
    });
    console.log('render');
    return <div onClick={() => setValue(c => c + 1)}>value: {value}</div>;
};

分別在useEffect、requestAnimationFrame、useLayoutEffect和render過程當中進行調試打印,以觀察它們的時序。能夠看到,當渲染App後將按以下順序打印:render、layoutEffect、requestAnimationFrame、effect。由此可知,useLayoutEffect的反作用都在「繪製」階段前,useEffect的反作用都在「繪製」階段後。經過瀏覽器調試工具觀察task的執行,如圖3-2所示。 在圖3-2中,①執行了useLayoutEffectCallback,爲useLayoutEffect的反作用;②爲瀏覽器的Paint流程;在Paint流程後,③的執行函數爲useEffectCallBack,執行了useEffect的反作用。

圖片

useRef

在使用class類組件時,一般須要聲明屬性,用以保存DOM節點。藉助useRef,一樣能夠在函數組件中保存DOM節點的引用:

import { useRef } from "React"
function App() {
    const inputRef = useRef(null);
    return<div>
        <input type="text" ref={inputRef} />
        {/*  經過inputRef.current獲取節點 */}
        <button onClick={() => inputRef.current.focus()}>focus</button>
    </div>
}// useRef的簽名爲:
interface MutableRefObject<T> {
    current: T;
}
function useRef<T>(initialValue: T): MutableRefObject<T>;

useRef返回一個可變的Ref對象,其 current 屬性被初始化爲傳遞的參數(initialValue)。useRef返回的可變對象就像一個「盒子」,這個「盒子」存在於組件的整個生命週期中,其current屬性保存了一個可變的值。

useRef不只適用於DOM節點的引用,相似於類上的實例屬性,useRef還可用來存放一些與UI無關的信息。useRef返回的可變對象,其current屬性能夠保存任何值,如對象、基本類型或函數等。因此,函數組件雖然沒有類的實例,沒有「this」,可是經過useRef依然能夠解決數據的存儲問題。如在2.1節,曾使用過useRef:

function Example(props) {
    const { history } = props; // 使用useRef保存註銷函數
    const historyUnBlockCb = React.useRef < UnregisterCallback > (() => {});
    React.useEffect(() => {
        return () => {
            // 在銷燬組件時調用,註銷history.block
            historyUnBlockCb.current();
        };
    }, []);
    function block() {
        // 解除以前的阻止
        historyUnBlockCb.current();
        // 從新設置彈框確認,更新註銷函數,單擊「肯定」按鈕,正常跳轉;單擊「取消」
        // 按鈕,跳轉不生效
        historyUnBlockCb.current = history.block('是否繼續?');
    }
    return (
        <>
            {' '}
            <button onClick={block}>阻止跳轉</button>{' '}
            <button
                onClick={() => {
                    historyUnBlockCb.current();
                }}
            >
                解除阻止
            </button>{' '}
        </>
    );
}

上例使用useRef返回了可變對象historyUnBlockCb,經過historyUnBlockCb.current保存了history.block的返回值。

注意,更改refObject.current的值不會致使從新渲染。若是但願從新渲染組件,則可以使用useState,或者使用某種forceUpdate方法。

useMemo

做爲React內置的Hooks,useMemo用於緩存某些函數的返回值。useMemo使用了緩存,可避免每次渲染都從新執行相關函數。useMemo接收一個函數及對應的依賴數組,當依賴數組中的一個依賴項發生變化時,將從新計算耗時函數。

function App() {
    const [count, setCount] = React.useState(0);
    const forceUpdate = useForceUpdate();
    const expensiveCalcCount = count => {
        console.log('expensive calc');
        let i = 0;
        while (i < 9999999) i++;
        return count;
    }; // 使用useMemo記錄高開銷的操做
    const letterCount = React.useMemo(() => expensiveCalcCount(count), [count]);
    console.log('component render');
    return (
        <div style={{ padding: '15px' }}>
            {' '}
            <div>{letterCount}</div> <button onClick={() => setCount(c => c + 1)}>改變count</button>{' '}
            <button onClick={forceUpdate}>更新</button>{' '}
        </div>
    );
}

在上面的示例中,除了使用了React.useState,還使用了一個自定義Hook——useForceUpdate,其返回了forceUpdate函數,與類組件中的forceUpdate函數功能一致。關於自定義Hook,將在3.2.7節介紹。

在初始渲染App時,React.useMemo中的函數會被計算一次,對應的count值與函數返回的結果都會被useMemo記錄下來。

若單擊「改變count」按鈕,因爲count改變,當App再次渲染時,React.useMemo發現count有變化,將從新調用expensiveCalcCount並計算其返回值。所以,控制檯會打印「expensive calc」「component render」。

而若單擊「更新」按鈕,則調用forceUpdate函數再次渲染。因爲在再次渲染過程當中React.useMemo發現count值沒有改變,所以將返回上一次React.useMemo中函數計算獲得的結果,渲染App控制檯僅打印「component render」。

同時,React也提供了useCallback用以緩存函數:

useCallback(fn, deps)

在實現上,useCallback等價於useMemo(() => fn, deps),所以這裏再也不贅述。

useContext

若但願在函數組件中使用3.1節中所述的Context,除使用Context.Consumer消費外,還可以使用useContext:

const contextValue = useContext(Context);

useContext接收一個Context對象(React.createContext 的返回值)並返回該Context的當前值。與3.1節中的Consumer相似,當前的Context值由上層組件中距離最近的Context.Provider提供。當更新上層組件中距離最近的Context.Provider時,使用useContext的函數組件會觸發從新渲染,並得到最新傳遞給Context.Provider的value值。

調用了useContext的組件總會在Context值變化時從新渲染,這個特性將會常用到。

在函數組件中,使用useContext獲取上下文內容,有效地解決了以前Provider、Consumer須要額外包裝組件的問題,且因爲其替代了Context.Consumer的render props寫法,這將使得組件樹更加簡潔。

自定義Hook

自定義Hook是一個函數,其名稱約定以use開頭,以即可以看出這是一個Hooks方法。若是某函數的名稱以use開頭,而且調用了其餘Hooks,就稱其爲一個自定義Hook。自定義Hook就像普通函數同樣,能夠定義任意的入參與出參,惟一要注意的是自定義Hook須要遵循Hooks的基本準則,如不能在條件循環中使用、不能在普通函數中使用。

自定義Hook解決了以前React組件中的共享邏輯問題。經過自定義Hook,可將如表單處理、動畫、聲明訂閱等邏輯抽象到函數中。自定義Hook是重用邏輯的一種方式,不受內部調用狀況的約束。事實上,每次調用Hooks都會有一個徹底隔離的狀態。所以,能夠在一個組件中使用兩次相同的自定義Hook。下面是兩個經常使用自定義Hook的示例:

// 獲取forceUpdate函數的自定義Hook
export default function useForceUpdate() {
    const [, dispatch] = useState(Object.create(null));
    const memoizedDispatch = useCallback(() => {
        // 引用變化
        dispatch(Object.create(null));
    }, [dispatch]);
    return memoizedDispatch;
}

獲取某個變量上一次渲染的值:

// 獲取上一次渲染的值
function usePrevious(value) {
    const ref = useRef();
    useEffect(() => {
        ref.current = value;
    }, [value]);
    return ref.current;
}

可基於基礎的React Hooks定義許多自定義Hook,如useLocalStorage、useLocation、useHistory (將在第5章中進行介紹)等。將邏輯抽象到自定義Hook中後,代碼將更具備可維護性。

Refs

createRef

前文曾介紹過useRef用以保存DOM節點,事實上也能夠經過createRef建立Ref對象:

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    render() {
        return <div ref={this.myRef} />;
    }
}

當this.myRef被傳遞給div元素時,可經過如下方式獲取div原生節點:

const node = this.myRef.current;

Ref不只能夠做用於DOM節點上,也能夠做用於類組件上。在類組件上使用該屬性時,Ref對象的current屬性將得到類組件的實例,於是也能夠調用該組件實例的公共方法。

forwardRef

引用傳遞(Ref forwading)是一種經過組件向子組件自動傳遞引用Ref的技術。例如,某些input組件須要控制其focus,原本是可使用Ref來控制的,可是由於該input已被包裹在組件中,因此這時就須要使用forwardRef來透過組件得到該input的引用。

import React, { Component } from 'react';
import ReactDOM, { render } from 'react-dom';
const ChildOrigin = (props, ref) => {
    return <div ref={ref}>{props.txt}</div>;
};
const Child = React.forwardRef(ChildOrigin);
class Parent extends Component {
    constructor() {
        super();
        this.myChild = React.createRef();
    }
    componentDidMount() {
        console.log(this.myChild.current);
        // 獲取的是Child組件中的div元素
    }
    render() {
        return <Child ref={this.myChild} txt="parent props txt" />;
    }
}

當對原ChildOrigin組件使用forwardRef得到了新的Child組件後,新Child組件的Ref將傳遞到ChildOrigin組件內部。在上面的示例中,可經過新Child組件的Ref值this.myChild. current獲取到ChildOrigin組件內部div元素的引用。

Memo

爲了提升React的運行性能,React v16.6.0提供了一個高階組件——React.memo。當React.memo包裝一個函數組件時,React會緩存輸出的渲染結果,以後當遇到相同的渲染條件時,會跳過這次渲染。與React的PureComponent組件相似,React.memo默認使用了淺比較的緩存策略,但React.memo對應的是函數組件,而React.PureComponent對應的是類組件。React.memo的簽名以下:

function memo<P extends object>(  
  Component: SFC<P>,  
  propsAreEqual?: (prevProps: Readonly<PropsWithChildren<P>>, 
  nextProps: Readonly<PropsWithChildren<P>>
) => boolean): NamedExoticComponent<P>;

React.memo參數列表中的第一個參數接收一個函數組件,第二個參數表示可選的props比對函數。React.memo包裝函數組件後,會返回一個新的記憶化組件。以一個示例來講明,如有一個子組件ChildComponent,沒有經過React.memo記憶化:

function ChildComponent({ count }) {
    console.log('childComponent render', count);
    return <>count:{count}</>;
}
const App = () => {
    const [count] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div>
            {' '}
            <button onClick={() => setChild(c => !c)}>隱藏/展現內容</button>{' '}
            {childShow && <div>內容</div>} <ChildComponent count={count} />{' '}
        </div>
    );
};

當重複單擊按鈕時,因爲觸發了從新渲染,ChildComponent將獲得更新,將屢次打印「childComponent render」。若引入React.memo(ChildComponent)緩存組件,則在渲染組件時,React將進行檢查。若是該組件渲染的props與先前渲染的props不一樣,則React將觸發渲染;反之,若是props先後沒有變化,則React不執行渲染,更不會執行虛擬DOM差別檢查,其將使用上一次的渲染結果。

function ChildComponent({ count }) {
    console.log('childComponent render');
    return <>count:{count}</>;
}
const MemoChildComponent = React.memo(ChildComponent);
const App = () => {
    const [count] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div>
            {' '}
            <button onClick={() => setChild(c => !c)}> 隱藏/展現內容</button>{' '}
            {childShow && <div>內容</div>} <MemoChildComponent count={count} />{' '}
        </div>
    );
};

當單擊「隱藏/展現內容」按鈕時,會致使從新渲染,但因爲原組件經過React.memo包裝過,使用了包裝後的組件MemoChildComponent,在屢次渲染時props沒有變化,所以這時不會屢次打印「childComponent render」。

同時,React.memo可使用第二個參數propsAreEqual來自定義渲染與否的邏輯:

const MemoChildComponent = React.memo(ChildComponent, function propsAreEqual(prevProps, nextProps) {
    return prevProps.count === nextProps.count;
});

propsAreEqual接收上一次的prevProps與即將渲染的nextProps,函數返回的boolean值代表先後的props是否相等。若返回「true」,則認爲先後props相等;反之,則認爲不相等,React將根據函數的返回值決定組件的渲染狀況(與shouldComponentUpdate相似)。所以,可認爲函數返回「true」,props相等,不進行渲染;函數返回「false」則認爲props有變化,React會執行渲染。 注意,不能把React.memo放在組件渲染過程當中。

const App = () => {
    // 每次都得到新的記憶化組件
    const MemoChildComponent = React.memo(ChildComponent);
    const [count] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div>
            {' '}
            <button onClick={() => setChild(c => !c)}>隱藏/展現內容</button>{' '}
            {childShow && <div>內容</div>} <MemoChildComponent count={count} />{' '}
        </div>
    );
};

這至關於每次渲染都開闢一塊新的緩存,原緩存沒法獲得利用,React.memo的記憶化將失效,開發者須要特別注意。

小結

本章介紹了Context、Hooks、Refs、Memo等React特性,在React Router源碼及相關第三方庫實現中,都涉及以上特性。掌握以上特性,對理解React Router及使用React Router進行實戰都有很是大的幫助。

相比props和state,React的Context特性能夠實現跨層級的組件通訊。咱們能夠在不少框架設計中找到使用Context的例子,React Router也是其一。學習使用Context對理解React Router十分重要。同時,本章介紹了React Hooks,做爲React v16.8的新特性,以及考慮到React Router從此的演進趨勢,學習使用React Hooks進行函數式組件開發將對讀者有極大的幫助。

參考文獻

相關文章
相關標籤/搜索