「react進(jìn)階」一文吃透react-hooks原理
一 前言
之前的兩篇文章,分別介紹了react-hooks如何使用,以及自定義hooks設(shè)計模式及其實戰(zhàn),本篇文章主要從react-hooks起源,原理,源碼角度,開始剖析react-hooks運行機(jī)制和內(nèi)部原理,相信這篇文章過后,對于面試的時候那些hooks問題,也就迎刃而解了。實際react-hooks也并沒有那么難以理解,聽起來很cool,實際就是函數(shù)組件解決沒有state,生命周期,邏輯不能復(fù)用的一種技術(shù)方案。
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
老規(guī)矩,?????????我們帶著疑問開始今天的探討(能回答上幾個,自己可以嘗試一下,掌握程度):
1 在無狀態(tài)組件每一次函數(shù)上下文執(zhí)行的時候,
react用什么方式記錄了hooks的狀態(tài)?2 多個
react-hooks用什么來記錄每一個hooks的順序的 ?換個問法!為什么不能條件語句中,聲明hooks?hooks聲明為什么在組件的最頂部?3
function函數(shù)組件中的useState,和class類組件setState有什么區(qū)別?4
react是怎么捕獲到hooks的執(zhí)行上下文,是在函數(shù)組件內(nèi)部的?5
useEffect,useMemo中,為什么useRef不需要依賴注入,就能訪問到最新的改變值?6
useMemo是怎么對值做緩存的?如何應(yīng)用它優(yōu)化性能?7 為什么兩次傳入
useState的值相同,函數(shù)組件不更新?...

