React16——看看setState過程當中fiber幹了什麼事情

目的

經過觀察調用棧和其餘博客的介紹親身體驗下setState過程當中fiber幹了什麼事情node

一、create-react-app建立一個demo

下圖是一個典型的create-react-app建立的項目,其中Text.js是我新增的子組件,在App.js中引用到。 react

clipboard.png

二、修改App.js文件和新增Text.js文件

App.js算法

import React, { Component } from 'react';
import { Text } from './Text'

class App extends Component {
    constructor(props) {
        super(props)
        this.state = {
            tab: 'Welcome to React'
        }
    }
    updateState = () => {
        this.setState(() => ({tab: 'Bye Bye'}))
    }
  render() {
    return (
      <div className="App">
          <Text tab={this.state.tab} updateState={this.updateState} />
      </div>
    );
  }
}
export default App;

複製代碼

Text.jssegmentfault

import React from 'react'

export const Text = (props) => {
    return (
        <span onClick={() => props.updateState()}>{props.tab}</span>
    )
}
複製代碼

三、執行setState

state.tab的初始值是'Welcome to React',在setState中,傳入了一個箭頭函數,去更新state.tab的值爲'Bye Bye',瀏覽器

//partialState爲() => ({tab: ''Bye Bye}),callback沒有傳入
Component.prototype.setState = function (partialState, callback) {
  //this爲當前組件的實例
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼

接着,執行了updater上的enqueueSetState方法,每個實例都會有一個updater(更新器),updater的做用在下面介紹,在當前App組件實例中,_reactInternalFiber是當前組件的fiber,而_reactInternalInstance是在react15使用的對象。bash

clipboard.png

四、updater更新器

updater有3個方法,只用關心enqueueSetState數據結構

var updater = {
    isMounted: isMounted,
    /*
    * instance: 上一步傳入的App組件實例,
    * partialState:須要執行更新的箭頭函數,
    * callback:undefined
    */
    enqueueSetState: function (instance, partialState, callback) {
      //獲取到當前實例上的fiber
      var fiber = get(instance);
      //計算當前fiber的到期時間(優先級)
      var expirationTime = computeExpirationForFiber(fiber);
      //一次更新須要的配置參數
      var update = {
        expirationTime: expirationTime, //優先級
        partialState: partialState, //更新的state,一般是函數而不推薦對象寫法
        callback: callback, //更新以後執行的回調函數
        isReplace: false, //
        isForced: false, //是否強制更新
        capturedValue: null, //捕獲的值
        next: null, //
      };
      //將update上須要更新的信息添加到fiber中
      insertUpdateIntoFiber(fiber, update);
      //調度器調度fiber任務
      scheduleWork(fiber, expirationTime);
    },
    //替換更新state,不關注
    enqueueReplaceState: function (instance, state, callback) {},
    //執行強制更新state,不關注
    enqueueForceUpdate: function (instance, callback) {}
  };
複製代碼

下面按步驟詳細看看這個函數內部的執行流程。app

獲取fiber:key === instance,fiber很重要,記錄了不少有用的信息,好比當前組件實例的各類屬性和狀態、優先級、標識等。異步

function get(key) {
  return key._reactInternalFiber;
}
複製代碼

也許你會很好奇fiber長什麼樣,圖上展現的是組件實例上注入的fiber數據結構。 ide

clipboard.png

計算到期時間:也就是計算當前fiber任務的優先級,從代碼上看須要判斷的條件比較多,既能夠是異步更新,也能夠是同步更新。在當前測試中,進入的是同步更新的流程。而同步對應的優先級就是1,因此expirationTime = 1。

//用來計算fiber的到期時間,到期時間用來表示任務的優先級。
 function computeExpirationForFiber(fiber) {
    var expirationTime = void 0;
    if (expirationContext !== NoWork) {
      // 
      expirationTime = expirationContext;
    } else if (isWorking) {
      if (isCommitting) {
        //同步模式,當即處理任務,默認是1
        expirationTime = Sync;
      } else {
        //渲染階段的更新應該與正在渲染的工做同時過時。
        expirationTime = nextRenderExpirationTime;
      }
    } else {
      //沒有到期時間的狀況下,建立一個到期時間
      if (fiber.mode & AsyncMode) {
        if (isBatchingInteractiveUpdates) {
          // 這是一個交互式更新
          var currentTime = recalculateCurrentTime();
          expirationTime = computeInteractiveExpiration(currentTime);
        } else {
          // 這是一個異步更新
          var _currentTime = recalculateCurrentTime();
          expirationTime = computeAsyncExpiration(_currentTime);
        }
      } else {
        // 這是一個同步更新
        expirationTime = Sync;
      }
    }
    if (isBatchingInteractiveUpdates) {
      //這是一個交互式的更新。跟蹤最低等待交互過時時間。這容許咱們在須要時同步刷新全部交互更新。
      if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
        lowestPendingInteractiveExpirationTime = expirationTime;
      }
    }
    return expirationTime;
  }
