「react進階」一文吃透react-hooks原理

一 前言

以前的兩篇文章,分別介紹了react-hooks如何使用,以及自定義hooks設計模式及其實戰,本篇文章主要從react-hooks起源,原理,源碼角度,開始剖析react-hooks運行機制和內部原理,相信這篇文章事後,對於面試的時候那些hooks問題,也就迎刃而解了。實際react-hooks也並無那麼難以理解,聽起來很cool,實際就是函數組件解決沒有state,生命週期,邏輯不能複用的一種技術方案。前端

Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。react

老規矩,🤔️🤔️🤔️咱們帶着疑問開始今天的探討(能回答上幾個,本身能夠嘗試一下,掌握程度):git

  • 1 在無狀態組件每一次函數上下文執行的時候,react用什麼方式記錄了hooks的狀態?
  • 2 多個react-hooks用什麼來記錄每個hooks的順序的 ? 換個問法!爲何不能條件語句中,聲明hooks? hooks聲明爲何在組件的最頂部?
  • 3 function函數組件中的useState,和 class類組件 setState有什麼區別?
  • 4 react 是怎麼捕獲到hooks的執行上下文,是在函數組件內部的?
  • 5 useEffect,useMemo 中,爲何useRef不須要依賴注入,就能訪問到最新的改變值?
  • 6 useMemo是怎麼對值作緩存的?如何應用它優化性能?
  • 7 爲何兩次傳入useState的值相同,函數組件不更新?
  • ...

大綱.jpg

若是你認真讀完這篇文章,這些問題全會迎刃而解。github

function組件和class組件本質的區別

在解釋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>
    }
}
複製代碼

打印結果?算法

再來看看函數組件中:redux

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>
}
複製代碼

打印結果?設計模式

------------公佈答案-------------
前端工程化

在第一個例子🌰打印結果: 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的面紗

1 當咱們引入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何時用到的 ?

2 開工造物,從無狀態組件的函數執行提及

想要完全弄明白hooks,就要從其根源開始,上述咱們在引入hooks的時候,最後以一個ReactCurrentDispatcher草草收尾,線索所有斷了,因此接下來咱們只能從函數組件執行開始。

renderWithHooks 執行函數

對於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樹的memoizedStateupdateQueue,爲何這麼作,由於在接下來的函數組件執行過程當中,要把新的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,咱們看看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就是經過這種函數組件執行賦值不一樣的hooks對象方式,判斷在hooks執行是否在函數組件內部,捕獲並拋出異常的。

最後,從新置空一些變量好比currentHookcurrentlyRenderingFiber,workInProgressHook等。

3 不一樣的hooks對象

上述講到在函數第一次渲染組件和更新組件分別調用不一樣的hooks對象,咱們如今就來看看HooksDispatcherOnMountHooksDispatcherOnUpdate

