從源碼剖析useState的執行過程

長文預警,若是以爲前戲太長可直接從第三章開始看~javascript

本文基於 React 16.8.6 進行講解css

使用的示例代碼:html

import React, { useState } from 'react'
import './App.css'

export default function App() {
  
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Star');
  
  // 調用三次setCount便於查看更新隊列的狀況
  const countPlusThree = () => {
    setCount(count+1);
    setCount(count+2);
    setCount(count+3);
  }
  return (
    <div className='App'> <p>{name} Has Clicked <strong>{count}</strong> Times</p> <button onClick={countPlusThree}>Click *3</button> </div>
  )
}
複製代碼

代碼很是簡單,點擊button使count+3,count的值會顯示在屏幕上。前端

Jietu20190419-090633@2x.png

一. 前置知識

1. 函數組件和類組件

本節參考:How Are Function Components Different from Classes?java

本節主要概念:react

  • 函數組件和類組件的區別
  • React如何區分這兩種組件

咱們來看一個簡單的Greeting組件,它支持定義成類和函數兩種性質。在使用它時,不用關心他是如何定義的。git

// 是類仍是函數 —— 無所謂
<Greeting />  // <p>Hello</p>
複製代碼

若是 Greeting 是一個函數,React 須要調用它。github

// Greeting.js
function Greeting() {
  return <p>Hello</p>;
}

// React 內部
const result = Greeting(props); // <p>Hello</p>
複製代碼

但若是 Greeting 是一個類,React 須要先將其實例化,再調用剛纔生成實例的 render 方法:react-native

// Greeting.js
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// React 內部
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
複製代碼

React經過如下方式來判斷組件的類型:數組

// React 內部
class Component {}
Component.prototype.isReactComponent = {};

// 檢查方式
class Greeting extends React.Component {}
console.log(Greeting.prototype.isReactComponent); // {}
複製代碼

2. React Fiber

本節參考:A cartoon intro to fiber

本節主要概念(瞭解便可):

  • React如今的渲染都是由Fiber來調度
  • Fiber調度過程當中的兩個階段(以Render爲界)

Fiber(可譯爲絲)比線程還細的控制粒度,是React 16中的新特性,旨在對渲染過程作更精細的調整。

產生緣由:

  1. Fiber以前的reconciler(被稱爲Stack reconciler)自頂向下的遞歸mount/update,沒法中斷(持續佔用主線程),這樣主線程上的佈局、動畫等週期性任務以及交互響應就沒法當即獲得處理,影響體驗
  2. 渲染過程當中沒有優先級可言

React Fiber的方式:

把一個耗時長的任務分紅不少小片,每個小片的運行時間很短,雖然總時間依然很長,可是在每一個小片執行完以後,都給其餘任務一個執行的機會,這樣惟一的線程就不會被獨佔,其餘任務依然有運行的機會。

React Fiber把更新過程碎片化,執行過程以下面的圖所示,每執行完一段更新過程,就把控制權交還給React負責任務協調的模塊,看看有沒有其餘緊急任務要作,若是沒有就繼續去更新,若是有緊急任務,那就去作緊急任務。

維護每個分片的數據結構,就是Fiber。

有了分片以後,更新過程的調用棧以下圖所示,中間每個波谷表明深刻某個分片的執行過程,每一個波峯就是一個分片執行結束交還控制權的時機。讓線程處理別的事情

Fiber的調度過程分爲如下兩個階段:

render/reconciliation階段 — 裏面的全部生命週期函數均可能被執行屢次,因此儘可能保證狀態不變

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit階段 — 不能被打斷,只會執行一次

  • componentDidMount
  • componentDidUpdate
  • compoenntWillunmount

Fiber的增量更新須要更多的上下文信息,以前的vDOM tree顯然難以知足,因此擴展出了fiber tree(即Fiber上下文的vDOM tree),更新過程就是根據輸入數據以及現有的fiber tree構造出新的fiber tree(workInProgress tree)

與Fiber有關的全部代碼位於packages/react-reconciler中,一個Fiber節點的詳細定義以下:

function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) {
  // Instance
  this.tag = tag; this.key = key; this.elementType = null; 
  this.type = null; this.stateNode = null;

  // Fiber
  this.return = null; this.child = null; this.sibling = null; 
  this.index = 0; this.ref = null; this.pendingProps = pendingProps;
  this.memoizedProps = null; this.updateQueue = null;
  
  // 重點
  this.memoizedState = null;
  
  this.contextDependencies = null; this.mode = mode;

  // Effects
  /** 細節略 **/
}
複製代碼

咱們只關注一下this.memoizedState

這個key用來存儲在上次渲染過程當中最終得到的節點的state,每次render以前,React會計算出當前組件最新的state而後賦值給組件,再執行render— 類組件和使用useState的函數組件均適用。

記住上面這句話,後面還會常常提到memoizedState

有關Fiber每一個key的具體含義能夠參見源碼的註釋

3. React渲染器與setState

本節參考:How Does setState Know What to Do?

本節主要概念:

  • React渲染器是什麼
  • setState爲何可以觸發更新

因爲React體系的複雜性以及目標平臺的多樣性。react包只暴露一些定義組件的API。絕大多數React的實現都存在於 渲染器(renderers)中。

react-domreact-dom/serverreact-nativereact-test-rendererreact-art都是常見的渲染器

這就是爲何無論目標平臺是什麼,react包都是可用的。從react包中導出的一切,好比React.ComponentReact.createElementReact.ChildrenHooks都是獨立於目標平臺的。不管運行React DOM,仍是 React DOM Server,或是 React Native,組件均可以使用一樣的方式導入和使用。

因此當咱們想使用新特性時,reactreact-dom都須要被更新。

例如,當React 16.3添加了Context API,React.createContext()API會被React包暴露出來。 可是React.createContext() 其實並無_實現_ context。由於在React DOM 和 React DOM Server 中一樣一個 API 應當有不一樣的實現。因此createContext()只返回了一些普通對象: **因此,若是你將react升級到了16.3+,可是不更新react-dom,那麼你就使用了一個尚不知道Provider 和 Consumer類型的渲染器。**這就是爲何老版本的react-dom報錯說這些類型是無效的

這就是setState 儘管定義在React包中,調用時卻可以更新DOM的緣由。它讀取由React DOM設置的this.updater,讓React DOM安排並處理更新。

Component.setState = function(partialState, callback) {
  // setState所作的一切就是委託渲染器建立這個組件的實例
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼

各個渲染器中的updater觸發不一樣平臺的更新渲染

// React DOM 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;

// React DOM Server 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;

// React Native 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
複製代碼

至於updater的具體實現,就不是這裏重點要討論的內容了,下面讓咱們正式進入本文的主題:React Hooks

二. 瞭解useState

1. useState的引入和觸發更新

本節主要概念:

  • useState是如何被引入以及調用的
  • useState爲何能觸發組件更新

全部的Hooks在React.js中被引入,掛載在React對象中

// React.js
import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
} from './ReactHooks';
複製代碼

咱們進入ReactHooks.js來看看,發現useState的實現居然異常簡單,只有短短兩行

// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
複製代碼

看來重點都在這個dispatcher上,dispatcher經過resolveDispatcher()來獲取,這個函數一樣也很簡單,只是將ReactCurrentDispatcher.current的值賦給了dispatcher

// ReactHooks.js
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}
複製代碼

因此useState(xxx) 等價於 ReactCurrentDispatcher.current.useState(xxx)

看到這裏,咱們回顧一下第一章第三小節所講的React渲染器與setState,是否是發現有點似曾相識。

與updater是setState可以觸發更新的核心相似,ReactCurrentDispatcher.current.useStateuseState可以觸發更新的關鍵緣由,這個方法的實現並不在react包內。下面咱們就來分析一個具體更新的例子。

2. 示例分析

以全文開頭給出的代碼爲例。

咱們從Fiber調度的開始:ReactFiberBeginwork來談起

以前已經說過,React有能力區分不一樣的組件,因此它會給不一樣的組件類型打上不一樣的tag, 詳見shared/ReactWorkTags.js

因此在beginWork的函數中,就能夠根據workInProgess(就是個Fiber節點)上的tag值來走不一樣的方法來加載或者更新組件。