複製代碼

將update上須要更新的信息添加到fiber中:這個函數的做用就是把咱們在上面經過計算以後獲得的update更新到fiber上面,實際操做是對象的賦值,跟合併是一個意思。

function insertUpdateIntoFiber(fiber, update) {
  //確保更新隊列存在,不存在則建立
  ensureUpdateQueues(fiber);
  //上一步已經將q1和q2隊列進行了處理,定義2個局部變量queue1和queue2來保存隊列信息。
  var queue1 = q1;
  var queue2 = q2;
  // 若是隻有一個隊列,請將更新添加到該隊列並退出。
  if (queue2 === null) {
    insertUpdateIntoQueue(queue1, update);
    return;
  }

  // 若是任一隊列爲空,咱們須要添加到兩個隊列中。
  if (queue1.last === null || queue2.last === null) {
    //將update的值更新到隊列1和隊列2上,而後退出該函數
    insertUpdateIntoQueue(queue1, update);
    insertUpdateIntoQueue(queue2, update);
    return;
  }

  // 若是兩個列表都不爲空,則因爲結構共享,兩個列表的最後更新都是相同的。因此,咱們應該只追加到其中一個列表。
  insertUpdateIntoQueue(queue1, update);
  // 可是咱們仍然須要更新queue2的`last`指針。
  queue2.last = update;
}
複製代碼

初始化的時候,fiber中的updateQueue是null,這時候,就要建立createUpdateQueue一個更新隊列。alternate本質上也是fiber,它記錄的是上一次setState操做的fiber,同時alternate又是fiber的一個屬性。

clipboard.png

ensureUpdateQueues的做用是確保更新隊列不爲null。

var q1 = void 0;
var q2 = void 0;
function ensureUpdateQueues(fiber) {
  q1 = q2 = null;
  // 咱們將至少有一個和最多兩個不一樣的更新隊列。
  //alternate是fiber上的一個屬性,初始化是null,執行了setState的過程當中,會將當前的FiberNode保存到alternate上,下次setState時,就能讀取到,能夠用來作狀態回滾。
  var alternateFiber = fiber.alternate;
  var queue1 = fiber.updateQueue;
  if (queue1 === null) {
    // 沒有隊列,就建立隊列
    queue1 = fiber.updateQueue = createUpdateQueue(null);
  }

  var queue2 = void 0;
  if (alternateFiber !== null) {
    queue2 = alternateFiber.updateQueue;
    if (queue2 === null) {
      queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
    }
  } else {
    queue2 = null;
  }
  queue2 = queue2 !== queue1 ? queue2 : null;

  // 使用模塊變量
  q1 = queue1;
  q2 = queue2;
}
複製代碼
//將update的值更新到queue中。
function insertUpdateIntoQueue(queue, update) {
  // 將更新附加到列表的末尾。
  if (queue.last === null) {
    // 隊列是空的
    queue.first = queue.last = update;
  } else {
    queue.last.next = update;
    queue.last = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > update.expirationTime) {
    queue.expirationTime = update.expirationTime;
  }
}
複製代碼

scheduleWork進行調度:上面的幾個步驟記得作了什麼嗎?拿到組件實例上的fiber,而後經過計算獲得優先級和其餘須要更新的fiber屬性,最後更新到fiber上,同時建立了更新隊列。可是react還沒開始幹活是否是,更新隊列有了,fibre也有了,react的大腦該對fiber進行調度了。

調度的邏輯很複雜,由於影響因素太多了,我沒法一一列舉,只能根據當前的調用棧識別用到的部分。

//傳入2個參數,fiber和優先級,內部又嵌套了一個函數scheduleWorkImpl,這個函數纔是邏輯部分。
function scheduleWork(fiber, expirationTime) {
    return scheduleWorkImpl(fiber, expirationTime, false);
  }