第一次渲染(我這裏只展現了經常使用的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初始化,咱們寫的hooks會變成什麼樣子

本文將重點圍繞四個中重點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最終會變成什麼?

1 mountWorkInProgressHook

在組件初始化的時候,每一次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以鏈表形式串聯起來,並賦值給workInProgressmemoizedState。也就證明了上述所說的,函數組件用memoizedState存放hooks鏈表。

至於hook對象中都保留了那些信息?我這裏先分別介紹一下 :

memoizedStateuseState中 保存 state 信息 | useEffect 中 保存着 effect 對象 | useMemo 中 保存的是緩存的值和 depsuseRef 中保存的是 ref 對象。

baseQueue : usestateuseReducer中 保存最新的更新隊列。

baseStateusestateuseReducer中,一次更新中 ,產生的最新state值。

queue : 保存待更新隊列 pendingQueue ,更新函數 dispatch 等信息。

next: 指向下一個 hooks對象。

那麼當咱們函數組件執行以後,四個hooksworkInProgress將是如圖的關係。

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,接下來咱們按照四個方向,分別介紹初始化的時候發生了什麼?

2 初始化useState -> mountState

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對象的 memoizedStatebaseState屬性,而後建立一個queue對象,裏面保存了負責更新的信息。

這裏先說一下,在無狀態組件中,useStateuseReducer觸發函數更新的方法都是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給改爲currentlyRenderingFiberqueue,咱們傳入的參數是第三個參數action

dispatchAction 無狀態組件更新機制

做爲更新的主要函數,咱們一下來研究一下,我把 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對象是否處於渲染階段,若是處於渲染階段,那麼不須要咱們在更新當前函數組件,只須要更新一下當前updateexpirationTime便可。

若是當前fiber沒有處於更新階段。那麼經過調用lastRenderedReducer獲取最新的state,和上一次的currentState,進行淺比較,若是相等,那麼就退出,這就證明了爲何useState,兩次值相等的時候,組件不渲染的緣由了,這個機制和Component模式下的setState有必定的區別。

若是兩次state不相等,那麼調用scheduleUpdateOnFiber調度渲染當前fiberscheduleUpdateOnFiberreact渲染更新的主要函數。

咱們把初始化mountState無狀態組件更新機制講明白了,接下來看一下其餘的hooks初始化作了些什麼操做?

3 初始化useEffect -> mountEffect

上述講到了無狀態組件中fiber對象memoizedState保存當前的hooks造成的鏈表。那麼updateQueue保存了什麼信息呢,咱們會在接下來探索useEffect過程當中找到答案。 當咱們調用useEffect的時候,在組件第一次渲染的時候會調用mountEffect方法,這個方法到底作了些什麼?

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信息,不一樣種類的hooksmemoizedState內容不一樣。上述的方法最後執行了一個pushEffect,咱們一塊兒看看pushEffect作了些什麼?

pushEffect 建立effect對象,掛載updateQueue

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 ,就是workInProgressupdateQueue。而後將effect放入updateQueue中。

假設咱們在一個函數組件中這麼寫:

useEffect(()=>{
    console.log(1)
},[ props.a ])
useEffect(()=>{
    console.log(2)
},[])
useEffect(()=>{
    console.log(3)
},[])
複製代碼

最後workInProgress.updateQueue會以這樣的形式保存:

7B8889E7-05B3-4BC4-870A-0D4C1CDF6981.jpg

拓展:effectList

effect list 能夠理解爲是一個存儲 effectTag 反作用列表容器。它是由 fiber 節點和指針 nextEffect 構成的單鏈表結構,這其中還包括第一個節點 firstEffect ,和最後一個節點 lastEffectReact 採用深度優先搜索算法,在 render 階段遍歷 fiber 樹時,把每個有反作用的 fiber 篩選出來,最後構建生成一個只帶反作用的 effect list 鏈表。 在 commit 階段,React 拿到 effect list 數據後,經過遍歷 effect list,並根據每個 effect 節點的 effectTag 類型,執行每一個effect,從而對相應的 DOM 樹執行更改。

4 初始化useMemo -> mountMemo

不知道你們是否把 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記錄下來,賦值給當前hookmemoizedState。總體上並無複雜的邏輯。

5 初始化useRef -> mountRef

對於useRef初始化處理,彷佛更是簡單,咱們一塊兒來看一下:

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
複製代碼

mountRef初始化很簡單, 建立一個ref對象, 對象的current 屬性來保存初始化的值,最後用memoizedState保存ref,完成整個操做。

6 mounted 階段 hooks 總結

咱們來總結一下初始化階段,react-hooks作的事情,在一個函數組件第一次渲染執行上下文過程當中,每一個react-hooks執行,都會產生一個hook對象,並造成鏈表結構,綁定在workInProgressmemoizedState屬性上,而後react-hooks上的狀態,綁定在當前hooks對象的memoizedState屬性上。對於effect反作用鉤子,會綁定在workInProgress.updateQueue上,等到commit階段,dom樹構建完成,在執行每一個 effect 反作用鉤子。

四 hooks更新階段

上述介紹了第一次渲染函數組件,react-hooks初始化都作些什麼,接下來,咱們分析一下,

對於更新階段,說明上一次 workInProgress 樹已經賦值給了 current 樹。存放hooks信息的memoizedState,此時已經存在current樹上,react對於hooks的處理邏輯和fiber樹邏輯相似。

對於一次函數組件更新,當再次執行hooks函數的時候,好比 useState(0) ,首先要從currenthooks中找到與當前workInProgressHook,對應的currentHooks,而後複製一份currentHooksworkInProgressHook,接下來hooks函數執行的時候,把最新的狀態更新到workInProgressHook,保證hooks狀態不丟失。

因此函數組件每次更新,每一次react-hooks函數執行,都須要有一個函數去作上面的操做,這個函數就是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 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 的狀況。

  • 最後複製currenthooks,把它賦值給workInProgressHook,用於更新新的一輪hooks狀態。

接下來咱們看一下四個種類的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) { //優先級不足
        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上的baseStatebaseQueue更新到最新的狀態。會循環baseQueueupdate,複製一份update,更新 expirationTime,對於有足夠優先級的update(上述三個setNumber產生的update都具備足夠的優先級),咱們要獲取最新的state狀態。,會一次執行useState上的每個action。獲得最新的state

更新state

sset1.jpg

這裏有會有兩個疑問🤔️:

  • 問題一:這裏不是執行最後一個action不就能夠了嘛?

答案: 緣由很簡單,上面說了 useState邏輯和useReducer差很少。若是第一個參數是一個函數,會引用上一次 update產生的 state, 因此須要循環調用,每個updatereducer,若是setNumber(2)是這種狀況,那麼只用更新值,若是是setNumber(state=>state+1),那麼傳入上一次的 state 獲得最新state

  • 問題二:什麼狀況下會有優先級不足的狀況(updateExpirationTime < renderExpirationTime)?

答案: 這種狀況,通常會發生在,當咱們調用setNumber時候,調用scheduleUpdateOnFiber渲染當前組件時,又產生了一次新的更新,因此把最終執行reducer更新state任務交給下一次更新。

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 |= fiberEffectTag
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}
複製代碼

