「前端發動機」深入 React hooks — useState

江三瘋發表於2019-09-16

前言

React Hooks的基本用法,官方文件 已經非常詳細。本文的目的,是想通過一個簡單的例子詳細分析一些令人疑惑的問題及其背後的原因。這是系列的第一篇,主要講解 useState。

個人部落格地址 ?? fe-code

思考

一起來看看這個栗子。

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

    return <h1>{count}</h1>;
}
複製程式碼

我們期望,useEffect 只執行一次,且後續每隔 1s,count 自動 + 1。然而, 實際上 count 從 0 到 1 後,再沒有變化,一直都是 1。難道是 setInterval 沒執行?於是我們很疑惑的加上了列印。

image.png

事實是,setInterval 每次執行的時候,拿到的 count 都是 0。很自然的我們會想到閉包,但是閉包能完全解釋這個現象嗎。我們稍加修改再看下這個例子。

function Counter() {
    const [count, setCount] = useState(0);
    let num = 0;
    useEffect(() => {
        const id = setInterval(() => {
            // 通過 num 來給 count 提供值
            console.log(num);
            setCount(++num);
        }, 1000);
    }, []);

    return <h1>{count}</h1>;
}
複製程式碼

image.png

我們可以看到,藉助 num 這個中間變數,我們可以得到想要的結果。但是,同樣是閉包,為什麼 num 就能記住之前的值呢?其實問題出在 count 上,繼續往下看:

function Counter() {
    // ...
    console.log('我是 num', num);

    return <h1>{count}-----{num}</h1>;
}
複製程式碼

image.png

渲染的 num 和定時器中的 num 為什麼會不一樣呢?

每次都是重新執行

到這裡我想說的到底是什麼呢?我們可以清晰看見渲染出的 num 和 setInterval 中的 num,是不同的。這是因為在 React 中,對於函式式元件來講,每次更新都會重新執行一遍函式。也就是說,每次更新都會在當前作用域重新宣告一個 let num = 0,所以,定時器中閉包引用的那個 num,和每次更新時渲染的 num,根本不是同一個。當然,我們可以很輕易的把它們變成同一個。

let num = 0; // 將宣告放到渲染元件外面
function Counter() {
    // ...
    return <h1>{count}-----{num}</h1>;
}
複製程式碼

嗯,說了這麼多,跟 count 有什麼關係呢?同理,正因為函式元件每次都會整體重新執行,那麼 Hooks 當然也是這樣。

function Counter() {
    const [count, setCount] = useState(0);
    // ...
}
複製程式碼

useState 應該理解為和普通的 javascript 函式一樣,而不是 React 的什麼黑魔法。函式元件更新的時候,useState 會重新執行,對應的,也會重新宣告 [count, setCount] 這一組常量。只不過 React 對這個函式做了一些特殊處理。比如:首次執行時,會將 useState 的引數初始化給 count,而以後再次執行時,則會直接取上次 setCount (如果有呼叫) 賦過的值(React 通過某種方式儲存起來的)。

有了這個概念,就不難知道,定時器裡的setCount(count + 1) ,這個 count 和每次更新重新宣告的 count,也是完全不同的兩個常量,只不過它們的值,可能會相等。

比如,我們嘗試把之前的 num,直接用 count 替代。

function Counter() {
    // 注意這裡變成 let
    let [count, setCount] = useState(0);
    useEffect(() => {
        const id = setInterval(() => {
            // 這種寫法是不好的
            setCount(++count);
        }, 1000);
    }, []);
    console.log(count);
    return <h1>{count}</h1>;
}
複製程式碼

這時候不論是列印還是頁面表現都和你期望的一樣,但是這違背了 React 的原則,而且也讓程式變得更讓人迷惑。也就導致你並不能清楚地知道:此時渲染的 count 和 setInterval 中的 count 已經不是同一個了。儘管他們的值是相等的。

當然,這種場景下 React 也提供了可行的方法,能夠每次拿到 count 的最新值,就是給 setCount 傳遞一個回撥函式。

function Counter() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        const id = setInterval(() => {
            // 注意:這裡變成回撥了
            setCount(count => count + 1);
        }, 1000);
    }, []);

    return <h1>{count}</h1>;
}
複製程式碼

執行圖解

回過頭再看看開始的例子:

function Counter() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        const id = setInterval(() => {
            setCount(count + 1);
        }, 1000);
    }, []);

    return <h1>{count}</h1>;
}
複製程式碼

image.png

小結

count 每次都被重新宣告瞭,setInterval 因為 useEffect 設定了只執行一次的緣故,在第一次更新時閉包引用的 count 始終是 0,後續更新的 count 和它沒關係。

交流群

微信群:掃碼回覆加群。

mmqrcode1566432627920.png

後記

如果你看到了這裡,且本文對你有一點幫助的話,希望你可以動動小手支援一下作者,感謝?。文中如有不對之處,也歡迎大家指出,共勉。好了,又耽誤大家的時間了,感謝閱讀,下次再見!

感興趣的同學可以關注下我的公眾號 前端發動機,好玩又有料。

「前端發動機」深入 React hooks — useState

相關文章