複製代碼

請注意當前傳入的fiber是合併了update屬性以後的fiber。

scheduleWorkImpl有寫讓人迷惑,司徒大佬的文章也沒有解釋清楚這個函數,一步步來看的話,recordScheduleUpdate的做用是先判斷當前有沒有正在提交更新或者已經在更新中的任務,應該是等updater執行完後,要用到的一些條件預設。

而後將node = fiber,別糾結爲何是直接相等,接着執行循環,當前node也就是fiber不爲空,根據條件,要在循環過程當中對node清空,清空以後退出函數。那麼,這個清空的過程作了什麼事情呢?

先是判斷node裏面的到期時間是否是等於NoWork,NoWork表示的是0,它表示的是當前沒有在調度中的fiber,而後判斷node的到期時間是否是大於傳入的到期時間,若是知足條件,就將node的到期時間更新爲新傳入的到期時間。

而後判斷alternate不爲空的狀況下,alternate在沒有執行過setState,一般是初始化的時候是空狀態,當執行過一次setState以後,就會將舊的FiberNode賦值給alternate,下面的函數中,若是alternate不爲空,而且expirationTime和上一個if的判斷一致的狀況下,就更新alternate中的expirationTime。

上2個條件是更新到期時間的,第3個條件是判斷return是否是等於null,return的含義在徹底理解fiber一文中有說到,表示當前的fiber任務向誰提交。在本demo中,當前是第一次執行,全部它的return爲null。

function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
    //記錄調度的狀態
    recordScheduleUpdate();
    var node = fiber;
    while (node !== null) {
      // 將父路徑移到根目錄並更新每一個節點的到期時間。
      if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
        node.expirationTime = expirationTime;
      }
      if (node.alternate !== null) {
        if (node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime) {
          node.alternate.expirationTime = expirationTime;
        }
      }
      if (node['return'] === null) {
        if (node.tag === HostRoot) {
          var root = node.stateNode;
          if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime) {
            // 是一箇中斷。 (用於性能跟蹤。)
            interruptedBy = fiber;
            resetStack();
          }
          if (
          // 若是咱們處於渲染階段,咱們不須要爲此更新安排此根目錄,由於咱們將在退出以前執行此操做。
          !isWorking || isCommitting ||
          // ......除非這是與咱們渲染的根不一樣的root。
          nextRoot !== root) {
            // 將root添加到root調度
            requestWork(root, expirationTime);
          }
        } else {
          return;
        }
      }
      node = node['return'];
    }
  }
複製代碼
//顧名思義,記錄調度的狀態
function recordScheduleUpdate() {
  if (enableUserTimingAPI) {
    if (isCommitting) {
      //當前是否有正在提交的調度任務,確定沒有啦。
      hasScheduledUpdateInCurrentCommit = true;
    }
    //currentPhase表示當前執行到哪一個生命週期了。
    if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {
      //當前是否有調度到某個生命週期階段的任務
      hasScheduledUpdateInCurrentPhase = true;
    }
  }
}
複製代碼

到這裏爲止,updater的函數執行完了,咱們總結一下它到底作了什麼事情。一共有4點:

  • 找到實例上的fiber
  • 計算獲得當前fiber的優先級
  • 將要更新的fiber推送到更新隊列
  • 根據fiber樹上的優先級肯定更新工做,從當前fiber的return爲起點,開始遞歸,直至到達根節點,根節點的return=null。

後續

根據調用棧,咱們看到了setState函數的執行過程,可是此時並無在瀏覽器上看到更新,由於具體的調度工做仍是要依靠react的核心算法去執行,updater只是將fiber更新到隊列中,和肯定了更新的優先級。

後面要經歷react的事件合成,Diff算法,虛擬DOM解析,生命週期執行等過程。很是多的代碼,要是一一解釋,能夠寫一本書出來了。

之後要是有時間,能夠將後半部分關於setState裏的() => ({tab: 'Bye Bye'})是如何更新的說說。 一切都要從createWorkInProgress(current, pendingProps, expirationTime)開始提及。

不管是國內哪位大神的博客,只要是介紹fiber的,萬變不離其宗,看國外的這篇文章:fiber詳解

Bye Bye,各位。

臥槽,這是什麼

clipboard.png
相關文章
相關標籤/搜索