本文來自《一文完全搞懂react hooks的原理和實現》,若是以爲不錯,歡迎給Github倉庫一個star。javascript
當使用 Hook 特性編寫組件的時候時候,總能感受到它的簡潔和方便。固然,「天下沒有免費的午飯」,它犧牲了可讀性而且存在內存泄漏風險(最後有提到)。但這並不妨礙探索它的魔力。html
在正式開始前,但願您讀過 Hook 的文檔或者在項目使用過它。但若是隻對函數編程感興趣,也可能有所收穫。java
爲了讓行文更流暢,我打算先拋出幾個問題,這些問題會在源碼實現的過程當中,逐步解決:react
Class
vs Hooks
⚠️ 代碼均由TypeScript
來實現,文中所有 demos 均在 gist.github.com/dongyuanxin…git
當調用 useState 的時候,會返回形如 (變量, 函數)
的一個元祖。而且 state 的初始值就是外部調用 useState 的時候,傳入的參數。github
理清楚了傳參和返回值,再來看下 useState 還作了些什麼。正以下面代碼所示,當點擊按鈕的時候,執行setNum
,狀態 num 被更新,而且 UI 視圖更新。顯然,useState 返回的用於更改狀態的函數,自動調用了render
方法來觸發視圖更新。編程
function App() {
const [num, setNum] = useState < number > 0;
return (
<div> <div>num: {num}</div> <button onClick={() => setNum(num + 1)}>加 1</button> </div>
);
}
複製代碼
有了上面的探索,藉助閉包,封裝一個 setState
以下:後端
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}
let state: any;
function useState<T>(initialState: T): [T, (newState: T) => void] {
state = state || initialState;
function setState(newState: T) {
state = newState;
render();
}
return [state, setState];
}
render(); // 首次渲染
複製代碼
這是一個簡易能用的useState
雛形了。它也解決了文章開始提到的「🤔️ useState 的實現原理」這個問題。但若是在函數內聲明多個 state,在當前代碼中,只有第一個 state 是生效的(請看state = state || initialState;
))。數組
先不要考慮題目說起的問題。思路仍是回到如何讓 useState 支持多個 state。《React hooks: not magic, just arrays》中說起,React Hook 看起來很是 Magic 的實現,本質上仍是經過 Array 來實現的。閉包
前面 useState 的簡單實現裏,初始的狀態是保存在一個全局變量中的。以此類推,多個狀態,應該是保存在一個專門的全局容器中。這個容器,就是一個樸實無華的 Array 對象。具體過程以下:
請看下面這張圖,每次使用 useState,都會向 STATE 容器中添加新的狀態。
實現的代碼以下:
import React from "react";
import ReactDOM from "react-dom";
const states: any[] = [];
let cursor: number = 0;
function useState<T>(initialState: T): [T, (newState: T) => void] {
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState; // 檢查是否渲染過
function setState(newState: T) {
states[currenCursor] = newState;
render();
}
++cursor; // update: cursor
return [states[currenCursor], setState];
}
function App() {
const [num, setNum] = useState < number > 0;
const [num2, setNum2] = useState < number > 1;
return (
<div>
<div>num: {num}</div>
<div>
<button onClick={() => setNum(num + 1)}>加 1</button>
<button onClick={() => setNum(num - 1)}>減 1</button>
</div>
<hr />
<div>num2: {num2}</div>
<div>
<button onClick={() => setNum2(num2 * 2)}>擴大一倍</button>
<button onClick={() => setNum2(num2 / 2)}>縮小一倍</button>
</div>
</div>
);
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
cursor = 0; // 重置cursor
}
render(); // 首次渲染
複製代碼
此時,若是想在循環、判斷等不在函數組件頂部的地方使用 Hook,以下所示:
let tag = true;
function App() {
const [num, setNum] = useState < number > 0;
// 只有初次渲染,才執行
if (tag) {
const [unusedNum] = useState < number > 1;
tag = false;
}
const [num2, setNum2] = useState < number > 2;
return (
<div> <div>num: {num}</div> <div> <button onClick={() => setNum(num + 1)}>加 1</button> <button onClick={() => setNum(num - 1)}>減 1</button> </div> <hr /> <div>num2: {num2}</div> <div> <button onClick={() => setNum2(num2 * 2)}>擴大一倍</button> <button onClick={() => setNum2(num2 / 2)}>縮小一倍</button> </div> </div>
);
}
複製代碼
因爲在條件判斷的邏輯中,重置了tag=false
,所以此後的渲染不會再進入條件判斷語句。看起來好像沒有問題?可是,因爲 useState 是基於 Array+Cursor 來實現的,第一次渲染時候,state 和 cursor 的對應關係以下表:
變量名 | cursor |
---|---|
num | 0 |
unusedNum | 1 |
num2 | 2 |
當點擊事件觸發再次渲染,並不會進入條件判斷中的 useState。因此,cursor=2 的時候對應的變量是 num2。而其實 num2 對應的 cursor 應該是 3。就會致使setNum2
並不起做用。
到此,解決了文章開頭提出的「🤔️ 爲何不能在循環、判斷內部使用 Hook」。在使用 Hook 的時候,請在函數組件頂部使用!
在探索 useEffect 原理的時候,一直被一個問題困擾:useEffect 做用和用途是什麼?固然,用於函數的反作用這句話誰都會講。舉個例子吧:
function App() {
const [num, setNum] = useState(0);
useEffect(() => {
// 模擬異步請求後端數據
setTimeout(() => {
setNum(num + 1);
}, 1000);
}, []);
return <div>{!num ? "請求後端數據..." : `後端數據是 ${num}`}</div>;
}
複製代碼
這段代碼,雖然這樣組織可讀性更高,畢竟能夠將這個請求理解爲函數的反作用。但這並非必要的。徹底能夠不使用useEffect
,直接使用setTimeout
,而且它的回調函數中更新函數組件的 state。
在閱讀A Complete Guide to useEffect和構建你本身的 Hooks以後,我才理解 useEffect 的存在的必要性和意義。
在 useEffect 的第二個參數中,咱們能夠指定一個數組,若是下次渲染時,數組中的元素沒變,那麼就不會觸發這個反作用(能夠類比 Class 類的關於 nextprops 和 prevProps 的生命週期)。好處顯然易見,相比於直接裸寫在函數組件頂層,useEffect 能根據須要,避免多餘的 render。
下面是一個不包括銷燬反作用功能的 useEffect 的 TypeScript 實現:
// 仍是利用 Array + Cursor的思路
const allDeps: any[][] = [];
let effectCursor: number = 0;
function useEffect(callback: () => void, deps: any[]) {
if (!allDeps[effectCursor]) {
// 初次渲染:賦值 + 調用回調函數
allDeps[effectCursor] = deps;
++effectCursor;
callback();
return;
}
const currenEffectCursor = effectCursor;
const rawDeps = allDeps[currenEffectCursor];
// 檢測依賴項是否發生變化,發生變化須要從新render
const isChanged = rawDeps.some(
(dep: any, index: number) => dep !== deps[index]
);
if (isChanged) {
callback();
}
++effectCursor;
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
effectCursor = 0; // 注意將 effectCursor 重置爲0
}
複製代碼
對於 useEffect 的實現,配合下面案例的使用會更容易理解。固然,你也能夠在這個 useEffect 中發起異步請求,並在接受數據後,調用 state 的更新函數,不會發生爆棧的狀況。
function App() {
const [num, setNum] = useState < number > 0;
const [num2] = useState < number > 1;
// 屢次觸發
// 每次點擊按鈕,都會觸發 setNum 函數
// 反作用檢測到 num 變化,會自動調用回調函數
useEffect(() => {
console.log("num update: ", num);
}, [num]);
// 僅第一次觸發
// 只會在compoentDidMount時,觸發一次
// 反作用函數不會屢次執行
useEffect(() => {
console.log("num2 update: ", num2);
}, [num2]);
return (
<div> <div>num: {num}</div> <div> <button onClick={() => setNum(num + 1)}>加 1</button> <button onClick={() => setNum(num - 1)}>減 1</button> </div> </div>
);
}
複製代碼
⚠️ useEffect 第一個回調函數能夠返回一個用於銷燬反作用的函數,至關於 Class 組件的 unmount 生命週期。這裏爲了方便說明,沒有進行實現。
在這一小節中,嘗試解答了 「🤔️ useEffect 的實現原理」和 「🤔️ useEffect 的應用場景」這兩個問題。
雖然 Hooks 看起來更酷炫,更簡潔。可是在實際開發中我更傾向於使用 Class 來聲明組件。兩種方法的對好比下:
Class | Hooks |
---|---|
代碼邏輯清晰(構造函數、componentDidMount 等) | 須要配合變量名和註釋 |
不容易內存泄漏 | 容易發生內存泄漏 |
總的來講,Hooks 對代碼編寫的要求較高,在沒有有效機制保證代碼可讀性、規避風險的狀況下,Class 依然是個人首選。關於內存泄漏,下面是一個例子(目前還沒找到方法規避這種向全局傳遞狀態更新函數的作法):
import React, { useState } from "react";
import ReactDOM from "react-dom";
let func: any;
setInterval(() => {
typeof func === "function" && func(Date.now());
console.log("interval");
}, 1000);
function App() {
const [num, setNum] = useState < number > 0;
if (typeof func !== "function") {
func = setNum;
}
return <div>{num}</div>;
}
function render() {
ReactDOM.render(<App />, document.getElementById("root")); } render(); 複製代碼
useEffect
實現有問題,回調函數調用 state 的更新函數,會爆棧文章中多有看法不到當之處,歡迎討論和指正。