如果你認(rèn)真讀完這篇文章,這些問題全會迎刃而解。
function組件和class組件本質(zhì)的區(qū)別
在解釋react-hooks原理的之前,我們要加深理解一下, 函數(shù)組件和類組件到底有什么區(qū)別,廢話不多說,我們先看 兩個代碼片段。
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>}}
打印結(jié)果?
再來看看函數(shù)組件中:
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>}
打印結(jié)果?
------------公布答案-------------
在第一個例子??打印結(jié)果:1 2 3 4 5
在第二個例子??打印結(jié)果:0 0 0 0 0
這個問題實際很蒙人,我們來一起分析一下,第一個類組件中,由于執(zhí)行上setState沒有在react正常的函數(shù)執(zhí)行上下文上執(zhí)行,而是setTimeout中執(zhí)行的,批量更新條件被破壞。原理這里我就不講了,所以可以直接獲取到變化后的state。
但是在無狀態(tài)組件中,似乎沒有生效。原因很簡單,在class狀態(tài)中,通過一個實例化的class,去維護(hù)組件中的各種狀態(tài);但是在function組件中,沒有一個狀態(tài)去保存這些信息,每一次函數(shù)上下文執(zhí)行,所有變量,常量都重新聲明,執(zhí)行完畢,再被垃圾機(jī)制回收。所以如上,無論setTimeout執(zhí)行多少次,都是在當(dāng)前函數(shù)上下文執(zhí)行,此時num = 0不會變,之后setNumber執(zhí)行,函數(shù)組件重新執(zhí)行之后,num才變化。
所以, 對于class組件,我們只需要實例化一次,實例中保存了組件的state等狀態(tài)。對于每一次更新只需要調(diào)用render方法就可以。但是在function組件中,每一次更新都是一次新的函數(shù)執(zhí)行,為了保存一些狀態(tài),執(zhí)行一些副作用鉤子,react-hooks應(yīng)運而生,去幫助記錄組件的狀態(tài),處理一些額外的副作用。
一 初識:揭開hooks的面紗
1 當(dāng)我們引入hooks時候發(fā)生了什么?
我們從引入 hooks開始,以useState為例子,當(dāng)我們從項目中這么寫:
import { useState } from 'react'于是乎我們?nèi)フ?code style="box-sizing: border-box;font-family: Menlo, Monaco, Consolas, "Courier New", monospace;font-size: 0.87em;word-break: break-word;border-radius: 2px;overflow-x: auto;background-color: rgb(255, 245, 245);color: rgb(255, 80, 44);padding: 0.065em 0.4em;">useState,看看它到底是哪路神仙?
react/src/ReactHooks.js
useState
export function useState(initialState){const dispatcher = resolveDispatcher();return dispatcher.useState(initialState);}
useState() 的執(zhí)行等于 dispatcher.useState(initialState) 這里面引入了一個dispatcher,我們看一下resolveDispatcher做了些什么?
resolveDispatcher
function resolveDispatcher() {const dispatcher = ReactCurrentDispatcher.currentreturn dispatcher}
ReactCurrentDispatcher
react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {current: null,};
我們看到ReactCurrentDispatcher.current初始化的時候為null,然后就沒任何下文了。我們暫且只能把**ReactCurrentDispatcher**記下來。看看ReactCurrentDispatcher什么時候用到的 ?
2 開工造物,從無狀態(tài)組件的函數(shù)執(zhí)行說起
想要徹底弄明白hooks,就要從其根源開始,上述我們在引入hooks的時候,最后以一個ReactCurrentDispatcher草草收尾,線索全部斷了,所以接下來我們只能從函數(shù)組件執(zhí)行開始。
renderWithHooks 執(zhí)行函數(shù)
對于function組件是什么時候執(zhí)行的呢?
react-reconciler/src/ReactFiberBeginWork.js
function組件初始化:
renderWithHooks(null, // current FiberworkInProgress, // workInProgress FiberComponent, // 函數(shù)組件本身props, // propscontext, // 上下文renderExpirationTime,// 渲染 ExpirationTime);
對于初始化是沒有current樹的,之后完成一次組件更新后,會把當(dāng)前workInProgress樹賦值給current樹。
function組件更新:
renderWithHooks(current,workInProgress,render,nextProps,context,renderExpirationTime,);
我們從上邊可以看出來,renderWithHooks函數(shù)作用是調(diào)用function組件函數(shù)的主要函數(shù)。我們重點看看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 = nullworkInProgressHook = null;didScheduleRenderPhaseUpdate = false;return children;}
所有的函數(shù)組件執(zhí)行,都是在這里方法中,首先我們應(yīng)該明白幾個感念,這對于后續(xù)我們理解useState是很有幫助的。
current fiber樹: 當(dāng)完成一次渲染之后,會產(chǎn)生一個current樹,current會在commit階段替換成真實的Dom樹。
workInProgress fiber樹: 即將調(diào)和渲染的 fiber 樹。再一次新的組件更新過程中,會從current復(fù)制一份作為workInProgress,更新完畢后,將當(dāng)前的workInProgress樹賦值給current樹。
workInProgress.memoizedState: 在class組件中,memoizedState存放state信息,在function組件中,這里可以提前透漏一下,memoizedState在一次調(diào)和渲染過程中,以鏈表的形式存放hooks信息。
workInProgress.expirationTime: react用不同的expirationTime,來確定更新的優(yōu)先級。
currentHook : 可以理解 current樹上的指向的當(dāng)前調(diào)度的 hooks節(jié)點。
workInProgressHook : 可以理解 workInProgress樹上指向的當(dāng)前調(diào)度的 hooks節(jié)點。
renderWithHooks函數(shù)主要作用:
首先先置空即將調(diào)和渲染的workInProgress樹的memoizedState和updateQueue,為什么這么做,因為在接下來的函數(shù)組件執(zhí)行過程中,要把新的hooks信息掛載到這兩個屬性上,然后在組件commit階段,將workInProgress樹替換成current樹,替換真實的DOM元素節(jié)點。并在current樹保存hooks信息。
然后根據(jù)當(dāng)前函數(shù)組件是否是第一次渲染,賦予ReactCurrentDispatcher.current不同的hooks,終于和上面講到的ReactCurrentDispatcher聯(lián)系到一起。對于第一次渲染組件,那么用的是HooksDispatcherOnMount hooks對象。對于渲染后,需要更新的函數(shù)組件,則是HooksDispatcherOnUpdate對象,那么兩個不同就是通過current樹上是否memoizedState(hook信息)來判斷的。如果current不存在,證明是第一次渲染函數(shù)組件。
接下來,調(diào)用Component(props, secondArg);執(zhí)行我們的函數(shù)組件,我們的函數(shù)組件在這里真正的被執(zhí)行了,然后,我們寫的hooks被依次執(zhí)行,把hooks信息依次保存到workInProgress樹上。 至于它是怎么保存的,我們馬上會講到。
接下來,也很重要,將ContextOnlyDispatcher賦值給 ReactCurrentDispatcher.current,由于js是單線程的,也就是說我們沒有在函數(shù)組件中,調(diào)用的hooks,都是ContextOnlyDispatcher對象上hooks,我們看看ContextOnlyDispatcherhooks,到底是什么。
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就是通過這種函數(shù)組件執(zhí)行賦值不同的hooks對象方式,判斷在hooks執(zhí)行是否在函數(shù)組件內(nèi)部,捕獲并拋出異常的。
最后,重新置空一些變量比如currentHook,currentlyRenderingFiber,workInProgressHook等。
3 不同的hooks對象
上述講到在函數(shù)第一次渲染組件和更新組件分別調(diào)用不同的hooks對象,我們現(xiàn)在就來看看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,本文的第二部分和第三部分,將重點兩者的聯(lián)系。
我們用流程圖來描述整個過程:

三 hooks初始化,我們寫的hooks會變成什么樣子
本文將重點圍繞四個中重點hooks展開,分別是負(fù)責(zé)組件更新的useState,負(fù)責(zé)執(zhí)行副作用useEffect ,負(fù)責(zé)保存數(shù)據(jù)的useRef,負(fù)責(zé)緩存優(yōu)化的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最終會變成什么?
1 mountWorkInProgressHook
在組件初始化的時候,每一次hooks執(zhí)行,如useState(),useRef(),都會調(diào)用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這個函數(shù)做的事情很簡單,首先每次執(zhí)行一個hooks函數(shù),都產(chǎn)生一個hook對象,里面保存了當(dāng)前hook信息,然后將每個hooks以鏈表形式串聯(lián)起來,并賦值給workInProgress的memoizedState。也就證實了上述所說的,函數(shù)組件用memoizedState存放hooks鏈表。
至于hook對象中都保留了哪些信息?我這里先分別介紹一下 :
memoizedState: useState中 保存 state 信息 | useEffect 中 保存著 effect 對象 | useMemo 中 保存的是緩存的值和 deps | useRef 中保存的是 ref 對象。
baseState : usestate和useReducer中 保存最新的更新隊列。
baseState : usestate和useReducer中,一次更新中 ,產(chǎn)生的最新state值。
queue :保存待更新隊列 pendingQueue ,更新函數(shù) dispatch 等信息。
next: 指向下一個 hooks對象。
那么當(dāng)我們函數(shù)組件執(zhí)行之后,四個hooks和workInProgress將是如圖的關(guān)系。

知道每個hooks關(guān)系之后,我們應(yīng)該理解了,為什么不能條件語句中,聲明hooks。
我們用一幅圖表示如果在條件語句中聲明會出現(xiàn)什么情況發(fā)生。
如果我們將上述demo其中的一個 useRef 放入條件語句中,
let curRef = null
if(isFisrt){
curRef = useRef(null)
}

