React深入淺出分析Hooks源碼
useState 解析
useState 使用
通常我們這樣來(lái)使用 useState 方法
function App() {
const [num, setNum] = useState(0);
const add = () => {
setNum(num + 1);
};
return (
<div>
<p>數(shù)字: {num}</p>
<button onClick={add}> +1 </button>
</div>
);
}
useState 的使用過(guò)程,我們先模擬一個(gè)大概的函數(shù)
function useState(initialValue) {
var value = initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
這個(gè)代碼有一個(gè)問(wèn)題,在執(zhí)行 useState 的時(shí)候每次都會(huì) var _val = initialValue,初始化數(shù)據(jù);
于是我們可以用閉包的形式來(lái)保存狀態(tài)。
const MyReact = (function() {
// 定義一個(gè) value 保存在該模塊的全局中
let value
return {
useState(initialValue) {
value = value || initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
}
})()
這樣在每次執(zhí)行的時(shí)候,就能夠通過(guò)閉包的形式 來(lái)保存 value。
不過(guò)這個(gè)還是不符合 react 中的 useState。因?yàn)樵趯?shí)際操作中會(huì)出現(xiàn)多次調(diào)用,如下。
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 儲(chǔ)存狀態(tài)的方式
useState 模擬實(shí)現(xiàn)
const MyReact = (function() {
// 開(kāi)辟一個(gè)儲(chǔ)存 hooks 的空間
let hooks = [];
// 指針從 0 開(kāi)始
let currentHook = 0
return {
// 偽代碼 解釋重新渲染的時(shí)候 會(huì)初始化 currentHook
render(Component) {
const Comp = Component()
Comp.render()
currentHook = 0 // 重新渲染時(shí)候改變 hooks 指針
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook
// 這里我們暫且默認(rèn) setState 方式第一個(gè)參數(shù)不傳 函數(shù),直接傳狀態(tài)
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
}
})()
因此當(dāng)重新渲染 App 的時(shí)候,再次執(zhí)行 useState 的時(shí)候傳入的參數(shù) kevin , 0 也就不會(huì)去使用,而是直接拿之前 hooks 存儲(chǔ)好的值。
hooks 規(guī)則
官網(wǎng) hoos 規(guī)則中明確的提出 hooks 不要再循環(huán),條件或嵌套函數(shù)中使用。

為什么不可以?
我們來(lái)看下
下面這樣一段代碼。執(zhí)行 useState 重新渲染,和初始化渲染 順序不一樣就會(huì)出現(xiàn)如下問(wèn)題
如果了解了上面 useState 模擬寫(xiě)法的存儲(chǔ)方式,那么這個(gè)問(wèn)題的原因就迎刃而解了。相關(guān)參考視頻:傳送門(mén)


useEffect 解析
useEffect 使用
初始化會(huì) 打印一次 ‘useEffect_execute’, 改變年齡重新render,會(huì)再打印, 改變名字重新 render, 不會(huì)打印。因?yàn)橐蕾嚁?shù)組里面就監(jiān)聽(tīng)了 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 的模擬實(shí)現(xiàn)
const MyReact = (function() {
// 開(kāi)辟一個(gè)儲(chǔ)存 hooks 的空間
let hooks = [];
// 指針從 0 開(kāi)始
let currentHook = 0 ;
// 定義個(gè)模塊全局的 useEffect 依賴
let deps;
return {
// 偽代碼 解釋重新渲染的時(shí)候 會(huì)初始化 currentHook
render(Component) {
const Comp = Component()
Comp.render()
currentHook = 0 // 重新渲染時(shí)候改變 hooks 指針
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook
// 這里我們暫且默認(rèn) setState 方式第一個(gè)參數(shù)不傳 函數(shù),直接傳狀態(tài)
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
useEffect(callback, depArray) {
const hasNoDeps = !depArray
// 如果沒(méi)有依賴,說(shuō)明是第一次渲染,或者是沒(méi)有傳入依賴參數(shù),那么就 為 true
// 有依賴 使用 every 遍歷依賴的狀態(tài)是否變化, 變化就會(huì) true
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
// 如果沒(méi)有依賴, 或者依賴改變
if (hasNoDeps || hasChangedDeps) {
// 執(zhí)行
callback()
// 更新依賴
deps = depArray
}
},
}
})()useEffect 注意事項(xiàng)
依賴項(xiàng)要真實(shí)
依賴需要想清楚。
剛開(kāi)始使用 useEffect 的時(shí)候,我只有想重新觸發(fā) useEffect 的時(shí)候才會(huì)去設(shè)置依賴
那么也就會(huì)出現(xiàn)如下的問(wèn)題。
希望的效果是界面中一秒增加一歲
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;其實(shí)你會(huì)發(fā)現(xiàn) 這里界面就增加了 一次 年齡。究其原因:
**在第一次渲染中,age是0。因此,setAge(age+ 1)在第一次渲染中等價(jià)于setAge(0 + 1)。然而我設(shè)置了0依賴為空數(shù)組,那么之后的 useEffect 不會(huì)再重新運(yùn)行,它后面每一秒都會(huì)調(diào)用setAge(0 + 1) **
也就是當(dāng)我們需要 依賴 age 的時(shí)候我們 就必須再 依賴數(shù)組中去記錄他的依賴。這樣useEffect 才會(huì)正常的給我們?nèi)ミ\(yùn)行。
所以我們想要每秒都遞增的話有兩種方法
方法一:
真真切切的把你所依賴的狀態(tài)填寫(xiě)到 數(shù)組中
// 通過(guò)監(jiān)聽(tīng) age 的變化。來(lái)重新執(zhí)行 useEffect 內(nèi)的函數(shù)
// 因此這里也就需要記錄定時(shí)器,當(dāng)卸載的時(shí)候我們?nèi)デ蹇斩〞r(shí)器,防止多個(gè)定時(shí)器重新觸發(fā)
useEffect(() => {
const id = setInterval(() => {
setAge(age + 1);
}, 1000);
return () => {
clearInterval(id)
};
}, [age]);方法二
useState 的參數(shù)傳入 一個(gè)方法。
注:上面我們模擬的 useState 并沒(méi)有做這個(gè)處理 后面我會(huì)講解源碼中去解析。
useEffect(() => {
setInterval(() => {
setAge(age => age + 1);
}, 1000);
}, []);
useEffect 只運(yùn)行了一次,通過(guò) useState 傳入函數(shù)的方式它不再需要知道當(dāng)前的age值。因?yàn)?React render 的時(shí)候它會(huì)幫我們處理
這正是setAge(age => age + 1)做的事情。再重新渲染的時(shí)候他會(huì)幫我們執(zhí)行這個(gè)方法,并且傳入最新的狀態(tài)。
所以我們做到了去時(shí)刻改變狀態(tài),但是依賴中卻不用寫(xiě)這個(gè)依賴,因?yàn)槲覀儗⒃镜氖褂玫降囊蕾囈瞥?。(這句話表達(dá)感覺(jué)不到位)
接口無(wú)限請(qǐng)求問(wèn)題
剛開(kāi)始使用 useEffect 的我,在接口請(qǐng)求的時(shí)候常常會(huì)這樣去寫(xiě)代碼。
props 里面有 頁(yè)碼,通過(guò)切換頁(yè)碼,希望監(jiān)聽(tīng)頁(yè)碼的變化來(lái)重新去請(qǐng)求數(shù)據(jù)
// 以下是偽代碼
// 這里用 dva 發(fā)送請(qǐng)求來(lái)模擬
import React, { useState, useEffect } from 'react';
import { connect } from 'dva';
function App(props) {
const { goods, dispatch, page } = props;
useEffect(() => {
// 頁(yè)面完成去發(fā)情請(qǐng)求
dispatch({
type: '/goods/list',
payload: {page, pageSize:10},
});
// xxxx
}, [props]);
return (
<div>
<p>商品: {goods}</p>
<button>點(diǎn)擊切下一頁(yè)</button>
</div>
);
}
export default connect(({ goods }) => ({
goods,
}))(App);然后得意洋洋的刷新界面,發(fā)現(xiàn) Network 中瘋狂循環(huán)的請(qǐng)求接口,導(dǎo)致頁(yè)面的卡死。
究其原因是因?yàn)樵谝蕾囍校覀兺ㄟ^(guò)接口改變了狀態(tài) props 的更新, 導(dǎo)致重新渲染組件,導(dǎo)致會(huì)重新執(zhí)行 useEffect 里面的方法,方法執(zhí)行完成之后 props 的更新, 導(dǎo)致重新渲染組件,依賴項(xiàng)目是對(duì)象,引用類型發(fā)現(xiàn)不相等,又去執(zhí)行 useEffect 里面的方法,又重新渲染,然后又對(duì)比,又不相等, 又執(zhí)行。因此產(chǎn)生了無(wú)限循環(huán)。
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 方法 返回初始化對(duì)象 { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, } */
const hook = mountWorkInProgressHook();
// 如果傳入的是函數(shù) 直接執(zhí)行,所以第一次這個(gè)參數(shù)是 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 相當(dāng)于 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));
// 可以看到這個(gè)dispatch就是dispatchAction綁定了對(duì)應(yīng)的 currentlyRenderingFiber 和 queue。最后return:
return [hook.memoizedState, dispatch];
}dispatchAction 源碼
function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
//... 省略驗(yàn)證的代碼
const alternate = fiber.alternate;
/* 這其實(shí)就是判斷這個(gè)更新是否是在渲染過(guò)程中產(chǎn)生的,currentlyRenderingFiber只有在FunctionalComponent更新的過(guò)程中才會(huì)被設(shè)置,在離開(kāi)更新的時(shí)候設(shè)置為null,所以只要存在并更產(chǎn)生更新的Fiber相等,說(shuō)明這個(gè)更新是在當(dāng)前渲染中產(chǎn)生的,則這是一次reRender。所有更新過(guò)程中產(chǎn)生的更新記錄在renderPhaseUpdates這個(gè)Map上,以每個(gè)Hook的queue為key。對(duì)于不是更新過(guò)程中產(chǎn)生的更新,則直接在queue上執(zhí)行操作就行了,注意在最后會(huì)發(fā)起一次scheduleWork的調(diào)度。 */
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 源碼
多勒第三個(gè)參數(shù),是函數(shù)執(zhí)行,默認(rèn)初始狀態(tài) 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];
}
通過(guò) react 源碼中,可以看出 useState 是特殊的 useReducer
- 可見(jiàn)
useState不過(guò)就是個(gè)語(yǔ)法糖,本質(zhì)其實(shí)就是useReducer - updateState 復(fù)用了 updateReducer(區(qū)別只是 updateState 將 reducer 設(shè)置為 updateReducer)
- mountState 雖沒(méi)直接調(diào)用 mountReducer,但是幾乎大同小異(區(qū)別只是 mountState 將 reducer 設(shè)置為basicStateReducer)
注:這里僅是 react 源碼,至于重新渲染這塊 react-dom 還沒(méi)有去深入了解。
更新:
分兩種情況,是否是 reRender,所謂reRender就是說(shuō)在當(dāng)前更新周期中又產(chǎn)生了新的更新,就繼續(xù)執(zhí)行這些更新知道當(dāng)前渲染周期中沒(méi)有更新為止
他們基本的操作是一致的,就是根據(jù) reducer 和 update.action 來(lái)創(chuàng)建新的 state,并賦值給Hook.memoizedState 以及 Hook.baseState。
注意這里,對(duì)于非reRender得情況,我們會(huì)對(duì)每個(gè)更新判斷其優(yōu)先級(jí),如果不是當(dāng)前整體更新優(yōu)先級(jí)內(nèi)得更新會(huì)跳過(guò),第一個(gè)跳過(guò)得Update會(huì)變成新的baseUpdate,他記錄了在之后所有得Update,即便是優(yōu)先級(jí)比他高得,因?yàn)樵谒粓?zhí)行得時(shí)候,需要保證后續(xù)的更新要在他更新之后的基礎(chǔ)上再次執(zhí)行,因?yàn)榻Y(jié)果可能會(huì)不一樣。
來(lái)源
preact 中的 hooks
Preact 最優(yōu)質(zhì)的開(kāi)源 React 替代品?。ㄝp量級(jí) 3kb)
注意:這里的替代是指如果不用 react 的話,可以使用這個(gè)。而不是取代。
useState 源碼解析
調(diào)用了 useReducer 源碼
export function useState(initialState) {
return useReducer(invokeOrReturn, initialState);
}
useReducer 源碼解析
// 模塊全局定義
/** @type {number} */
let currentIndex; // 狀態(tài)的索引,也就是前面模擬實(shí)現(xiàn) useState 時(shí)候所說(shuō)的指針
let currentComponent; // 當(dāng)前的組件
export function useReducer(reducer, initialState, init) {
/** @type {import('./internal').ReducerHookState} */
// 通過(guò) getHookState 方法來(lái)獲取 hooks
const hookState = getHookState(currentIndex++);
// 如果沒(méi)有組件 也就是初始渲染
if (!hookState._component) {
hookState._component = currentComponent;
hookState._value = [
// 沒(méi)有 init 執(zhí)行 invokeOrReturn
// invokeOrReturn 方法判斷 initialState 是否是函數(shù)
// 是函數(shù) initialState(null) 因?yàn)槌跏蓟瘺](méi)有值默認(rèn)為null
// 不是函數(shù) 直接返回 initialState
!init ? invokeOrReturn(null, initialState) : init(initialState),
action => {
// reducer == invokeOrReturn
const nextValue = reducer(hookState._value[0], action);
// 如果當(dāng)前的值,不等于 下一個(gè)值
// 也就是更新的狀態(tài)的值,不等于之前的狀態(tài)的值
if (hookState._value[0]!==nextValue) {
// 儲(chǔ)存最新的狀態(tài)
hookState._value[0] = nextValue;
// 渲染組件
hookState._component.setState({});
}
}
];
}
// hookState._value 數(shù)據(jù)格式也就是 [satea:any, action:Function] 的數(shù)據(jù)格式拉
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;
}
總結(jié)
使用 hooks 幾個(gè)月了。基本上所有類組件我都使用函數(shù)式組件來(lái)寫(xiě)?,F(xiàn)在 react 社區(qū)的很多組件,都也開(kāi)始支持hooks。大概了解了點(diǎn)重要的源碼,做到知其然也知其所以然,那么在實(shí)際工作中使用他可以減少不必要的 bug,提高效率。
到此這篇關(guān)于React深入淺出分析Hooks源碼的文章就介紹到這了,更多相關(guān)React Hooks內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React應(yīng)用中避免白屏現(xiàn)象的方法小結(jié)
在開(kāi)發(fā)React應(yīng)用程序時(shí),我們都曾遇到過(guò)這樣的場(chǎng)景:一個(gè)未被捕獲的異常突然中斷了組件的渲染流程,導(dǎo)致用戶界面呈現(xiàn)出一片空白,也就是俗稱的“白屏”現(xiàn)象,本文將探討如何在React應(yīng)用中有效捕獲并處理這些錯(cuò)誤,避免白屏現(xiàn)象的發(fā)生,需要的朋友可以參考下2024-06-06
React經(jīng)典面試題之倒計(jì)時(shí)組件詳解
這些天也都在面試,面試的內(nèi)容也大多千篇一律,無(wú)外乎vue、react這些框架的一些原理,和使用方法,但是也遇到些有趣的題目,這篇文章主要給大家介紹了關(guān)于React經(jīng)典面試題之倒計(jì)時(shí)組件的相關(guān)資料,需要的朋友可以參考下2022-03-03
D3.js(v3)+react 實(shí)現(xiàn)帶坐標(biāo)與比例尺的柱形圖 (V3版本)
這篇文章主要介紹了D3.js(v3)+react 制作 一個(gè)帶坐標(biāo)與比例尺的柱形圖 (V3版本) ,本文通過(guò)實(shí)例代碼文字相結(jié)合的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2019-05-05
Taro?React自定義TabBar使用useContext解決底部選中異常
這篇文章主要為大家介紹了Taro?React底部自定義TabBar使用React?useContext解決底部選中異常(需要點(diǎn)兩次才能選中的問(wèn)題)示例詳解,有需要的朋友可以借鑒參考下2023-08-08

