本篇文章主要從react-hooks
起源,原理,源碼角度,開始剖析react-hooks
運行機制和內部原理,相信這篇文章事後,對於面試的時候那些hooks
問題,也就迎刃而解了。實際react-hooks
也並無那麼難以理解,聽起來很cool
,實際就是函數組件解決沒有state
,生命週期,邏輯不能複用的一種技術方案。react
Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。
老規矩,🤔️🤔️🤔️咱們帶着疑問開始今天的探討(能回答上幾個,本身能夠嘗試一下,掌握程度):面試
react
用什麼方式記錄了hooks
的狀態?react-hooks
用什麼來記錄每個hooks
的順序的 ? 換個問法!爲何不能條件語句中,聲明hooks
? hooks
聲明爲何在組件的最頂部?function
函數組件中的useState
,和 class
類組件 setState
有什麼區別?react
是怎麼捕獲到hooks
的執行上下文,是在函數組件內部的?useEffect
,useMemo
中,爲何useRef
不須要依賴注入,就能訪問到最新的改變值?useMemo
是怎麼對值作緩存的?如何應用它優化性能?useState
的值相同,函數組件不更新?大綱.jpg算法
若是你認真讀完這篇文章,這些問題全會迎刃而解。數組
在解釋react-hooks
原理的以前,咱們要加深理解一下, 函數組件和類組件到底有什麼區別,廢話很少說,咱們先看 兩個代碼片斷。緩存
class Index extends React.Component<any,any>{ constructor(props){ super(props) this.state={ number:0 } } handerClick=()=>{ for(let i = 0 ;i<5;i++){ setTimeout(()=>{ this.setState({ number:this.state.number+1 }) console.log(this.state.number) },1000) } } render(){ return <div> <button onClick={ this.handerClick } >num++</button> </div> } }
打印結果?閉包
再來看看函數組件中:app
function Index(){ const [ num ,setNumber ] = React.useState(0) const handerClick=()=>{ for(let i=0; i<5;i++ ){ setTimeout(() => { setNumber(num+1) console.log(num) }, 1000) } } return <button onClick={ handerClick } >{ num }</button> }
打印結果?dom
------------公佈答案-------------ide
在第一個例子🌰打印結果: 1 2 3 4 5函數
在第二個例子🌰打印結果: 0 0 0 0 0
這個問題實際很蒙人,咱們來一塊兒分析一下,第一個類組件中,因爲執行上setState
沒有在react
正常的函數執行上下文上執行,而是setTimeout
中執行的,批量更新條件被破壞。原理這裏我就不講了,因此能夠直接獲取到變化後的state
。
可是在無狀態組件中,彷佛沒有生效。緣由很簡單,在class
狀態中,經過一個實例化的class
,去維護組件中的各類狀態;可是在function
組件中,沒有一個狀態去保存這些信息,每一次函數上下文執行,全部變量,常量都從新聲明,執行完畢,再被垃圾機制回收。因此如上,不管setTimeout
執行多少次,都是在當前函數上下文執行,此時num = 0
不會變,以後setNumber
執行,函數組件從新執行以後,num
才變化。
因此, 對於class
組件,咱們只須要實例化一次,實例中保存了組件的state
等狀態。對於每一次更新只須要調用render
方法就能夠。可是在function
組件中,每一次更新都是一次新的函數執行,爲了保存一些狀態,執行一些反作用鉤子,react-hooks
應運而生,去幫助記錄組件的狀態,處理一些額外的反作用。
咱們從引入 hooks
開始,以useState
爲例子,當咱們從項目中這麼寫:
import { useState } from 'react'
因而乎咱們去找useState
,看看它究竟是哪路神仙?
react/src/ReactHooks.js
useState
export function useState(initialState){ const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
useState()
的執行等於 dispatcher.useState(initialState)
這裏面引入了一個dispatcher
,咱們看一下resolveDispatcher
作了些什麼?
resolveDispatcher
function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current return dispatcher }
ReactCurrentDispatcher
react/src/ReactCurrentDispatcher.js const ReactCurrentDispatcher = { current: null, };
咱們看到ReactCurrentDispatcher.current
初始化的時候爲null
,而後就沒任何下文了。咱們暫且只能把ReactCurrentDispatcher
記下來。看看ReactCurrentDispatcher
何時用到的 ?
想要完全弄明白hooks
,就要從其根源開始,上述咱們在引入hooks
的時候,最後以一個ReactCurrentDispatcher
草草收尾,線索所有斷了,因此接下來咱們只能從函數組件執行開始。
對於function
組件是何時執行的呢?
react-reconciler/src/ReactFiberBeginWork.js
function
組件初始化:
renderWithHooks( null, // current Fiber workInProgress, // workInProgress Fiber Component, // 函數組件自己 props, // props context, // 上下文 renderExpirationTime,// 渲染 ExpirationTime );
對於初始化是沒有current
樹的,以後完成一次組件更新後,會把當前workInProgress
樹賦值給current
樹。
function
組件更新:
renderWithHooks( current, workInProgress, render, nextProps, context, renderExpirationTime, );
咱們從上邊能夠看出來,renderWithHooks
函數做用是調用function
組件函數的主要函數。咱們重點看看renderWithHooks
作了些什麼?
renderWithHooks react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRenderExpirationTime, ) { renderExpirationTime = nextRenderExpirationTime; currentlyRenderingFiber = workInProgress; workInProgress.memoizedState = null; workInProgress.updateQueue = null; workInProgress.expirationTime = NoWork; ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; let children = Component(props, secondArg); if (workInProgress.expirationTime === renderExpirationTime) { // ....這裏的邏輯咱們先放一放 } ReactCurrentDispatcher.current = ContextOnlyDispatcher; renderExpirationTime = NoWork; currentlyRenderingFiber = null; currentHook = null workInProgressHook = null; didScheduleRenderPhaseUpdate = false; return children; }
全部的函數組件執行,都是在這裏方法中,首先咱們應該明白幾個感念,這對於後續咱們理解useState
是頗有幫助的。
current fiber樹
: 當完成一次渲染以後,會產生一個current
樹,current
會在commit
階段替換成真實的Dom
樹。
workInProgress fiber樹
: 即將調和渲染的 fiber
樹。再一次新的組件更新過程當中,會從current
複製一份做爲workInProgress
,更新完畢後,將當前的workInProgress
樹賦值給current
樹。
workInProgress.memoizedState
: 在class
組件中,memoizedState
存放state
信息,在function
組件中,這裏能夠提早透漏一下,memoizedState
在一次調和渲染過程當中,以鏈表的形式存放hooks
信息。
workInProgress.expirationTime
: react
用不一樣的expirationTime
,來肯定更新的優先級。
currentHook
: 能夠理解 current
樹上的指向的當前調度的 hooks
節點。
workInProgressHook
: 能夠理解 workInProgress
樹上指向的當前調度的 hooks
節點。
renderWithHooks
函數主要做用:
首先先置空即將調和渲染的workInProgress
樹的memoizedState
和updateQueue
,爲何這麼作,由於在接下來的函數組件執行過程當中,要把新的hooks
信息掛載到這兩個屬性上,而後在組件commit
階段,將workInProgress
樹替換成current
樹,替換真實的DOM
元素節點。並在current
樹保存hooks
信息。
而後根據當前函數組件是不是第一次渲染,賦予ReactCurrentDispatcher.current
不一樣的hooks
,終於和上面講到的ReactCurrentDispatcher
聯繫到一塊兒。對於第一次渲染組件,那麼用的是HooksDispatcherOnMount
hooks對象。 對於渲染後,須要更新的函數組件,則是HooksDispatcherOnUpdate
對象,那麼兩個不一樣就是經過current
樹上是否memoizedState
(hook信息)來判斷的。若是current
不存在,證實是第一次渲染函數組件。
接下來,調用Component(props, secondArg);
執行咱們的函數組件,咱們的函數組件在這裏真正的被執行了,而後,咱們寫的hooks
被依次執行,把hooks
信息依次保存到workInProgress
樹上。 至於它是怎麼保存的,咱們立刻會講到。
接下來,也很重要,將ContextOnlyDispatcher
賦值給 ReactCurrentDispatcher.current
,因爲js
是單線程的,也就是說咱們沒有在函數組件中,調用的hooks
,都是ContextOnlyDispatcher
對象上hooks
,咱們看看ContextOnlyDispatcher
hooks,究竟是什麼。
const ContextOnlyDispatcher = { useState:throwInvalidHookError } function throwInvalidHookError() { invariant( false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', ); }
原來如此,react-hooks
就是經過這種函數組件執行賦值不一樣的hooks
對象方式,判斷在hooks
執行是否在函數組件內部,捕獲並拋出異常的。
最後,從新置空一些變量好比currentHook
,currentlyRenderingFiber
,workInProgressHook
等。
hooks
對象上述講到在函數第一次渲染組件和更新組件分別調用不一樣的hooks
對象,咱們如今就來看看HooksDispatcherOnMount
和 HooksDispatcherOnUpdate
。
第一次渲染(我這裏只展現了經常使用的hooks
):
const HooksDispatcherOnMount = { useCallback: mountCallback, useEffect: mountEffect, useLayoutEffect: mountLayoutEffect, useMemo: mountMemo, useReducer: mountReducer, useRef: mountRef, useState: mountState, };
更新組件:
const HooksDispatcherOnUpdate = { useCallback: updateCallback, useEffect: updateEffect, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, useReducer: updateReducer, useRef: updateRef, useState: updateState };
看來對於第一次渲染組件,和更新組件,react-hooks
採用了兩套Api
,本文的第二部分和第三部分,將重點二者的聯繫。
咱們用流程圖來描述整個過程:
17AC0A26-745A-4FD8-B91B-7CADB717234C.jpg
本文將重點圍繞四個中重點hooks
展開,分別是負責組件更新的useState
,負責執行反作用useEffect
,負責保存數據的useRef
,負責緩存優化的useMemo
, 至於useCallback
,useReducer
,useLayoutEffect
原理和那四個重點hooks
比較相近,就不一一解釋了。
咱們先寫一個組件,而且用到上述四個主要hooks
:
請記住以下代碼片斷,後面講解將以以下代碼段展開
import React , { useEffect , useState , useRef , useMemo } from 'react' function Index(){ const [ number , setNumber ] = useState(0) const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[]) const curRef = useRef(null) useEffect(()=>{ console.log(curRef.current) },[]) return <div ref={ curRef } > hello,world { number } { DivDemo } <button onClick={() => setNumber(number+1) } >number++</button> </div> }
接下來咱們一塊兒研究一下咱們上述寫的四個hooks
最終會變成什麼?
在組件初始化的時候,每一次hooks
執行,如useState()
,useRef()
,都會調用mountWorkInProgressHook
,mountWorkInProgressHook
到底作了寫什麼,讓咱們一塊兒來分析一下:
react-reconciler/src/ReactFiberHooks.js \-> mountWorkInProgressHook function mountWorkInProgressHook() { const hook: Hook = { memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 對象 | useMemo 中 保存的是緩存的值和deps | useRef中保存的是ref 對象 baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // 例子中的第一個`hooks`-> useState(0) 走的就是這樣。 currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
mountWorkInProgressHook
這個函數作的事情很簡單,首先每次執行一個hooks
函數,都產生一個hook
對象,裏面保存了當前hook
信息,而後將每一個hooks
以鏈表形式串聯起來,並賦值給workInProgress
的memoizedState
。也就證明了上述所說的,函數組件用memoizedState
存放hooks
鏈表。
至於hook
對象中都保留了那些信息?我這裏先分別介紹一下 :
memoizedState: useState中
保存 state
信息 | useEffect
中 保存着 effect
對象 | useMemo
中 保存的是緩存的值和 deps
| useRef
中保存的是 ref
對象。
baseQueue : usestate
和useReducer
中 保存最新的更新隊列。
baseState : usestate
和useReducer
中,一次更新中 ,產生的最新state
值。
queue : 保存待更新隊列 pendingQueue
,更新函數 dispatch
等信息。
next: 指向下一個 hooks
對象。
那麼當咱們函數組件執行以後,四個hooks
和workInProgress
將是如圖的關係。
shunxu.jpg
知道每一個hooks
關係以後,咱們應該理解了,爲何不能條件語句中,聲明hooks
。
咱們用一幅圖表示若是在條件語句中聲明會出現什麼狀況發生。
若是咱們將上述demo
其中的一個 useRef
放入條件語句中,
let curRef = null if(isFisrt){ curRef = useRef(null) }
hoo11.jpg
由於一旦在條件語句中聲明hooks
,在下一次函數組件更新,hooks
鏈表結構,將會被破壞,current
樹的memoizedState
緩存hooks
信息,和當前workInProgress
不一致,若是涉及到讀取state
等操做,就會發生異常。
上述介紹了 hooks
經過什麼來證實惟一性的,答案 ,經過hooks
鏈表順序。和爲何不能在條件語句中,聲明hooks
,接下來咱們按照四個方向,分別介紹初始化的時候發生了什麼?
mountState
function mountState( initialState ){ const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { // 若是 useState 第一個參數爲函數,執行函數獲得state initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { pending: null, // 帶更新的 dispatch: null, // 負責更新函數 lastRenderedReducer: basicStateReducer, //用於獲得最新的 state , lastRenderedState: initialState, // 最後一次獲得的 state }); const dispatch = (queue.dispatch = (dispatchAction.bind( // 負責更新的函數 null, currentlyRenderingFiber, queue, ))) return [hook.memoizedState, dispatch]; }
mountState
到底作了些什麼,首先會獲得初始化的state
,將它賦值給mountWorkInProgressHook
產生的hook
對象的 memoizedState
和baseState
屬性,而後建立一個queue
對象,裏面保存了負責更新的信息。
這裏先說一下,在無狀態組件中,useState
和useReducer
觸發函數更新的方法都是dispatchAction
,useState
,能夠當作一個簡化版的useReducer
,至於dispatchAction
怎麼更新state
,更新組件的,咱們接着往下研究dispatchAction
。
在研究以前 咱們先要弄明白dispatchAction
是什麼?
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) const [ number , setNumber ] = useState(0)
dispatchAction
就是 setNumber
, dispatchAction
第一個參數和第二個參數,已經被bind
給改爲currentlyRenderingFiber
和 queue
,咱們傳入的參數是第三個參數action
做爲更新的主要函數,咱們一下來研究一下,我把 dispatchAction
精簡,精簡,再精簡,
function dispatchAction(fiber, queue, action) { // 計算 expirationTime 過程略過。 /* 建立一個update */ const update= { expirationTime, suspenseConfig, action, eagerReducer: null, eagerState: null, next: null, } /* 把建立的update */ const pending = queue.pending; if (pending === null) { // 證實第一次更新 update.next = update; } else { // 不是第一次更新 update.next = pending.next; pending.next = update; } queue.pending = update; const alternate = fiber.alternate; /* 判斷當前是否在渲染階段 */ if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) { didScheduleRenderPhaseUpdate = true; update.expirationTime = renderExpirationTime; currentlyRenderingFiber.expirationTime = renderExpirationTime; } else { /* 當前函數組件對應fiber沒有處於調和渲染階段 ,那麼獲取最新state , 執行更新 */ if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) { const lastRenderedReducer = queue.lastRenderedReducer; if (lastRenderedReducer !== null) { let prevDispatcher; try { const currentState = queue.lastRenderedState; /* 上一次的state */ const eagerState = lastRenderedReducer(currentState, action); /**/ update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { return } } } } scheduleUpdateOnFiber(fiber, expirationTime); } }
不管是類組件調用setState
,仍是函數組件的dispatchAction
,都會產生一個 update
對象,裏面記錄了這次更新的信息,而後將此update
放入待更新的pending
隊列中,dispatchAction
第二步就是判斷當前函數組件的fiber
對象是否處於渲染階段,若是處於渲染階段,那麼不須要咱們在更新當前函數組件,只須要更新一下當前update
的expirationTime
便可。
若是當前fiber
沒有處於更新階段。那麼經過調用lastRenderedReducer
獲取最新的state
,和上一次的currentState
,進行淺比較,若是相等,那麼就退出,這就證明了爲何useState
,兩次值相等的時候,組件不渲染的緣由了,這個機制和Component
模式下的setState
有必定的區別。
若是兩次state
不相等,那麼調用scheduleUpdateOnFiber
調度渲染當前fiber
,scheduleUpdateOnFiber
是react
渲染更新的主要函數。
咱們把初始化mountState
*和*無狀態組件更新機制講明白了,接下來看一下其餘的hooks初始化作了些什麼操做?
上述講到了無狀態組件中fiber
對象memoizedState
保存當前的hooks
造成的鏈表。那麼updateQueue
保存了什麼信息呢,咱們會在接下來探索useEffect
過程當中找到答案。 當咱們調用useEffect
的時候,在組件第一次渲染的時候會調用mountEffect
方法,這個方法到底作了些什麼?
function mountEffect( create, deps, ) { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = pushEffect( HookHasEffect | hookEffectTag, create, // useEffect 第一次參數,就是反作用函數 undefined, nextDeps, // useEffect 第二次參數,deps ); }
每一個hooks
初始化都會建立一個hook
對象,而後將hook的memoizedState
保存當前effect hook
信息。
有兩個memoizedState
你們千萬別混淆了,我這裏再友情提示一遍
workInProgress / current
樹上的 memoizedState
保存的是當前函數組件每一個hooks
造成的鏈表。hooks
上的memoizedState
保存了當前hooks
信息,不一樣種類的hooks
的memoizedState
內容不一樣。上述的方法最後執行了一個pushEffect
,咱們一塊兒看看pushEffect
作了些什麼?function pushEffect(tag, create, destroy, deps) { const effect = { tag, create, destroy, deps, next: null, }; let componentUpdateQueue = currentlyRenderingFiber.updateQueue if (componentUpdateQueue === null) { // 若是是第一個 useEffect componentUpdateQueue = { lastEffect: null } currentlyRenderingFiber.updateQueue = componentUpdateQueue componentUpdateQueue.lastEffect = effect.next = effect; } else { // 存在多個effect const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
這一段實際很簡單,首先建立一個 effect
,判斷組件若是第一次渲染,那麼建立 componentUpdateQueue
,就是workInProgress
的updateQueue
。而後將effect
放入updateQueue
中。
假設咱們在一個函數組件中這麼寫:
useEffect(()=>{ console.log(1) },[ props.a ]) useEffect(()=>{ console.log(2) },[]) useEffect(()=>{ console.log(3) },[])
最後workInProgress.updateQueue
會以這樣的形式保存:
7B8889E7-05B3-4BC4-870A-0D4C1CDF6981.jpg
effect list
能夠理解爲是一個存儲 effectTag
反作用列表容器。它是由 fiber
節點和指針 nextEffect
構成的單鏈表結構,這其中還包括第一個節點 firstEffect
,和最後一個節點 lastEffect
。 React
採用深度優先搜索算法,在 render
階段遍歷 fiber
樹時,把每個有反作用的 fiber
篩選出來,最後構建生成一個只帶反作用的 effect list
鏈表。 在 commit
階段,React
拿到 effect list
數據後,經過遍歷 effect list
,並根據每個 effect
節點的 effectTag
類型,執行每一個effect
,從而對相應的 DOM
樹執行更改。
不知道你們是否把 useMemo
想象的過於複雜了,實際相比其餘 useState
, useEffect
等,它的邏輯實際簡單的很。
function mountMemo(nextCreate,deps){ const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
初始化useMemo
,就是建立一個hook
,而後執行useMemo
的第一個參數,獲得須要緩存的值,而後將值和deps
記錄下來,賦值給當前hook
的memoizedState
。總體上並無複雜的邏輯。
對於useRef
初始化處理,彷佛更是簡單,咱們一塊兒來看一下:
function mountRef(initialValue) { const hook = mountWorkInProgressHook(); const ref = {current: initialValue}; hook.memoizedState = ref; return ref; }
mountRef
初始化很簡單, 建立一個ref對象, 對象的current
屬性來保存初始化的值,最後用memoizedState
保存ref
,完成整個操做。
咱們來總結一下初始化階段,react-hooks
作的事情,在一個函數組件第一次渲染執行上下文過程當中,每一個react-hooks
執行,都會產生一個hook
對象,並造成鏈表結構,綁定在workInProgress
的memoizedState
屬性上,而後react-hooks
上的狀態,綁定在當前hooks
對象的memoizedState
屬性上。對於effect
反作用鉤子,會綁定在workInProgress.updateQueue
上,等到commit
階段,dom
樹構建完成,在執行每一個 effect
反作用鉤子。
上述介紹了第一次渲染函數組件,react-hooks
初始化都作些什麼,接下來,咱們分析一下,
對於更新階段,說明上一次 workInProgress
樹已經賦值給了 current
樹。存放hooks
信息的memoizedState
,此時已經存在current
樹上,react
對於hooks
的處理邏輯和fiber
樹邏輯相似。
對於一次函數組件更新,當再次執行hooks
函數的時候,好比 useState(0)
,首先要從current
的hooks
中找到與當前workInProgressHook
,對應的currentHooks
,而後複製一份currentHooks
給workInProgressHook
,接下來hooks
函數執行的時候,把最新的狀態更新到workInProgressHook
,保證hooks
狀態不丟失。
因此函數組件每次更新,每一次react-hooks
函數執行,都須要有一個函數去作上面的操做,這個函數就是updateWorkInProgressHook
,咱們接下來一塊兒看這個updateWorkInProgressHook
。
function updateWorkInProgressHook() { let nextCurrentHook; if (currentHook === null) { /* 若是 currentHook = null 證實它是第一個hooks */ const current = currentlyRenderingFiber.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { /* 不是第一個hooks,那麼指向下一個 hooks */ nextCurrentHook = currentHook.next; } let nextWorkInProgressHook if (workInProgressHook === null) { //第一次執行hooks // 這裏應該注意一下,當函數組件更新也是調用 renderWithHooks ,memoizedState屬性是置空的 nextWorkInProgressHook = currentlyRenderingFiber.memoizedState; } else { nextWorkInProgressHook = workInProgressHook.next; } if (nextWorkInProgressHook !== null) { /* 這個狀況說明 renderWithHooks 執行 過程發生屢次函數組件的執行 ,咱們暫時先不考慮 */ workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; } else { invariant( nextCurrentHook !== null, 'Rendered more hooks than during the previous render.', ); currentHook = nextCurrentHook; const newHook = { //建立一個新的hook memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, }; if (workInProgressHook === null) { // 若是是第一個hooks currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; } else { // 從新更新 hook workInProgressHook = workInProgressHook.next = newHook; } } return workInProgressHook; }
這一段的邏輯大體是這樣的:
hooks
函數,那麼從current
樹上取出memoizedState
,也就是舊的hooks
。nextWorkInProgressHook
,這裏應該值得注意,正常狀況下,一次renderWithHooks
執行,workInProgress
上的memoizedState
會被置空,hooks
函數順序執行,nextWorkInProgressHook
應該一直爲null
,那麼什麼狀況下nextWorkInProgressHook
不爲null
,也就是當一次renderWithHooks
執行過程當中,執行了屢次函數組件,也就是在renderWithHooks
中這段邏輯。if (workInProgress.expirationTime === renderExpirationTime) { // ....這裏的邏輯咱們先放一放 }
這裏面的邏輯,實際就是斷定,若是當前函數組件執行後,當前函數組件的仍是處於渲染優先級,說明函數組件又有了新的更新任務,那麼循壞執行函數組件。這就形成了上述的,nextWorkInProgressHook
不爲 null
的狀況。
current
的hooks
,把它賦值給workInProgressHook
,用於更新新的一輪hooks
狀態。接下來咱們看一下四個種類的hooks
,在一次組件更新中,分別作了那些操做。
useState
function updateReducer( reducer, initialArg, init, ){ const hook = updateWorkInProgressHook(); const queue = hook.queue; queue.lastRenderedReducer = reducer; const current = currentHook; let baseQueue = current.baseQueue; const pendingQueue = queue.pending; if (pendingQueue !== null) { // 這裏省略... 第一步:將 pending queue 合併到 basequeue } if (baseQueue !== null) { const first = baseQueue.next; let newState = current.baseState; let newBaseState = null; let newBaseQueueFirst = null; let newBaseQueueLast = null; let update = first; do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { //優先級不足 const clone = { expirationTime: update.expirationTime, ... }; if (newBaseQueueLast === null) { newBaseQueueFirst = newBaseQueueLast = clone; newBaseState = newState; } else { newBaseQueueLast = newBaseQueueLast.next = clone; } } else { //此更新確實具備足夠的優先級。 if (newBaseQueueLast !== null) { const clone= { expirationTime: Sync, ... }; newBaseQueueLast = newBaseQueueLast.next = clone; } /* 獲得新的 state */ newState = reducer(newState, action); } update = update.next; } while (update !== null && update !== first); if (newBaseQueueLast === null) { newBaseState = newState; } else { newBaseQueueLast.next = newBaseQueueFirst; } hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } const dispatch = queue.dispatch return [hook.memoizedState, dispatch]; }
這一段看起來很複雜,讓咱們慢慢吃透,首先將上一次更新的pending queue
合併到 basequeue
,爲何要這麼作,好比咱們再一次點擊事件中這麼寫,
function Index(){ const [ number ,setNumber ] = useState(0) const handerClick = ()=>{ // setNumber(1) // setNumber(2) // setNumber(3) setNumber(state=>state+1) // 獲取上次 state = 1 setNumber(state=>state+1) // 獲取上次 state = 2 setNumber(state=>state+1) } console.log(number) // 3 return <div> <div>{ number }</div> <button onClick={ ()=> handerClick() } >點擊</button> </div> }
點擊按鈕, 打印 3
三次setNumber
產生的update
會暫且放入pending queue
,在下一次函數組件執行時候,三次 update
被合併到 baseQueue
。結構以下圖:
setState.jpg
接下來會把當前useState
或是useReduer
對應的hooks
上的baseState
和baseQueue
更新到最新的狀態。會循環baseQueue
的update
,複製一份update
,更新 expirationTime
,對於有足夠優先級的update
(上述三個setNumber
產生的update
都具備足夠的優先級),咱們要獲取最新的state
狀態。,會一次執行useState
上的每個action
。獲得最新的state
。
更新state
sset1.jpg
這裏有會有兩個疑問🤔️:
action
不就能夠了嘛?答案: 緣由很簡單,上面說了 useState
邏輯和useReducer
差很少。若是第一個參數是一個函數,會引用上一次 update
產生的 state
, 因此須要循環調用,每個update
的reducer
,若是setNumber(2)
是這種狀況,那麼只用更新值,若是是setNumber(state=>state+1)
,那麼傳入上一次的 state
獲得最新state
。
updateExpirationTime < renderExpirationTime
)?答案: 這種狀況,通常會發生在,當咱們調用setNumber
時候,調用scheduleUpdateOnFiber
渲染當前組件時,又產生了一次新的更新,因此把最終執行reducer
更新state
任務交給下一次更新。
function updateEffect(create, deps): void { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(hookEffectTag, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.effectTag |= fiberEffectTag hook.memoizedState = pushEffect( HookHasEffect | hookEffectTag, create, destroy, nextDeps, ); }
useEffect
作的事很簡單,判斷兩次deps
相等,若是相等說明這次更新不須要執行,則直接調用 pushEffect
,這裏注意 effect
的標籤,hookEffectTag
,若是不相等,那麼更新 effect
,而且賦值給hook.memoizedState
,這裏標籤是 HookHasEffect | hookEffectTag
,而後在commit
階段,react
會經過標籤來判斷,是否執行當前的 effect
函數。
function updateMemo( nextCreate, deps, ) { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // 新的 deps 值 const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps = prevState[1]; // 以前保存的 deps 值 if (areHookInputsEqual(nextDeps, prevDeps)) { //判斷兩次 deps 值 return prevState[0]; } } } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
在組件更新過程當中,咱們執行useMemo
函數,作的事情實際很簡單,就是判斷兩次 deps
是否相等,若是不想等,證實依賴項發生改變,那麼執行 useMemo
的第一個函數,獲得新的值,而後從新賦值給hook.memoizedState
,若是相等 證實沒有依賴項改變,那麼直接獲取緩存的值。
不過這裏有一點,值得注意,nextCreate()
執行,若是裏面引用了usestate
等信息,變量會被引用,沒法被垃圾回收機制回收,就是閉包原理,那麼訪問的屬性有可能不是最新的值,因此須要把引用的值,添加到依賴項 dep
數組中。每一次dep
改變,從新執行,就不會出現問題了。
舒適小提示: 有不少同窗說 useMemo
怎麼用,到底什麼場景用,用了會不會起到副作用,經過對源碼原理解析,我能夠明確的說,基本上能夠放心使用,說白了就是能夠定製化緩存,存值取值而已。
function updateRef(initialValue){ const hook = updateWorkInProgressHook() return hook.memoizedState }
函數組件更新useRef作的事情更簡單,就是返回了緩存下來的值,也就是不管函數組件怎麼執行,執行多少次,hook.memoizedState
內存中都指向了一個對象,因此解釋了useEffect
,useMemo
中,爲何useRef
不須要依賴注入,就能訪問到最新的改變值。
91A72028-3A38-4491-9375-0895F420B7CD.jpg
上面咱們從函數組件初始化,到函數組件更新渲染,兩個維度分解講解了react-hooks
原理,掌握了react-hooks
原理和內部運行機制,有助於咱們在工做中,更好的使用react-hooks
。