因為一旦在條件語句中聲明hooks,在下一次函數(shù)組件更新,hooks鏈表結(jié)構(gòu),將會被破壞,current樹的memoizedState緩存hooks信息,和當(dāng)前workInProgress不一致,如果涉及到讀取state等操作,就會發(fā)生異常。
上述介紹了 hooks通過什么來證明唯一性的,答案 ,通過hooks鏈表順序。和為什么不能在條件語句中,聲明hooks,接下來我們按照四個方向,分別介紹初始化的時候發(fā)生了什么?
2 初始化useState -> mountState
mountState
function mountState(initialState){const hook = mountWorkInProgressHook();if (typeof initialState === 'function') {// 如果 useState 第一個參數(shù)為函數(shù),執(zhí)行函數(shù)得到stateinitialState = initialState();}hook.memoizedState = hook.baseState = initialState;const queue = (hook.queue = {pending: null, // 帶更新的dispatch: null, // 負(fù)責(zé)更新函數(shù)lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,lastRenderedState: initialState, // 最后一次得到的 state});const dispatch = (queue.dispatch = (dispatchAction.bind( // 負(fù)責(zé)更新的函數(shù)null,currentlyRenderingFiber,queue,)))return [hook.memoizedState, dispatch];}
mountState到底做了些什么,首先會得到初始化的state,將它賦值給mountWorkInProgressHook產(chǎn)生的hook對象的 memoizedState和baseState屬性,然后創(chuàng)建一個queue對象,里面保存了負(fù)責(zé)更新的信息。
這里先說一下,在無狀態(tài)組件中,useState和useReducer觸發(fā)函數(shù)更新的方法都是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 第一個參數(shù)和第二個參數(shù),已經(jīng)被bind給改成currentlyRenderingFiber和 queue,我們傳入的參數(shù)是第三個參數(shù)action
dispatchAction 無狀態(tài)組件更新機(jī)制
作為更新的主要函數(shù),我們一下來研究一下,我把 dispatchAction 精簡,精簡,再精簡:
function dispatchAction(fiber, queue, action) {// 計算 expirationTime 過程略過。/* 創(chuàng)建一個update */const update= {expirationTime,suspenseConfig,action,eagerReducer: null,eagerState: null,next: null,}/* 把創(chuàng)建的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;/* 判斷當(dāng)前是否在渲染階段 */if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {didScheduleRenderPhaseUpdate = true;update.expirationTime = renderExpirationTime;currentlyRenderingFiber.expirationTime = renderExpirationTime;} else { /* 當(dāng)前函數(shù)組件對應(yīng)fiber沒有處于調(diào)和渲染階段 ,那么獲取最新state , 執(zhí)行更新 */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);}}
無論是類組件調(diào)用setState,還是函數(shù)組件的dispatchAction ,都會產(chǎn)生一個 update對象,里面記錄了此次更新的信息,然后將此update放入待更新的pending隊列中,dispatchAction第二步就是判斷當(dāng)前函數(shù)組件的fiber對象是否處于渲染階段,如果處于渲染階段,那么不需要我們在更新當(dāng)前函數(shù)組件,只需要更新一下當(dāng)前update的expirationTime即可。
如果當(dāng)前fiber沒有處于更新階段。那么通過調(diào)用lastRenderedReducer獲取最新的state,和上一次的currentState,進(jìn)行淺比較,如果相等,那么就退出,這就證實了為什么useState,兩次值相等的時候,組件不渲染的原因了,這個機(jī)制和Component模式下的setState有一定的區(qū)別。
如果兩次state不相等,那么調(diào)用scheduleUpdateOnFiber調(diào)度渲染當(dāng)前fiber,scheduleUpdateOnFiber是react渲染更新的主要函數(shù)。
我們把初始化mountState和無狀態(tài)組件更新機(jī)制講明白了,接下來看一下其他的hooks初始化做了些什么操作?
3 初始化useEffect -> mountEffect
上述講到了無狀態(tài)組件中fiber對象memoizedState保存當(dāng)前的hooks形成的鏈表。那么updateQueue保存了什么信息呢,我們會在接下來探索useEffect過程中找到答案。當(dāng)我們調(diào)用useEffect的時候,在組件第一次渲染的時候會調(diào)用mountEffect方法,這個方法到底做了些什么?
mountEffect
function mountEffect(create,deps,) {const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;hook.memoizedState = pushEffect(HookHasEffect | hookEffectTag,create, // useEffect 第一次參數(shù),就是副作用函數(shù)undefined,nextDeps, // useEffect 第二次參數(shù),deps);}
每個hooks初始化都會創(chuàng)建一個hook對象,然后將hook的memoizedState保存當(dāng)前effect hook信息。
有兩個memoizedState大家千萬別混淆了,我這里再友情提示一遍
workInProgress / current樹上的memoizedState保存的是當(dāng)前函數(shù)組件每個hooks形成的鏈表。每個
hooks上的memoizedState保存了當(dāng)前hooks信息,不同種類的hooks的memoizedState內(nèi)容不同。上述的方法最后執(zhí)行了一個pushEffect,我們一起看看pushEffect做了些什么?
pushEffect 創(chuàng)建effect對象,掛載updateQueue
function pushEffect(tag, create, destroy, deps) {const effect = {tag,create,destroy,deps,next: null,};let componentUpdateQueue = currentlyRenderingFiber.updateQueueif (componentUpdateQueue === null) { // 如果是第一個 useEffectcomponentUpdateQueue = { lastEffect: null }currentlyRenderingFiber.updateQueue = componentUpdateQueuecomponentUpdateQueue.lastEffect = effect.next = effect;} else { // 存在多個effectconst 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;}
這一段實際很簡單,首先創(chuàng)建一個 effect ,判斷組件如果第一次渲染,那么創(chuàng)建 componentUpdateQueue ,就是workInProgress的updateQueue。然后將effect放入updateQueue中,不過這里順序要主要,越靠后的effect,越在updateQueue前邊。
假設(shè)我們在一個函數(shù)組件中這么寫:
useEffect(()=>{console.log(1)},[ props.a ])useEffect(()=>{console.log(2)},[])useEffect(()=>{console.log(3)},[])
最后workInProgress.updateQueue會以這樣的形式保存:

拓展:effectList
effect list 可以理解為是一個存儲 effectTag 副作用列表容器。它是由 fiber 節(jié)點和指針 nextEffect 構(gòu)成的單鏈表結(jié)構(gòu),這其中還包括第一個節(jié)點 firstEffect ,和最后一個節(jié)點 lastEffect。 React 采用深度優(yōu)先搜索算法,在 render 階段遍歷 fiber 樹時,把每一個有副作用的 fiber 篩選出來,最后構(gòu)建生成一個只帶副作用的 effect list 鏈表。在 commit 階段,React 拿到 effect list 數(shù)據(jù)后,通過遍歷 effect list,并根據(jù)每一個 effect 節(jié)點的 effectTag 類型,執(zhí)行每個effect,從而對相應(yīng)的 DOM 樹執(zhí)行更改。
4 初始化useMemo -> mountMemo
不知道大家是否把 useMemo 想象的過于復(fù)雜了,實際相比其他 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,就是創(chuàng)建一個hook,然后執(zhí)行useMemo的第一個參數(shù),得到需要緩存的值,然后將值和deps記錄下來,賦值給當(dāng)前hook的memoizedState。整體上并沒有復(fù)雜的邏輯。
5 初始化useRef -> mountRef
對于useRef初始化處理,似乎更是簡單,我們一起來看一下:
function mountRef(initialValue) {const hook = mountWorkInProgressHook();const ref = {current: initialValue};hook.memoizedState = ref;return ref;}
mountRef初始化很簡單, 創(chuàng)建一個ref對象, 對象的current 屬性來保存初始化的值,最后用memoizedState保存ref,完成整個操作。
6 mounted 階段 hooks 總結(jié)
我們來總結(jié)一下初始化階段,react-hooks做的事情,在一個函數(shù)組件第一次渲染執(zhí)行上下文過程中,每個react-hooks執(zhí)行,都會產(chǎn)生一個hook對象,并形成鏈表結(jié)構(gòu),綁定在workInProgress的memoizedState屬性上,然后react-hooks上的狀態(tài),綁定在當(dāng)前hooks對象的memoizedState屬性上。對于effect副作用鉤子,會綁定在workInProgress.updateQueue上,等到commit階段,dom樹構(gòu)建完成,在執(zhí)行每個 effect 副作用鉤子。
四 hooks更新階段
上述介紹了第一次渲染函數(shù)組件,react-hooks初始化都做些什么,接下來,我們分析一下,
對于更新階段,說明上一次 workInProgress 樹已經(jīng)賦值給了 current 樹。存放hooks信息的memoizedState,此時已經(jīng)存在current樹上,react對于hooks的處理邏輯和fiber樹邏輯類似。
對于一次函數(shù)組件更新,當(dāng)再次執(zhí)行hooks函數(shù)的時候,比如 useState(0) ,首先要從current的hooks中找到與當(dāng)前workInProgressHook,對應(yīng)的currentHooks,然后復(fù)制一份currentHooks給workInProgressHook,接下來hooks函數(shù)執(zhí)行的時候,把最新的狀態(tài)更新到workInProgressHook,保證hooks狀態(tài)不丟失。
所以函數(shù)組件每次更新,每一次react-hooks函數(shù)執(zhí)行,都需要有一個函數(shù)去做上面的操作,這個函數(shù)就是updateWorkInProgressHook,我們接下來一起看這個updateWorkInProgressHook。
1 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 nextWorkInProgressHookif (workInProgressHook === null) { //第一次執(zhí)行hooks// 這里應(yīng)該注意一下,當(dāng)函數(shù)組件更新也是調(diào)用 renderWithHooks ,memoizedState屬性是置空的nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;} else {nextWorkInProgressHook = workInProgressHook.next;}if (nextWorkInProgressHook !== null) {/* 這個情況說明 renderWithHooks 執(zhí)行 過程發(fā)生多次函數(shù)組件的執(zhí)行 ,我們暫時先不考慮 */workInProgressHook = nextWorkInProgressHook;nextWorkInProgressHook = workInProgressHook.next;currentHook = nextCurrentHook;} else {invariant(nextCurrentHook !== null,'Rendered more hooks than during the previous render.',);currentHook = nextCurrentHook;const newHook = { //創(chuàng)建一個新的hookmemoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null,};if (workInProgressHook === null) { // 如果是第一個hookscurrentlyRenderingFiber.memoizedState = workInProgressHook = newHook;} else { // 重新更新 hookworkInProgressHook = workInProgressHook.next = newHook;}}return workInProgressHook;}
這一段的邏輯大致是這樣的:
首先如果是第一次執(zhí)行
hooks函數(shù),那么從current樹上取出memoizedState,也就是舊的hooks。然后聲明變量
nextWorkInProgressHook,這里應(yīng)該值得注意,正常情況下,一次renderWithHooks執(zhí)行,workInProgress上的memoizedState會被置空,hooks函數(shù)順序執(zhí)行,nextWorkInProgressHook應(yīng)該一直為null,那么什么情況下nextWorkInProgressHook不為null,也就是當(dāng)一次renderWithHooks執(zhí)行過程中,執(zhí)行了多次函數(shù)組件,也就是在renderWithHooks中這段邏輯。if (workInProgress.expirationTime === renderExpirationTime) {// ....這里的邏輯我們先放一放}
這里面的邏輯,實際就是判定,如果當(dāng)前函數(shù)組件執(zhí)行后,當(dāng)前函數(shù)組件的還是處于渲染優(yōu)先級,說明函數(shù)組件又有了新的更新任務(wù),那么循壞執(zhí)行函數(shù)組件。這就造成了上述的,nextWorkInProgressHook不為 null 的情況。
最后復(fù)制
current的hooks,把它賦值給workInProgressHook,用于更新新的一輪hooks狀態(tài)。
接下來我們看一下四個種類的hooks,在一次組件更新中,分別做了那些操作。
2 updateState
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) { //優(yōu)先級不足const clone = {expirationTime: update.expirationTime,...};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}} else { //此更新確實具有足夠的優(yōu)先級。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.dispatchreturn [hook.memoizedState, dispatch];}
這一段看起來很復(fù)雜,讓我們慢慢吃透,首先將上一次更新的pending queue 合并到 basequeue,為什么要這么做,比如我們在一次點擊事件中這么寫,
function Index(){const [ number ,setNumber ] = useState(0)const handerClick = ()=>{// setNumber(1)// setNumber(2)// setNumber(3)setNumber(state=>state+1)// 獲取上次 state = 1setNumber(state=>state+1)// 獲取上次 state = 2setNumber(state=>state+1)}console.log(number) // 3return <div><div>{ number }</div><button onClick={ ()=> handerClick() } >點擊</button></div>}
點擊按鈕, 打印 3
三次setNumber產(chǎn)生的update會暫且放入pending queue,在下一次函數(shù)組件執(zhí)行時候,三次 update被合并到 baseQueue。結(jié)構(gòu)如下圖:

接下來會把當(dāng)前useState或是useReduer對應(yīng)的hooks上的baseState和baseQueue更新到最新的狀態(tài)。會循環(huán)baseQueue的update,復(fù)制一份update,更新 expirationTime,對于有足夠優(yōu)先級的update(上述三個setNumber產(chǎn)生的update都具有足夠的優(yōu)先級),我們要獲取最新的state狀態(tài)。,會一次執(zhí)行useState上的每一個action。得到最新的state。
更新state

這里有會有兩個疑問???:
問題一:這里不是執(zhí)行最后一個
action不就可以了嘛?
答案:原因很簡單,上面說了 useState邏輯和useReducer差不多。如果第一個參數(shù)是一個函數(shù),會引用上一次 update產(chǎn)生的 state, 所以需要循環(huán)調(diào)用,每一個update的reducer,如果setNumber(2)是這種情況,那么只用更新值,如果是setNumber(state=>state+1),那么傳入上一次的 state 得到最新state。
問題二:什么情況下會有優(yōu)先級不足的情況(
updateExpirationTime < renderExpirationTime)?
答案:這種情況,一般會發(fā)生在,當(dāng)我們調(diào)用setNumber時候,調(diào)用scheduleUpdateOnFiber渲染當(dāng)前組件時,又產(chǎn)生了一次新的更新,所以把最終執(zhí)行reducer更新state任務(wù)交給下一次更新。
3 updateEffect
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 |= fiberEffectTaghook.memoizedState = pushEffect(HookHasEffect | hookEffectTag,create,destroy,nextDeps,);}
useEffect 做的事很簡單,判斷兩次deps 相等,如果相等說明此次更新不需要執(zhí)行,則直接調(diào)用 pushEffect,這里注意 effect的標(biāo)簽,hookEffectTag,如果不相等,那么更新 effect ,并且賦值給hook.memoizedState,這里標(biāo)簽是 HookHasEffect | hookEffectTag,然后在commit階段,react會通過標(biāo)簽來判斷,是否執(zhí)行當(dāng)前的 effect 函數(shù)。
4 updateMemo
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;}
在組件更新過程中,我們執(zhí)行useMemo函數(shù),做的事情實際很簡單,就是判斷兩次 deps是否相等,如果不想等,證明依賴項發(fā)生改變,那么執(zhí)行 useMemo的第一個函數(shù),得到新的值,然后重新賦值給hook.memoizedState,如果相等 證明沒有依賴項改變,那么直接獲取緩存的值。
不過這里有一點,值得注意,nextCreate()執(zhí)行,如果里面引用了usestate等信息,變量會被引用,無法被垃圾回收機(jī)制回收,就是閉包原理,那么訪問的屬性有可能不是最新的值,所以需要把引用的值,添加到依賴項 dep 數(shù)組中。每一次dep改變,重新執(zhí)行,就不會出現(xiàn)問題了。
溫馨小提示:有很多同學(xué)說 useMemo怎么用,到底什么場景用,用了會不會起到反作用,通過對源碼原理解析,我可以明確的說,基本上可以放心使用,說白了就是可以定制化緩存,存值取值而已。
5 updateRef
function updateRef(initialValue){const hook = updateWorkInProgressHook()return hook.memoizedState}
函數(shù)組件更新useRef做的事情更簡單,就是返回了緩存下來的值,也就是無論函數(shù)組件怎么執(zhí)行,執(zhí)行多少次,hook.memoizedState內(nèi)存中都指向了一個對象,所以解釋了useEffect,useMemo 中,為什么useRef不需要依賴注入,就能訪問到最新的改變值。
一次點擊事件更新

五 總結(jié)
上面我們從函數(shù)組件初始化,到函數(shù)組件更新渲染,兩個維度分解講解了react-hooks原理,掌握了react-hooks原理和內(nèi)部運行機(jī)制,有助于我們在工作中,更好的使用react-hooks。
最后, 送人玫瑰,手留余香,覺得有收獲的朋友可以給筆者點贊,關(guān)注一波 ,陸續(xù)更新前端超硬核文章。
