useState 解析
useState 使用
通常我們這樣來使用 useState 方法
function App() {
const [num, setNum] = useState(0);
const add = () => {
setNum(num + 1);
};
return (
<div>
<p>數字: {num}</p>
<button onClick={add}> +1 </button>
</div>
);
}
複製程式碼
useState 的使用過程,我們先模擬一個大概的函式
function useState(initialValue) {
var value = initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
複製程式碼
這個程式碼有一個問題,在執行 useState 的時候每次都會 var _val = initialValue,初始化資料;
於是我們可以用閉包的形式來儲存狀態。
const MyReact = (function() {
// 定義一個 value 儲存在該模組的全域性中
let value
return {
useState(initialValue) {
value = value || initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
}
})()
複製程式碼
這樣在每次執行的時候,就能夠通過閉包的形式 來儲存 value。
不過這個還是不符合 react 中的 useState。因為在實際操作中會出現多次呼叫,如下。
function App() {
const [name, setName] = useState('Kevin');
const [age, setAge] = useState(0);
const handleName = () => {
setNum('Dom');
};
const handleAge = () => {
setAge(age + 1);
};
return (
<div>
<p>姓名: {name}</p>
<button onClick={handleName}> 改名字 </button>
<p>年齡: {age}</p>
<button onClick={handleAge}> 加一歲 </button>
</div>
);
}
複製程式碼
因此我們需要在改變 useState 儲存狀態的方式
useState 模擬實現
const MyReact = (function() {
// 開闢一個儲存 hooks 的空間
let hooks = [];
// 指標從 0 開始
let currentHook = 0
return {
// 虛擬碼 解釋重新渲染的時候 會初始化 currentHook
render(Component) {
const Comp = Component()
Comp.render()
currentHook = 0 // 重新渲染時候改變 hooks 指標
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook
// 這裡我們暫且預設 setState 方式第一個引數不傳 函式,直接傳狀態
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
}
})()
複製程式碼
因此當重新渲染 App 的時候,再次執行 useState 的時候傳入的引數 kevin , 0 也就不會去使用,而是直接拿之前 hooks 儲存好的值。
hooks 規則
官網 hoos 規則中明確的提出 hooks 不要再迴圈,條件或巢狀函式中使用。
為什麼不可以?
我們來看下
下面這樣一段程式碼。執行 useState 重新渲染,和初始化渲染 順序不一樣就會出現如下問題
如果瞭解了上面 useState 模擬寫法的儲存方式,那麼這個問題的原因就迎刃而解了。
useEffect 解析
useEffect 使用
初始化會 列印一次 ‘useEffect_execute’, 改變年齡重新render,會再列印, 改變名字重新 render, 不會列印。因為依賴陣列裡面就監聽了 age 的值
import React, { useState, useEffect } from 'react';
function App() {
const [name, setName] = useState('Kevin');
const [age, setAge] = useState(0);
const handleName = () => {
setName('Don');
};
const handleAge = () => {
setAge(age + 1);
};
useEffect(()=>{
console.log('useEffect_execute')
}, [age])
return (
<div>
<p>姓名: {name}</p>
<button onClick={handleName}> 改名字 </button>
<p>年齡: {age}</p>
<button onClick={handleAge}> 加一歲 </button>
</div>
);
}
export default App;
複製程式碼
useEffect 的模擬實現
const MyReact = (function() {
// 開闢一個儲存 hooks 的空間
let hooks = [];
// 指標從 0 開始
let currentHook = 0 ;
// 定義個模組全域性的 useEffect 依賴
let deps;
return {
// 虛擬碼 解釋重新渲染的時候 會初始化 currentHook
render(Component) {
const Comp = Component()
Comp.render()
currentHook = 0 // 重新渲染時候改變 hooks 指標
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook
// 這裡我們暫且預設 setState 方式第一個引數不傳 函式,直接傳狀態
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
useEffect(callback, depArray) {
const hasNoDeps = !depArray
// 如果沒有依賴,說明是第一次渲染,或者是沒有傳入依賴引數,那麼就 為 true
// 有依賴 使用 every 遍歷依賴的狀態是否變化, 變化就會 true
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
// 如果有 依賴, 並且依賴改變
if (hasNoDeps || hasChangedDeps) {
// 執行
callback()
// 更新依賴
deps = depArray
}
},
}
})()
複製程式碼
useEffect 注意事項
依賴項要真實
依賴需要想清楚。
剛開始使用 useEffect 的時候,我只有想重新觸發 useEffect 的時候才會去設定依賴
那麼也就會出現如下的問題。
希望的效果是介面中一秒增加一歲
import React, { useState, useEffect } from 'react';
function App() {
const [name, setName] = useState('Kevin');
const [age, setAge] = useState(0);
const handleName = () => {
setName('Don');
};
const handleAge = () => {
setAge(age + 1);
};
useEffect(() => {
setInterval(() => {
setAge(age + 1);
console.log(age)
}, 1000);
}, []);
return (
<div>
<p>姓名: {name}</p>
<button onClick={handleName}> 改名字 </button>
<p>年齡: {age}</p>
<button onClick={handleAge}> 加一歲 </button>
</div>
);
}
export default App;
複製程式碼
其實你會發現 這裡介面就增加了 一次 年齡。究其原因:
**在第一次渲染中,age
是0
。因此,setAge(age+ 1)
在第一次渲染中等價於setAge(0 + 1)
。然而我設定了0依賴為空陣列,那麼之後的 useEffect 不會再重新執行,它後面每一秒都會呼叫setAge(0 + 1) **
也就是當我們需要 依賴 age 的時候我們 就必須再 依賴陣列中去記錄他的依賴。這樣useEffect 才會正常的給我們去執行。
所以我們想要每秒都遞增的話有兩種方法
方法一:
真真切切的把你所依賴的狀態填寫到 陣列中
// 通過監聽 age 的變化。來重新執行 useEffect 內的函式
// 因此這裡也就需要記錄定時器,當解除安裝的時候我們去清空定時器,防止多個定時器重新觸發
useEffect(() => {
const id = setInterval(() => {
setAge(age + 1);
}, 1000);
return () => {
clearInterval(id)
};
}, [age]);
複製程式碼
方法二
useState 的引數傳入 一個方法。
注:上面我們模擬的 useState 並沒有做這個處理 後面我會講解原始碼中去解析。
useEffect(() => {
setInterval(() => {
setAge(age => age + 1);
}, 1000);
}, []);
複製程式碼
useEffect 只執行了一次,通過 useState 傳入函式的方式它不再需要知道當前的age
值。因為 React render 的時候它會幫我們處理
這正是setAge(age => age + 1)
做的事情。再重新渲染的時候他會幫我們執行這個方法,並且傳入最新的狀態。
所以我們做到了去時刻改變狀態,但是依賴中卻不用寫這個依賴,因為我們將原本的使用到的依賴移除了。(這句話表達感覺不到位)
介面無限請求問題
剛開始使用 useEffect 的我,在介面請求的時候常常會這樣去寫程式碼。
props 裡面有 頁碼,通過切換頁碼,希望監聽頁碼的變化來重新去請求資料
// 以下是虛擬碼
// 這裡用 dva 傳送請求來模擬
import React, { useState, useEffect } from 'react';
import { connect } from 'dva';
function App(props) {
const { goods, dispatch, page } = props;
useEffect(() => {
// 頁面完成去發情請求
dispatch({
type: '/goods/list',
payload: {page, pageSize:10},
});
// xxxx
}, [props]);
return (
<div>
<p>商品: {goods}</p>
<button>點選切下一頁</button>
</div>
);
}
export default connect(({ goods }) => ({
goods,
}))(App);
複製程式碼
然後得意洋洋的重新整理介面,發現 Network 中瘋狂迴圈的請求介面,導致頁面的卡死。
究其原因是因為在依賴中,我們通過介面改變了狀態 props 的更新, 導致重新渲染元件,導致會重新執行 useEffect 裡面的方法,方法執行完成之後 props 的更新, 導致重新渲染元件,依賴專案是物件,引用型別發現不相等,又去執行 useEffect 裡面的方法,又重新渲染,然後又對比,又不相等, 又執行。因此產生了無限迴圈。
Hooks 原始碼解析
該原始碼位置: react/packages/react-reconciler/src/ReactFiberHooks.js
const Dispatcher={
useReducer: mountReducer,
useState: mountState,
// xxx 省略其他的方法
}
複製程式碼
mountState 原始碼
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
/*
mountWorkInProgressHook 方法 返回初始化物件
{
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
}
*/
const hook = mountWorkInProgressHook();
// 如果傳入的是函式 直接執行,所以第一次這個引數是 undefined
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
/*
定義 dispatch 相當於
const dispatch = queue.dispatch =
dispatchAction.bind(null,currentlyRenderingFiber,queue);
*/
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
// 可以看到這個dispatch就是dispatchAction繫結了對應的 currentlyRenderingFiber 和 queue。最後return:
return [hook.memoizedState, dispatch];
}
複製程式碼
dispatchAction 原始碼
function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
//... 省略驗證的程式碼
const alternate = fiber.alternate;
/*
這其實就是判斷這個更新是否是在渲染過程中產生的,currentlyRenderingFiber只有在FunctionalComponent更新的過程中才會被設定,在離開更新的時候設定為null,所以只要存在並更產生更新的Fiber相等,說明這個更新是在當前渲染中產生的,則這是一次reRender。
所有更新過程中產生的更新記錄在renderPhaseUpdates這個Map上,以每個Hook的queue為key。
對於不是更新過程中產生的更新,則直接在queue上執行操作就行了,注意在最後會發起一次scheduleWork的排程。
*/
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
didScheduleRenderPhaseUpdate = true;
const update: Update<A> = {
expirationTime: renderExpirationTime,
action,
next: null,
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update: Update<A> = {
expirationTime,
action,
next: null,
};
flushPassiveEffects();
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
scheduleWork(fiber, expirationTime);
}
}
複製程式碼
mountReducer 原始碼
多勒第三個引數,是函式執行,預設初始狀態 undefined
其他的和 上面的 mountState 大同小異
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
// 其他和 useState 一樣
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
複製程式碼
通過 react 原始碼中,可以看出 useState 是特殊的 useReducer
- 可見
useState
不過就是個語法糖,本質其實就是useReducer
- updateState 複用了 updateReducer(區別只是 updateState 將 reducer 設定為 updateReducer)
- mountState 雖沒直接呼叫 mountReducer,但是幾乎大同小異(區別只是 mountState 將 reducer 設定為basicStateReducer)
注:這裡僅是 react 原始碼,至於重新渲染這塊 react-dom 還沒有去深入瞭解。
更新:
分兩種情況,是否是 reRender,所謂reRender
就是說在當前更新週期中又產生了新的更新,就繼續執行這些更新知道當前渲染週期中沒有更新為止
他們基本的操作是一致的,就是根據 reducer
和 update.action
來建立新的 state
,並賦值給Hook.memoizedState
以及 Hook.baseState
。
注意這裡,對於非reRender
得情況,我們會對每個更新判斷其優先順序,如果不是當前整體更新優先順序內得更新會跳過,第一個跳過得Update
會變成新的baseUpdate
,他記錄了在之後所有得Update,即便是優先順序比他高得,因為在他被執行得時候,需要保證後續的更新要在他更新之後的基礎上再次執行,因為結果可能會不一樣。
preact 中的 hooks
Preact 最優質的開源 React 替代品!(輕量級 3kb)
注意:這裡的替代是指如果不用 react 的話,可以使用這個。而不是取代。
useState 原始碼解析
呼叫了 useReducer 原始碼
export function useState(initialState) {
return useReducer(invokeOrReturn, initialState);
}
複製程式碼
useReducer 原始碼解析
// 模組全域性定義
/** @type {number} */
let currentIndex; // 狀態的索引,也就是前面模擬實現 useState 時候所說的指標
let currentComponent; // 當前的元件
export function useReducer(reducer, initialState, init) {
/** @type {import('./internal').ReducerHookState} */
// 通過 getHookState 方法來獲取 hooks
const hookState = getHookState(currentIndex++);
// 如果沒有元件 也就是初始渲染
if (!hookState._component) {
hookState._component = currentComponent;
hookState._value = [
// 沒有 init 執行 invokeOrReturn
// invokeOrReturn 方法判斷 initialState 是否是函式
// 是函式 initialState(null) 因為初始化沒有值預設為null
// 不是函式 直接返回 initialState
!init ? invokeOrReturn(null, initialState) : init(initialState),
action => {
// reducer == invokeOrReturn
const nextValue = reducer(hookState._value[0], action);
// 如果當前的值,不等於 下一個值
// 也就是更新的狀態的值,不等於之前的狀態的值
if (hookState._value[0]!==nextValue) {
// 儲存最新的狀態
hookState._value[0] = nextValue;
// 渲染元件
hookState._component.setState({});
}
}
];
}
// hookState._value 資料格式也就是 [satea:any, action:Function] 的資料格式拉
return hookState._value;
}
複製程式碼
getHookState 方法
function getHookState(index) {
if (options._hook) options._hook(currentComponent);
const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], _pendingLayoutEffects: [] });
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
複製程式碼
invokeOrReturn 方法
function invokeOrReturn(arg, f) {
return typeof f === 'function' ? f(arg) : f;
}
複製程式碼
總結
使用 hooks 幾個月了。基本上所有類元件我都使用函式式元件來寫。現在 react 社群的很多元件,都也開始支援hooks。大概瞭解了點重要的原始碼,做到知其然也知其所以然,那麼在實際工作中使用他可以減少不必要的 bug,提高效率。
最後
全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!
參考: