React Hooks源碼解析,原來這麼簡單~

忙於業務的緣由,很長一段時間沒有寫過博客了,最近開始繼續拿起筆來學習,本篇是筆者React源碼閱讀系列的第一篇,展現的代碼都通過簡化,去掉了靜態類型和fiber有關的邏輯,保留最本質的Hook有關邏輯,但願對你們有幫助~react

前言

從React Hooks發佈以來,整個社區都以積極的態度去擁抱它、學習它。期間也涌現了不少關於React Hooks 源碼解析的文章。本文(基於v16.8.6)就以筆者本身的角度來寫一篇屬於本身的文章吧。但願能夠深刻淺出、圖文並茂的幫助你們對React Hooks的實現原理進行學習與理解。本文將以文字、代碼、圖畫的形式來呈現內容。主要對經常使用Hooks中的 useState、useReducer、useEffect 進行學習,儘量的揭開Hooks的面紗。數據結構

使用Hooks時的疑惑

Hooks的面世讓咱們的Function Component逐步擁有了對標Class Component的特性,好比私有狀態,生命週期函數等。useState與useReducer這兩個Hooks讓咱們能夠在 Function Component裏使用到私有狀態。而useState其實就是閹割版的useReducer,這也是我那它們兩個放在一塊兒講的緣由。應用一下官方的例子:函數

function PersionInfo ({initialAge,initialName}) {
  const [age, setAge] = useState(initialAge);
  const [name, setName] = useState(initialName);
  return (
    <> Age: {age}, Name: {name} <button onClick={() => setAge(age + 1)}>Growing up</button> </> ); } 複製代碼

經過 useState 咱們能夠初始化一個私有狀態,它會返回這個狀態的最新值和一個用來更新狀態的方法。而useReducer則是針對更復雜的狀態管理場景:學習

const initialState = {age: 0, name: 'Dan'};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state, age: state.age + action.age};
    case 'decrement':
      return {...state, age: state.age - action.age};
    default:
      throw new Error();
  }
}
function PersionInfo() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <> Age: {state.age}, Name: {state.name} <button onClick={() => dispatch({type: 'decrement', age: 1})}>-</button> <button onClick={() => dispatch({type: 'increment', age: 1})}>+</button> </> ); } 複製代碼

一樣也是返回當前最新的狀態,並返回一個用來更新數據的方法。在使用這兩個方法的時候也許咱們會想過這樣的問題:ui

const [age, setAge] = useState(initialAge);
  const [name, setName] = useState(initialName);
複製代碼

React是內部是怎麼區分這兩個狀態的呢? Function Component 不像 Class Component那樣能夠將私有狀態掛載到類實例中並經過對應的key來指向對應的狀態,並且每次的頁面的刷新或者說組件的從新渲染都會使得 Function 從新執行一遍。因此React中一定有一種機制來區分這些Hooks。spa

const [age, setAge] = useState(initialAge);
 // 或
 const [state, dispatch] = useReducer(reducer, initialState);
複製代碼

另外一個問題就是React是如何在每次從新渲染以後都能返回最新的狀態? Class Component由於自身的特色能夠將私有狀態持久化的掛載到類實例上,每時每刻保存的都是最新的值。而 Function Component 因爲本質就是一個函數,而且每次渲染都會從新執行。因此React一定擁有某種機制去記住每一次的更新操做,並最終得出最新的值返回。固然咱們還會有其餘的一些問題,好比這些狀態究竟存放在哪?爲何只能在函數頂層使用Hooks而不能在條件語句等裏面使用Hooks?3d

答案盡在源碼之中

咱們先來了解useState以及useReducer的源碼實現,並從中解答咱們在使用Hooks時的種種疑惑。首先咱們從源頭開始:code

import React, { useState } from 'react';
複製代碼

在項目中咱們一般會以這種方式來引入useState方法,被咱們引入的這個useState方法是什麼樣子的呢?其實這個方法就在源碼 packages/react/src/ReactHook.js 中。component

// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  // ... 
  return dispatcher;
}

// 咱們代碼中引入的useState方法
export function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState)
}
複製代碼

從源碼中能夠看到,咱們調用的實際上是 ReactCurrentDispatcher.js 中的dispatcher.useState(),那麼咱們繼續前往ReactCurrentDispatcher.js文件:cdn

import type {Dispacther} from 'react-reconciler/src/ReactFiberHooks';

const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;
複製代碼