// ReactFiberBeginWork.js
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
  /** 省略與本文無關的部分 **/

  // 根據不一樣的組件類型走不一樣的方法
  switch (workInProgress.tag) {
    // 不肯定組件
    case IndeterminateComponent: {
      const elementType = workInProgress.elementType;
      // 加載初始組件
      return mountIndeterminateComponent(
        current,
        workInProgress,
        elementType,
        renderExpirationTime,
      );
    }
    // 函數組件
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      // 更新函數組件
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    // 類組件
    case ClassComponent {
      /** 細節略 **/
  	}
  }
複製代碼

下面咱們來找出useState發揮做用的地方。

2.1 第一次加載

mount過程執行mountIndeterminateComponent時,會執行到renderWithHooks這個函數

function mountIndeterminateComponent( _current, workInProgress, Component, renderExpirationTime, ) {
 
 /** 省略準備階段代碼 **/ 
  
  // value就是渲染出來的APP組件
  let value;

  value = renderWithHooks(
    null,
    workInProgress,
    Component,
    props,
    context,
    renderExpirationTime,
  );
  /** 省略無關代碼 **/ 
  }
  workInProgress.tag = FunctionComponent;
  reconcileChildren(null, workInProgress, value, renderExpirationTime);
  return workInProgress.child;
}
複製代碼

執行前: nextChildren = value

11.jpg

執行後: value= 組件的虛擬DOM表示

12.jpg

至於這個value是如何被渲染成真實的DOM節點,咱們並不關心,state值咱們已經經過renderWithHooks取到並渲染

2.2 更新

點擊一下按鈕:此時count從0變爲3

更新過程執行的是updateFunctionComponent函數,一樣會執行到renderWithHooks這個函數,咱們來看一下這個函數執行先後發生的變化:

執行前: nextChildren = undefined

13.jpg

**執行後:**nextChildren=更新後的組件的虛擬DOM表示

14.jpg

一樣的,至於這個nextChildren是如何被渲染成真實的DOM節點,咱們並不關心,最新的state值咱們已經經過renderWithHooks取到並渲染

因此,renderWithHooks函數就是處理各類hooks邏輯的核心部分

三. 核心步驟分析

ReactFiberHooks.js包含着各類關於Hooks邏輯的處理,本章中的代碼均來自該文件。

1. Hook對象

在以前的章節有介紹過,Fiber中的memorizedStated用來存儲state

在類組件中state是一整個對象,能夠和memoizedState一一對應。可是在Hooks中,React並不知道咱們調用了幾回useState因此React經過將一個Hook對象掛載在memorizedStated上來保存函數組件的state

Hook對象的結構以下:

// ReactFiberHooks.js
export type Hook = {
  memoizedState: any, 

  baseState: any,    
  baseUpdate: Update<any, any> | null,  
  queue: UpdateQueue<any, any> | null,  

  next: Hook | null, 
};
複製代碼

重點關注memoizedStatenext

  • memoizedState是用來記錄當前useState應該返回的結果的
  • queue:緩存隊列,存儲屢次更新行爲
  • next:指向下一次useState對應的Hook對象。

結合示例代碼來看:

import React, { useState } from 'react'
import './App.css'

export default function App() {
  
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Star');
  
  // 調用三次setCount便於查看更新隊列的狀況
  const countPlusThree = () => {
    setCount(count+1);
    setCount(count+2);
    setCount(count+3);
  }
  return (
    <div className='App'> <p>{name} Has Clicked <strong>{count}</strong> Times</p> <button onClick={countPlusThree}>Click *3</button> </div>
  )
}
複製代碼

第一次點擊按鈕觸發更新時,memoizedState的結構以下

Jietu20190419-100634@2x.png

只是符合以前對Hook對象結構的分析,只是queue中的結構貌似有點奇怪,咱們將在第三章第2節中進行分析。

2. renderWithHooks

renderWithHooks的運行過程以下:

// ReactFiberHooks.js
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
 
  // 若是current的值爲空,說明尚未hook對象被掛載
  // 而根據hook對象結構可知,current.memoizedState指向下一個current
  nextCurrentHook = current !== null ? current.memoizedState : null;

  // 用nextCurrentHook的值來區分mount和update,設置不一樣的dispatcher
  ReactCurrentDispatcher.current =
      nextCurrentHook === null
      // 初始化時
        ? HooksDispatcherOnMount
  		// 更新時
        : HooksDispatcherOnUpdate;
  
  // 此時已經有了新的dispatcher,在調用Component時就能夠拿到新的對象
  let children = Component(props, refOrContext);
  
  // 重置
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  // 更新memoizedState和updateQueue
  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  
   /** 省略與本文無關的部分代碼,便於理解 **/
}
複製代碼

2.1 初始化時

核心: 建立一個新的hook,初始化state, 並綁定觸發器

初始化階段ReactCurrentDispatcher.current 會指向HooksDispatcherOnMount 對象

// ReactFiberHooks.js

const HooksDispatcherOnMount: Dispatcher = {
/** 省略其它Hooks **/
  useState: mountState,
};

// 因此調用useState(0)返回的就是HooksDispatcherOnMount.useState(0),也就是mountState(0)
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    // 訪問Hook鏈表的下一個節點,獲取到新的Hook對象
  const hook = mountWorkInProgressHook();
//若是入參是function則會調用,可是不提供參數
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
// 進行state的初始化工做
  hook.memoizedState = hook.baseState = initialState;
// 進行queue的初始化工做
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    eagerReducer: basicStateReducer, // useState使用基礎reducer
    eagerState: (initialState: any),
  });
	// 返回觸發器
  const dispatch: Dispatch<BasicStateAction<S>,> 
    = (queue.dispatch = (dispatchAction.bind(
    	null,
    	//綁定當前fiber結點和queue
    	((currentlyRenderingFiber: any): Fiber),
    	queue,
  ));
  // 返回初始state和觸發器
  return [hook.memoizedState, dispatch];
}

// 對於useState觸發的update action來講(假設useState裏面都傳的變量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
複製代碼

重點講一下返回的這個更新函數 dispatchAction