useEffect 作的事很簡單,判斷兩次deps 相等,若是相等說明這次更新不須要執行,則直接調用 pushEffect,這裏注意 effect的標籤,hookEffectTag,若是不相等,那麼更新 effect ,而且賦值給hook.memoizedState,這裏標籤是 HookHasEffect | hookEffectTag,而後在commit階段,react會經過標籤來判斷,是否執行當前的 effect 函數。

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;
}
複製代碼

在組件更新過程當中,咱們執行useMemo函數,作的事情實際很簡單,就是判斷兩次 deps是否相等,若是不想等,證實依賴項發生改變,那麼執行 useMemo的第一個函數,獲得新的值,而後從新賦值給hook.memoizedState,若是相等 證實沒有依賴項改變,那麼直接獲取緩存的值。

不過這裏有一點,值得注意,nextCreate()執行,若是裏面引用了usestate等信息,變量會被引用,沒法被垃圾回收機制回收,就是閉包原理,那麼訪問的屬性有可能不是最新的值,因此須要把引用的值,添加到依賴項 dep 數組中。每一次dep改變,從新執行,就不會出現問題了。

舒適小提示: 有不少同窗說 useMemo怎麼用,到底什麼場景用,用了會不會起到副作用,經過對源碼原理解析,我能夠明確的說,基本上能夠放心使用,說白了就是能夠定製化緩存,存值取值而已。

5 updateRef

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

最後, 送人玫瑰,手留餘香,以爲有收穫的朋友能夠給筆者點贊,關注一波 ,陸續更新前端超硬核文章。

react好文彙總

react-hooks三部曲另外兩部

react進階系列

react源碼系列

開源項目系列

參考文檔

相關文章
相關標籤/搜索