好吧,它繼續將咱們帶向 react-reconciler/src/ReactFiberHooks.js這個文件。那麼咱們繼續前往這個文件。

// react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {
  useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
  useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: (I) => S,
  ): [S, Dispatch<A>],
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  // 其餘hooks類型定義
}
複製代碼

兜兜轉轉咱們終於清楚了React Hooks 的源碼就放 react-reconciler/src/ReactFiberHooks.js 目錄下面。 在這裏如上圖所示咱們能夠看到有每一個Hooks的類型定義。同時咱們也能夠看到Hooks的具體實現,你們能夠多看看這個文件。首先咱們注意到,咱們大部分的Hooks都有兩個定義:

// react-reconciler/src/ReactFiberHooks.js
// Mount 階段Hooks的定義
const HooksDispatcherOnMount: Dispatcher = {
  useEffect: mountEffect,
  useReducer: mountReducer,
  useState: mountState,
 // 其餘Hooks
};

// Update階段Hooks的定義
const HooksDispatcherOnUpdate: Dispatcher = {
  useEffect: updateEffect,
  useReducer: updateReducer,
  useState: updateState,
  // 其餘Hooks
};
複製代碼

從這裏能夠看出,咱們的Hooks在Mount階段和Update階段的邏輯是不同的。在Mount階段和Update階段他們是兩個不一樣的定義。咱們先來看Mount階段的邏輯。在看以前咱們先思考一些問題。React Hooks須要在Mount階段作什麼呢?就拿咱們的useState和useReducer來講:

  1. 咱們須要初始化狀態,並返回修改狀態的方法,這是最基本的。
  2. 咱們要區分管理每一個Hooks。
  3. 提供一個數據結構去存放更新邏輯,以便後續每次更新能夠拿到最新的值。

咱們一下React的實現,先來看mountState的實現。

// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
  // 獲取當前的Hook節點,同時將當前Hook添加到Hook鏈表中
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  // 聲明一個鏈表來存放更新
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer,
    lastRenderedState,
  });
  // 返回一個dispatch方法用來修改狀態,並將這次更新添加update鏈表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  // 返回當前狀態和修改狀態的方法 
  return [hook.memoizedState, dispatch];
}
複製代碼

區分管理Hooks

關於第一件事,初始化狀態並返回狀態和更新狀態的方法。這個沒有問題,源碼也很清晰利用initialState來初始化狀態,而且返回了狀態和對應更新方法 return [hook.memoizedState, dispatch]。那麼咱們來看看React是如何區分不一樣的Hooks的,這裏咱們能夠從 mountState 裏的 mountWorkInProgressHook方法和Hook的類型定義中找到答案。

// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,  // 指向下一個Hook
};
複製代碼

首先從Hook的類型定義中就能夠看到,React 對Hooks的定義是鏈表。也就是說咱們組件裏使用到的Hooks是經過鏈表來聯繫的,上一個Hooks的next指向下一個Hooks。這些Hooks節點是怎麼利用鏈表數據結構串聯在一塊兒的呢?相關邏輯就在每一個具體mount 階段 Hooks函數調用的 mountWorkInProgressHook方法裏:

// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // 當前workInProgressHook鏈表爲空的話,
    // 將當前Hook做爲第一個Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 不然將當前Hook添加到Hook鏈表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
複製代碼

在mount階段,每當咱們調用Hooks方法,好比useState,mountState就會調用mountWorkInProgressHook 來建立一個Hook節點,並把它添加到Hooks鏈表上。好比咱們的這個例子:

const [age, setAge] = useState(initialAge);
  const [name, setName] = useState(initialName);
  useEffect(() => {})
複製代碼

那麼在mount階段,就會生產以下圖這樣的單鏈表:

返回最新的值

而關於第三件事,useState和useReducer都是使用了一個queue鏈表來存放每一次的更新。以便後面的update階段能夠返回最新的狀態。每次咱們調用dispatchAction方法的時候,就會造成一個新的updata對象,添加到queue鏈表上,並且這個是一個循環鏈表。能夠看一下 dispatchAction 方法的實現:

// react-reconciler/src/ReactFiberHooks.js
// 去除特殊狀況和與fiber相關的邏輯
function dispatchAction(fiber,queue,action,) {
    const update = {
      action,
      next: null,
    };
    // 將update對象添加到循環鏈表中
    const last = queue.last;
    if (last === null) {
      // 鏈表爲空,將當前更新做爲第一個,並保持循環
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // 在最新的update對象後面插入新的update對象
        update.next = first;
      }
      last.next = update;
    }
    // 將表頭保持在最新的update對象上
    queue.last = update;
   // 進行調度工做
    scheduleWork();
}
複製代碼

