「前端發動機」深入 React hooks — 3 分鐘搞定 useState

前言

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

後記

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章