使用React Hooks你可能會忽視的做用域問題

前言

其實React Hooks已經推出來一段時間了,直到前一陣子纔去嘗試了下,看到的一些博客都是以API的使用居多,還有一些是對於原理的解析。而我這篇文章想寫的是關於React Hooks使用中的做用域問題,但願能夠幫助到曾經有過困惑的你。javascript

useEffect基礎使用

在講做用域以前,首先幫助你熟悉或者複習一下useEffect的使用,useEffect的基本使用以下:java

useEffect(() => {
    // do something
    return () => {
        // release something
    };
}, [value1, value2...])
複製代碼

useEffect接受兩個參數:一個函數和一個值數組,第二個參數是指在下次render的時候,若是這個數組中的任意一個值發生變化,那麼這個effect的函數(第一個參數)會從新執行。react

這麼講可能比較抽象,咱們如下面的一個例子來講明:數組

如圖,頁面中有1個按鈕,當點擊 "+" 按鈕時count要加1,computed始終要爲count + 1(實際業務中,這個計算每每不會是這麼簡單的),如今咱們就用useEffect來計算computed:bash

import React, { useState, useEffect } from 'react';

export default () => {
    const [count, setCount] = useState(0);
    const [computed, setComputed] = useState(0);

    useEffect(() => {
        setComputed(count + 1);
        // return () => {};
    }, [count]);

    return View代碼略;
};

複製代碼

代碼很簡單,useEffect的第二個參數爲[count],表示當count變化時,函數須要執行,在這個函數裏面咱們去設置computed爲count+1,這樣就完成了咱們的需求。app

下面咱們深刻講解下useEffect的執行流程。異步

useEffect執行流程

咱們利用console.log來幫助你們理解執行流程,上面代碼改成:函數

export default () => {
    const [count, setCount] = useState(0);
    const [computed, setComputed] = useState(0);
    
    console.log('render before useEffect', count, computed);
    
    useEffect(() => {
        console.log('in useEffect', count, computed);
        setComputed(count + 1);
        return () => {
            console.log('just log release')
        };
    }, [count]);
    
    console.log('render after useEffect', count, computed);

    return View代碼略;
};

複製代碼

首次刷新時,打印日誌爲:ui

咱們來看發生了什麼事情:google

一、第一次render執行的時候,useEffect的函數是異步執行的,是在render後執行的,準確的說,在第一個render的時候是在DOM生成後執行的,至關於類組件的componentDidMount和componentDidUpdate。

二、render後開始執行useEffect的函數,這時候咱們執行了setComputed函數,觸發state的修改,觸發從新render。

三、第二次render的時候,useEffect的函數原本應該是要異步執行的,可是這時候注意了,useEffect是有第二個參數的,第二次render的時候,count不變,因此useEffect的函數不執行。

咱們點擊下 "+" 按鈕,再看下打印日誌:

一、setCount觸發render,首先執行render

二、檢測useEffect第二個參數,發現count已經變化,因此這個effect要從新執行,執行effect以前,會去看前一次effect執行時是否返回了函數,若是返回了函數,那麼會首先執行這個函數(主要讓咱們釋放反作用)。

三、執行完release函數後,開始執行effect函數,這時候執行setComputed

四、setComputed再次觸發render,此次的render,useEffect檢測到count沒有發生變化,因此不會從新再執行effect。

若是你沒看懂這其中render、effect函數、release函數的執行順序,那麼對於後續的一些做用域問題你可能沒法理解,麻煩多看幾遍這個日誌打印的例子。

做用域問題

首先咱們看段代碼:

import React, { useState, useEffect } from 'react';

export default () => {
    const [state, setState] = useState({
        count: 0,
        computed: 1,
    });
    useEffect(() => {
        const buttonNode = document.getElementById('button');
        
        function handler() {
            console.log('in handler', state.count, state.computed);
            setState({
                count: state.count + 1,
                computed: state.count + 2,
            });
        }
        
        buttonNode.addEventListener('click', handler);
        
        return () => buttonNode.removeEventListener('click', handler);
    }, []);

    console.log('render', state.count, state.computed);

    return (
        <div className="app"> <p>count: {state.count}, computed: {state.computed}</p> <button id="button"> + </button> </div>
    );
};
複製代碼

咱們把以前的例子改造了下,把button的點擊事件改爲了在useEffect裏面綁定,useEffect的第二個參數傳入空數組[],表示這個effect函數只在componentDidMount的時候執行。咱們不斷點擊 "+" 按鈕,期待的結果應該是和上面的例子同樣,count不斷增長,computed始終爲count + 1,咱們看下打印日誌:

你猜對結果了嗎?咱們期待的count並無不斷增長,而handler裏獲取到的state.count竟然始終爲0。

按照咱們的習慣,handler裏面用到了state,在handler這個函數做用域裏面沒有這個變量,那麼應該去render這個函數裏面找,在第二次點擊按鈕的時候,state.count應該已是1了,可是爲何拿到的仍是0呢?

若是你看到這個結果沒有一刻的困惑,那麼你應該是個基礎異常紮實的人,很不容易。

這個問題的答案要用做用域來解釋。

靜態做用域

關於做用域的詳細解釋你們本身去google,好文章不少,這裏不展開講太多,簡單看段代碼:

function foo() {
    console.log(a); 
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;

bar();
複製代碼

這段代碼執行打印結果爲:2

爲何呢?由於JS的函數會建立一個做用域,這個做用域是在函數被定義的時候就定好的,在上面的代碼中,foo函數定義的時候,它的外層做用域是global,global裏面a變量是2,因此打印出來的結果是2,若是是動態做用域,那麼打印出來的就是3。

記住了嗎?

模擬useEffect的做用域問題

因爲React Hooks的內部原理須要去看源碼才能知道,這裏咱們用原生JS來模擬,這樣你就能夠更純粹地理解。

let init = true;

const value = {count: 0};

function render() {
    let count = value.count;
    if (init) {
        function handler() {
            console.log(count);
            value.count = count + 1;
            render();
        }
        document.addEventListener('click', handler);
        init = false;
    }
}

render();
複製代碼

這段代碼定義了一個函數render,render裏面綁定了document點擊事件,回調函數裏面執行了value.count爲count + 1,而後觸發render,模擬修改state後觸發render行爲。

這裏handler的count也是始終爲0,爲何呢?

咱們把上面說過的做用域概念引入就很好解釋了,當第一次執行render的時候,render函數建立了一個做用域,這個做用域中count = value.count,也就是0,這時init爲true,因此handler被定義,詞法做用域被建立,它的上層做用域就是剛纔執行render的建立的做用域。

根據靜態做用域的特性,handler裏面的count在它被定義的時候就決定是0了,因此它始終是0.

理解嗎?

若是理解了,那麼咱們返回來看useEffect的做用域。

useEffect做用域問題

仍然是這段代碼:

import React, { useState, useEffect } from 'react';

export default () => {
    const [state, setState] = useState({
        count: 0,
        computed: 1,
    });
    
    useEffect(() => {
        const buttonNode = document.getElementById('button');
        
        function handler() {
            setState({
                count: state.count + 1,
                computed: state.count + 2,
            });
        }
        
        buttonNode.addEventListener('click', handler);
        
        return () => buttonNode.removeEventListener('click', handler);
    }, []);

  
    return View省略;
};
複製代碼

一、在第一次render的時候,執行到useEffect函數的時候,能夠想象成React內部是相似下面的代碼:

const fnArray = [];
const consArray = [];

function useEffect(callback, conditions) {
    const index = <該useEffect對應的index>; if (<首次render>) { fnArray.push(callback); consArray.push(conditions); } else if (<根據conditions斷定須要從新執行effect>) { fnArray[index] = callback; consArray[index] = conditions; } } 複製代碼

源碼確定不是這樣的,可是能夠這麼理解,是用數組在維護hooks,因此useEffect的函數的做用域在執行useEffect的時候就定好了,當你傳入的conditions(第二個參數)斷定不須要從新執行時,effect函數的做用域的外層爲前面某個render建立的做用域,此次render中,conditions發生了變化,斷定須要從新執行effect,

普通的useEffect,也就是第二個參數不傳,每次都update的effect,這樣的effect在每次render執行後,都會更新最新的effect函數,所以能夠拿到最新的state

useEffect(() => {
    // do something
})
複製代碼

一個技巧

利用effect執行時機來記錄前一個render的值

export function usePrevious(value) {
    const ref = useRef();
    useEffect(() => {
        ref.current = value;
    });
    return ref.current;
}

複製代碼

而後你在你的組件中就能夠這麼用:

const Component = () => {
    const [count, setCount] = useState(0);
    const prevCount = usePrevious(count); // 獲取上一次render的count

    return (View代碼);
}
複製代碼
相關文章
相關標籤/搜索