也就是咱們每次執行dispatchAction方法,好比setAge或setName。就會建立一個保存着這次更新信息的update對象,添加到更新鏈表queue上。而後每一個Hooks節點就會有本身的一個queque。好比假設咱們執行了下面幾個語句:

setAge(19);
setAge(20);
setAge(21);
複製代碼

那麼咱們的Hooks鏈表就會變成這樣:

updateQueue
在Hooks節點上面,會如上圖那樣,經過鏈表來存放全部的歷史更新操做。以便在update階段能夠經過這些更新獲取到最新的值返回給咱們。這就是在第一次調用useState或useReducer以後,每次更新都能返回最新值的緣由。再來看看mountReducer,你會發現和mountState幾乎一摸同樣,只是狀態的初始化邏輯有那麼一點區別。畢竟useState其實就是閹割版的useReducer。這裏就不詳細介紹mountReducer了。

// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init,) {
  // 獲取當前的Hook節點,同時將當前Hook添加到Hook鏈表中
  const hook = mountWorkInProgressHook();
  let initialState;
  // 初始化
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg ;
  }
  hook.memoizedState = hook.baseState = initialState;
  // 存放更新對象的鏈表
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  // 返回一個dispatch方法用來修改狀態,並將這次更新添加update鏈表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
 // 返回狀態和修改狀態的方法
  return [hook.memoizedState, dispatch];
}
複製代碼

而後咱們來看看update階段,也就是看一下咱們的useState或useReducer是如何利用現有的信息,去給咱們返回最新的最正確的值的。先來看一下useState在update階段的代碼也就是updateState:

// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}
複製代碼

能夠看到,updateState底層調用的其實就會死updateReducer,由於咱們調用useState的時候,並不會傳入reducer,因此這裏會默認傳遞一個basicStateReducer進去。咱們先看看這個basicStateReducer

// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
  return typeof action === 'function' ? action(state) : action;
} 
複製代碼

在使用useState(action)的時候,action一般會是一個值,而不是一個方法。因此baseStateReducer要作的其實就是將這個action返回。來繼續看一下updateReducer的邏輯:

// react-reconciler/src/ReactFiberHooks.js
// 去掉與fiber有關的邏輯

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新列表的表頭
  const last = queue.last;

  // 獲取最先的那個update對象
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState;
    let update = first;
    do {
      // 執行每一次更新,去更新狀態
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的狀態和修改狀態的方法
  return [hook.memoizedState, dispatch];
}
複製代碼

在update階段,也就是咱們組件第二次第三次。。執行到useState或useReducer的時候,會遍歷update對象循環鏈表,執行每一次更新去計算出最新的狀態來返回,以保證咱們每次刷新組件都能拿到當前最新的狀態。useState的reducer是baseStateReducer,由於傳入的update.action爲值,因此會直接返回update.action,而useReducer 的reducer是用戶定義的reducer,因此會根據傳入的action和每次循環獲得的newState逐步計算出最新的狀態。

updateReducer1

useState/useReducer 小總結

看到這裏咱們在回頭看看最初的一些疑問:

  1. React 如何管理區分Hooks?

    • React經過單鏈表來管理Hooks
    • 按Hooks的執行順序依次將Hook節點添加到鏈表中
  2. useState和useReducer如何在每次渲染時,返回最新的值?

    • 每一個Hook節點經過循環鏈表記住全部的更新操做
    • 在update階段會依次執行update循環鏈表中的全部更新操做,最終拿到最新的state返回
  3. 爲何不能在條件語句等中使用Hooks?

    • 鏈表!

好比如圖所示,咱們在mount階段調用了 useState('A'), useState('B'), useState('C'),若是咱們將 useState('B') 放在條件語句內執行,而且在update階段中由於不知足條件而沒有執行的話,那麼無法正確的重Hooks鏈表中獲取信息。React也會給咱們報錯。

Hooks鏈表放在哪?

好的,如今咱們已經瞭解了React 經過鏈表來管理 Hooks,同時也是經過一個循環鏈表來存放每一次的更新操做,得以在每次組件更新的時候能夠計算出最新的狀態返回給咱們。那麼咱們這個Hooks鏈表又存放在那裏呢?理所固然的咱們須要將它存放到一個跟當前組件相對於的地方。那麼很明顯這個與組件一一對應的地方就是咱們的FiberNode。

