useState
執行過程解析
function App() {
const [n, setN] = useState(0); //使用 myUseState()
return (
<div>
<p>{n}</p>
<p>
<button
onClick={() => {
setN(n + 1);
}}
>
+1
</button>
</p>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App></App>, rootElement);
第一次渲染
-
呼叫
render <App />
,<App / >元件呼叫App()
函式 -
App()
函式呼叫const [n, setN] = useState(0)
給 n 賦值,並且得到 一個setN
函式 -
App()
函式會根據n
的值 和setN
返回一個虛擬的DOM
節點 -
React 會根據
虛擬DOM
建立一個真實DOM
節點,並且將真實DOM
渲染到瀏覽器上,所以我們可以通過除錯工具看到一個渲染出來的DIV
節點
點選一次button按鈕
-
呼叫
onClick()
函式,onClick()
函式又呼叫setN()
函式 -
setN
在更新了 n 的值(先不管是怎麼更新的)之後會重新呼叫render <App />
-
元件重新呼叫 App()
函式 -
App
函式同樣會執行const [n, setN] = useState(0)
,根據新的n
返回一個新的虛擬DOM
-
React 根據
DOM Diff
演算法 將新的虛擬DOM
和舊的虛擬DOM
進行對比,得到最小的變動範圍物件(patch 物件) -
根據最小的變動範圍物件來
更新真實DOM
注:
- 每次呼叫
App()
都會執行const [n, setN] = useState(0)
- 同樣的一句話每次呼叫之後,每次得到的
n
的值是不一樣的
- 每次呼叫
點選button 後 n 是怎麼變的 ?
首先要思考幾個問題:
-
執行
setN()
的時候會發生什麼: n 會變嗎?App()
會重新執行嗎? -
如果
App()
會重新執行,那麼useState(0)
的時候,n
每次的值會有不同嗎?以上問題通過 console.log()就能得到答案
執行 setN
的時候
-
一定會重新渲染UI,但是這時候 n 會變嗎?n 還沒有變!!
-
setN( n + 1)
並沒有改變 n,而是改變了一箇中介資料state
(state 是什麼?繼續往下看) -
因為要重新渲染UI,所以
render <App />
會重新執行App()
重新執行App()
的時候
- 重新執行
const [n, setN] = useState(0)
的時候確實 n 會得到不同的值!
分析
-
setN
setN(n + 1)
一定會修改資料state
,將n + 1
存入state
setN()
一定會觸發重新渲染(re-render)
-
useState
useState
肯定會從state
讀取n
的最新值
-
state
- 每個元件都有自己的資料
state
,這是我們需要理解的核心
- 每個元件都有自己的資料
嘗試實現一個 useState
const myUseState = (initialValue) => {
let state = initialValue ;// 我們設定的一個 state 接收 初始值
const setState = (newValue) => {
// 我們設定的一個 setState函式
state = newValue; // 更新 state 的值
re_render(); // useState會重新渲染頁面UI
};
return [state, setState]; // 返回一個 state 和 更新state的函式: setState
};
const re_render = () => {
ReactDOM.render(<App></App>, rootElement);
};
// 上面是我們實現的一個 myUseState
function App() {
console.log("App 執行了");
const [n, setN] = myUseState(0);
console.log(n)
return (
<div>
<p>{n}</p>
<p>
<button
onClick={() => {
setN(n + 1);
}}
>
+1
</button>
</p>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App></App>, rootElement);
上面的程式碼還存在一些問題
-
每次呼叫
myUseState(0)
都會將state
重置為初始值 -
我們需要一個不會被
myUseState
重置的state
變數 -
還需要判斷
state
的取值是 初始值還是新值
改進 state
變數
let _state;
const myUseState = (initialValue) => {
_state = _state === undefined ? initialValue : _state;// 我們設定的一個 state 接收 初始值
const setState = (newValue) => {
// 我們設定的一個 setState函式
_state = newValue; // 更新 state 的值
re_render(); // useState會重新渲染頁面UI
};
return [_state, setState]; // 返回一個 state 和 更新state的函式: setState
};
const re_render = () => {
ReactDOM.render(<App></App>, rootElement);
};
// 上面是我們實現的一個 myUseState
function App() {
console.log("App 執行了");
const [n, setN] = myUseState(0);
console.log(n)
return (
<div>
<p>{n}</p>
<p>
<button
onClick={() => {
setN(n + 1);
}}
>
+1
</button>
</p>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App></App>, rootElement);
這樣我們就實現了一個 useState
!!
useState
那麼簡單嗎?
- 上面的
useState
只能實現一個元件使用一個useState
的情況 - 如果一個元件使用了
兩個 useState
,由於資料都放在 _state,所以會產生衝突
改進思路
- 把_state做成一個物件
- 比如_state = { n:0, m:0 }
- 不行, 因為
useState(0)
並不知道變數叫n
還是m
- 把_state 做成陣列
- 比如_state = [0, 0]
- 貌似可以
let _state = [];
let index = 0;
const myUseState = (initialValue) => {
const currentIndex = index; // 需要一個 currentIndex 來儲存當前的 index 的值
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
re_render();
};
index += 1; // 當前index設定完之後,後面的state要存放在下一個 index中
return [_state[currentIndex], setState];
};
const re_render = () => {
ReactDOM.render(<App></App>, rootElement);
};
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
return (
<div>
<p>n :{n}</p>
<button
onClick={() => {
setN(n + 1);
}}
>
n+1
</button>
<p>m: {m}</p>
<button
onClick={() => {
setM(m + 1);
}}
>
m+1
</button>
</div>
);
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App></App>, rootElement);
程式碼分析
- 上面的程式碼貌似可以解決兩個 state 同時儲存的問題,但是還是不能完成
useState
的操作 - 我們應該要注意到當我們只是想重新更新
state
的值的時候需要重新渲染App()
,index += 1
同樣也會執行,這就意味著這個陣列每次調動useState
之後都會變長一個單位!變長的總長度就是App()
裡面使用的useState
的次數。 - 所以我們需要在每次渲染
<App />
之前都需要重置一下index = 0
重置 index
let _state = [];
let index = 0;
const myUseState = (initialValue) => {
const currentIndex = index; // 需要一個 currentIndex 來儲存當前的 index 的值
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
re_render();
};
index += 1; // 當前index設定完之後,後面的state要存放在下一個 index中
return [_state[currentIndex], setState];
};
const re_render = () => {
index = 0; // !! 這裡很重要,重新渲染之前必須要重置 index = 0,否則陣列會變長!
ReactDOM.render(<App></App>, rootElement);
};
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
return (
<div>
<p>n :{n}</p>
<button
onClick={() => {
setN(n + 1);
}}
>
n+1
</button>
<p>m: {m}</p>
<button
onClick={() => {
setM(m + 1);
}}
>
m+1
</button>
</div>
);
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App></App>, rootElement);
終於成功實現了useState
的邏輯!
總結一下:
1. 我們要知道 `useState`的執行過程,n 是怎麼進行改變的
2. 當使用兩個 `state`的時候要保證不衝突,所以要把 state 定義成陣列用來儲存
3. 使用 `currentIndex` 記錄當前的 index
4. 在 App 渲染之前要重置 `index = 0`,否則 state 陣列會變長!
useState
很明顯的缺點
-
useState
呼叫順序不能亂- 如果第一次渲染時 n 是第一個,m是第二個,k是第三個
- 則第二次渲染時必須保證順序完全一致
- 所以React不允許出現以下程式碼
不允許出現的程式碼:
if( n % 2 === 0){ [m, setM] = React.useState(0); }
否則會報錯:
/* React Hook 'React.useState' is called conditionally. React Hooks must be called in the exact same order in every component render.
這個報錯的原理其實就是我們儲存state的陣列的順序是必須要一致的,否則如果存在判斷語句的話 state的呼叫順序不一致,就會導致 state的值混亂!這會直接導致出現bug或者錯誤!!
注:vue3 目前已經克服了這個問題,似然vue3是借鑑的 react 的思想
-
App 用了 _state 和 index,那其他元件還能用嗎?
- 我們可以給每個元件都建立 _state 和 index
-
_state 和 index 放在全域性作用域重名了咋辦?
- 每個元件都有一個虛擬DOM節點
- 我們可以把 _state和index放到虛擬DOM上