其實React Hooks已經推出來一段時間了,直到前一陣子纔去嘗試了下,看到的一些博客都是以API的使用居多,還有一些是對於原理的解析。而我這篇文章想寫的是關於React Hooks使用中的做用域問題,但願能夠幫助到曾經有過困惑的你。javascript
在講做用域以前,首先幫助你熟悉或者複習一下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的執行流程。異步
咱們利用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。
記住了嗎?
因爲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的做用域。
仍然是這段代碼:
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代碼);
}
複製代碼