hooksInFiberNode

如圖所示,組件構建的Hooks鏈表會掛載到FiberNode節點的memoizedState上面去。

hooksInFiberNode2

useEffect

看到這,相信你已經對Hooks的源碼實現模式已經有必定的瞭解了,因此你嘗試去看一下Effect的實現你會一會兒就看懂。首先咱們先回憶一下useEffect是怎麼樣工做的?

function PersionInfo () {
  const [age, setAge] = useState(18);
  useEffect(() =>{
      console.log(age)
  }, [age])

 const [name, setName] = useState('Dan');
 useEffect(() =>{
      console.log(name)
  }, [name])
  return (
    <> ... </> ); } 複製代碼

PersionInfo組件第一次渲染的時候會在控制檯輸出age和name,在後面組件的每次update中,若是useEffect中的deps依賴的值發生了變化的話,也會在控制檯中輸出對應的狀態,同時在unmount的時候就會執行清除函數(若是有)。React中是怎麼實現的呢?其實很簡單,在FiberNode中經過一個updateQueue來存放全部須要被執行的effect,而後在每次渲染以後依次執行全部的effect。useEffect 也分爲mountEffect和updateEffect

mountEffect

// react-reconciler/src/ReactFiberHooks.js
// 簡化去掉特殊邏輯

function mountEffect( create,deps,) {
  return mountEffectImpl(
    create,
    deps,
  );
}

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  // 獲取當前Hook,並把當前Hook添加到Hook鏈表
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 將當前effect保存到Hook節點的memoizedState屬性上,
  // 以及添加到fiberNode的updateQueue上
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // componentUpdateQueue 會被掛載到fiberNode的updateQueue上
  if (componentUpdateQueue === null) {
    // 若是當前Queue爲空,將當前effect做爲第一個節點
    componentUpdateQueue = createFunctionComponentUpdateQueue();
   // 保持循環
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 不然,添加到當前的Queue鏈表中
    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; 
}
複製代碼

能夠看到在mount階段,useEffect作的事情就是將本身的effect添加到了componentUpdateQueue上。這個componentUpdateQueue會在renderWithHooks方法中賦值到fiberNode的updateQueue上。

// react-reconciler/src/ReactFiberHooks.js
// 簡化去掉特殊邏輯
export function renderWithHooks() {
   const renderedWork = currentlyRenderingFiber;
   renderedWork.updateQueue = componentUpdateQueue;
}
複製代碼

也就是在mount階段咱們全部的effect都以鏈表的形式被掛載到了fiberNode上。而後在組件渲染完畢以後,React就會執行updateQueue中的全部方法。

useEffectInFiberNode1

updateEffect

// react-reconciler/src/ReactFiberHooks.js
// 簡化去掉特殊邏輯

function updateEffect(create,deps){
  return updateEffectImpl(
    create,
    deps,
  );
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
  // 獲取當前Hook節點,並把它添加到Hook鏈表
  const hook = updateWorkInProgressHook();
  // 依賴 
  const nextDeps = deps === undefined ? null : deps;
 // 清除函數
  let destroy = undefined;

  if (currentHook !== null) {
    // 拿到前一次渲染該Hook節點的effect
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 對比deps依賴
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 若是依賴有變化,就添加到FiberNode的updateQueue中
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        return;
      }
    }
  }
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
複製代碼

update階段和mount階段相似,只不過此次會考慮effect 的依賴deps,只有deps改變了的effect纔會被添加到FiberNode的updateQueue,以便渲染完畢後執行全部effect。

useEffect 小總結

useEffectInFiberNode2
useEffect作了什麼?

  1. FiberNdoe節點中會又一個updateQueue鏈表來存放全部的本次渲染須要執行的effect。
  2. mountEffect 也就是第一次渲染(mount階段)會把全部的effcect添加到updateQueue鏈表中,以便渲染後依次執行。至關於聲明週期componentDidMount。
  3. updateEffect 也就是update階段,將全部deps改變的effect添加到updateQueue鏈表中,以便渲染後依次執行,至關於 componentDidUpdate

到此爲止,useState/useReducer/useEffect源碼也閱讀完畢了,相信有了這些基礎,剩下的Hooks的源碼閱讀不會成問題,最後放上完整圖示:

allHooks1
相關文章
相關標籤/搜索