function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) {

   /** 省略Fiber調度相關代碼 **/
  
  // 建立新的新的update, action就是咱們setCount裏面的值(count+1, count+2, count+3…)
    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
	  
    // 重點:構建query
    // queue.last是最近的一次更新,而後last.next開始是每一次的action
    const last = queue.last;
    if (last === null) {
      // 只有一個update, 本身指本身-造成環
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    /** 省略特殊狀況相關代碼 **/
    
    // 建立一個更新任務
    scheduleWork(fiber, expirationTime);

}
複製代碼

dispatchAction中維護了一份query的數據結構。

query是一個有環鏈表,規則:

  • query.last指向最近一次更新
  • last.next指向第一次更新
  • 後面就依次類推,最終倒數第二次更新指向last,造成一個環。

因此每次插入新update時,就須要將原來的first指向query.last.next。再將update指向query.next,最後將query.last指向update.

下面結合示例代碼來畫圖說明一下:

前面給出了第一次點擊按鈕更新時,memorizedState中的query值

Jietu20190419-130310@2x.png

其構建過程以下圖所示:

Jietu20190430-155220@2x.png

即保證query.last始終爲最新的action, 而query.last.next始終爲action: 1

2.2 更新時

核心:獲取該Hook對象中的 queue,內部存有本次更新的一系列數據,進行更新

更新階段 ReactCurrentDispatcher.current 會指向HooksDispatcherOnUpdate對象

// ReactFiberHooks.js

// 因此調用useState(0)返回的就是HooksDispatcherOnUpdate.useState(0),也就是updateReducer(basicStateReducer, 0)

const HooksDispatcherOnUpdate: Dispatcher = {
  /** 省略其它Hooks **/
   useState: updateState,
}

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

// 能夠看到updateReducer的過程與傳的initalState已經無關了,因此初始值只在第一次被使用

// 爲了方便閱讀,刪去了一些無關代碼
// 查看完整代碼:https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberHooks.js#L606
function updateReducer(reducer, initialArg, init) {
// 獲取初始化時的 hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 開始渲染更新
  if (numberOfReRenders > 0) {
    const dispatch = queue.dispatch;
    if (renderPhaseUpdates !== null) {
      // 獲取Hook對象上的 queue,內部存有本次更新的一系列數據
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        let newState = hook.memoizedState;
        let update = firstRenderPhaseUpdate;
        // 獲取更新後的state
        do {
          const action = update.action;
          // 此時的reducer是basicStateReducer,直接返回action的值
          newState = reducer(newState, action);
          update = update.next;
        } while (update !== null);
        // 對 更新hook.memoized 
        hook.memoizedState = newState;
        // 返回新的 state,及更新 hook 的 dispatch 方法
        return [newState, dispatch];
      }
    }
  }
  
// 對於useState觸發的update action來講(假設useState裏面都傳的變量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
複製代碼

2.3 總結

單個hooks的更新行爲全都掛在Hooks.queue下,因此可以管理好queue的核心就在於

  • 初始化queue - mountState
  • 維護queue - dispatchAction
  • 更新queue - updateReducer

結合示例代碼:

  • 當咱們第一次調用[count, setCount] = useState(0)時,建立一個queue
  • 每一次調用setCount(x),就dispach一個內容爲x的action(action的表現爲:將count設爲x),action存儲在queue中,之前面講述的有環鏈表規則來維護
  • 這些action最終在updateReducer中被調用,更新到memorizedState上,使咱們可以獲取到最新的state值。

四. 總結

1. 對官方文檔中Rules of Hooks的理解

官方文檔對於使用hooks有如下兩點要求:

Jietu20190418-094347@2x.png

2.1 爲何不能在循環/條件語句中執行

以useState爲例:

和類組件存儲state不一樣,React並不知道咱們調用了幾回useState,對hooks的存儲是按順序的(參見Hook結構),一個hook對象的next指向下一個hooks。因此當咱們創建示例代碼中的對應關係後,Hook的結構以下:

// hook1: const [count, setCount] = useState(0) — 拿到state1
{
  memorizedState: 0
  next : {
    // hook2: const [name, setName] = useState('Star') - 拿到state2
    memorizedState: 'Star'
    next : {
      null
    }
  }
}

// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState
複製代碼

因此若是把hook1放到一個if語句中,當這個沒有執行時,hook2拿到的state實際上是上一次hook1執行後的state(而不是上一次hook2執行後的)。這樣顯然會發生錯誤。

關於這塊內容若是想了解更多能夠看一下這篇文章

2.2 爲何只能在函數組件中使用hooks

只有函數組件的更新纔會觸發renderWithHooks函數,處理Hooks相關邏輯。

仍是以setState爲例,類組件和函數組件從新渲染的邏輯不一樣 :

類組件: 用setState觸發updater,從新執行組件中的render方法

函數組件: 用useState返回的setter函數來dispatch一個update action,觸發更新(dispatchAction最後的scheduleWork),用updateReducer處理更新邏輯,返回最新的state值(與Redux比較像)

2. useState總體運做流程總結

說了這麼多,最後再簡要總結下useState的執行流程~

初始化: 構建dispatcher函數和初始值

更新時:

  1. 調用dispatcher函數,按序插入update(其實就是一個action)
  2. 收集update,調度一次React的更新
  3. 在更新的過程當中將ReactCurrentDispatcher.current指向負責更新的Dispatcher
  4. 執行到函數組件App()時,useState會被從新執行,在resolve dispatcher的階段拿到了負責更新的dispatcher。
  5. useState會拿到Hook對象,Hook.query中存儲了更新隊列,依次進行更新後,便可拿到最新的state
  6. 函數組件App()執行後返回的nextChild中的count值已是最新的了。FiberNode中的memorizedState也被設置爲最新的state
  7. Fiber渲染出真實DOM。更新結束。

關於咱們:

咱們是螞蟻保險體驗技術團隊,來自螞蟻金服保險事業羣。咱們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。咱們支持了阿里集團幾乎全部的保險業務。18年咱們產出的相互寶轟動保險界,19年咱們更有多個重量級項目籌備動員中。現伴隨着事業羣的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入咱們~

咱們但願你是:技術上基礎紮實、某領域深刻(Node/互動營銷/數據可視化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。

若有興趣加入咱們,歡迎發送簡歷至郵箱:xingyan.hyx@antfin.com


本文做者:螞蟻保險-體驗技術組-星焰

掘金地址:STAR🌟

相關文章
相關標籤/搜索