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