深入 React 的 setState 機制

2021-03-03 前端精神時光屋

本文在我的 Github博客 https://github.com/Jacky-Summer/personal-blog 同步更新

前言

本篇寫的 setState(涉及源碼部分)是針對 React15 版本,即是沒有 Fiber 介入的;為了方便看和寫,所以選擇舊版本,Fiber 寫起來有點難,先留著將會寫。setState 在 React 15 的原理能理解,16 版本的也是大同小異。

雖然已經用 React Hooks 很久了,React15 的this.setState()形式都很少用了,但依然是站在回顧與總結的角度,看待 React 的變遷和發展,所以最近開始重新回顧以前一知半解的一些原理問題,慢慢沉澱技術。

setState 經典問題
setState(updater, [callback])

React 通過 this.setState() 來更新 state,當使用 this.setState()的時候 ,React 會調用 render 方法來重新渲染 UI。

setState 的幾種用法就不用我說了,來看看網上討論 setState 比較多的問題:

批量更新
import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>加1</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

點擊按鈕觸發事件,列印的都是 1,頁面顯示 count 的值為 2。

這就是常常說的 setState 批量更新,對同一個值進行多次 setState , setState 的批量更新策略會對其進行覆蓋,取最後一次的執行結果。所以每次 setState 之後立即列印值都是初始值 1,而最後頁面顯示的值則為最後一次的執行結果,也就是 2。

setTimeout
import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      })
      console.log(this.state.count) // 3
    })
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>加1</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

點擊按鈕觸發事件,發現 setTimeout 裡面的 count 值列印值為 3,頁面顯示 count 的值為 3。setTimeout 裡面 setState 之後能馬上能到最新值。

在 setTimeout 裡面,setState 是同步的;經過前面兩次的 setState 批量更新,count 值已經更新為 2。在 setTimeout 裡面的首先拿到新的 count 值 2,再一次 setState,然後能實時拿到 count 的值為 3。

DOM 原生事件
import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  componentDidMount() {
    document.getElementById('btn').addEventListener('click', this.handleClick)
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 2

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 3

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 4
  }

  render() {
    return (
      <>
        <button id='btn'>觸發原生事件</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

點擊按鈕,會發現每次 setState 列印出來的值都是實時拿到的,不會進行批量更新。

在 DOM 原生事件裡面,setState 也是同步的。

setState 同步異步問題

這裡討論的同步和異步並不是指 setState 是否異步執行,使用了什麼異步代碼,而是指調用 setState 之後 this.state 能否立即更新。

React 中的事件都是合成事件,都是由 React 內部封裝好的。React 本身執行的過程和代碼都是同步的,只是合成事件和鉤子函數的調用順序在更新之前,導致在合成事件和鉤子函數中沒法立馬拿到更新後的值,就是我們所說的"異步"了。

由上面也可以得知 setState 在原生事件和 setTimeout 中都是同步的。

setState 源碼層面

源碼選擇的 React 版本為15.6.2

setState 函數

源碼裡面,setState 函數的代碼

React 組件繼承自React.Component,而 setState 是React.Component的方法

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState)
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState')
  }
}

可以看到它直接調用了 this.updater.enqueueSetState 這個方法。

enqueueSetState
enqueueSetState: function(publicInstance, partialState) {
  // 拿到對應的組件實例
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',
  );
  // queue 對應一個組件實例的 state 數組
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState); // 將 partialState 放入待更新 state 隊列
  // 處理當前的組件實例
  enqueueUpdate(internalInstance);
}

_pendingStateQueue表示待更新隊列

enqueueSetState 做了兩件事:

用 enqueueUpdate 來處理將要更新的實例對象。

接下來看看 enqueueUpdate 做了什麼:

function enqueueUpdate(component) {
  ensureInjected()
  // isBatchingUpdates 標識著當前是否處於批量更新過程
  if (!batchingStrategy.isBatchingUpdates) {
    // 若當前沒有處於批量創建/更新組件的階段,則立即更新組件
    batchingStrategy.batchedUpdates(enqueueUpdate, component)
    return
  }
  // 需要批量更新,則先把組件塞入 dirtyComponents 隊列
  dirtyComponents.push(component)
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1
  }
}

batchingStrategy 表示批量更新策略,isBatchingUpdates表示當前是否處於批量更新過程,默認是 false。

enqueueUpdate做的事情:

判斷組件是否處於批量更新模式,如果是,即isBatchingUpdates為 true 時,不進行 state 的更新操作,而是將需要更新的組件添加到dirtyComponents數組中;

如果不是處於批量更新模式,則對所有隊列中的更新執行batchedUpdates方法

當中 batchingStrategy該對象的isBatchingUpdates屬性直接決定了是馬上要走更新流程,還是應該進入隊列等待;所以大概可以得知batchingStrategy用於管控批量更新的對象。

來看看它的源碼:

/**
 *  batchingStrategy源碼
 **/
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, // 初始值為 false 表示當前並未進行任何批量更新操作

  // 發起更新動作的方法
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates

    ReactDefaultBatchingStrategy.isBatchingUpdates = true

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e)
    } else {
      // 啟動事務,將 callback 放進事務裡執行
      return transaction.perform(callback, null, a, b, c, d, e)
    }
  },
}

「每當 React 調用 batchedUpdate 去執行更新動作時,會先把isBatchingUpdates置為 true,表明正處於批量更新過程中。」

看完批量更新整體的管理機制,發現還有一個操作是transaction.perform,這就引出 React 中的 Transaction(事務)機制。

Transaction(事務)機制

Transaction 是創建一個黑盒,該黑盒能夠封裝任何的方法。因此,那些需要在函數運行前、後運行的方法可以通過此方法封裝(即使函數運行中有異常拋出,這些固定的方法仍可運行)。

在 React 中源碼有關於 Transaction 的注釋如下:

 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +--|---|----+
 *                    |                 v        |              |
 *                    |      ++   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  ++   v    |         |
 *                    |   |          +---+  |         |
 *                    |   |     +----|   wrapper2  |---+   |
 *                    |   |     |    +---+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +----+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +-->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +----+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-+
 * </pre>

根據以上注釋,可以看出:一個 Transaction 就是將需要執行的 method 使用 wrapper(一組 initialize 及 close 方法稱為一個 wrapper) 封裝起來,再通過 Transaction 提供的 perform 方法執行。

在 perform 之前,先執行所有 wrapper 中的 initialize 方法;perform 完成之後(即 method 執行後)再執行所有的 close 方法,而且 Transaction 支持多個 wrapper 疊加。這就是 React 中的事務機制。

batchingStrategy 批量更新策略

再看回batchingStrategy批量更新策略,ReactDefaultBatchingStrategy 其實就是一個批量更新策略事務,它的 wrapper 有兩個:FLUSH_BATCHED_UPDATES 和 RESET_BATCHED_UPDATES。

isBatchingUpdates在 close 方法被復位為 false,如下代碼:

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false
  },
}
//  flushBatchedUpdates 將所有的臨時 state 合併並計算出最新的 props 及 state
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]

「React 鉤子函數」

都說 React 鉤子函數也是異步更新,則必須一開始 isBatchingUpdates 為 ture,但默認 isBatchingUpdates 為 false,它是在哪裡被設置為 true 的呢?來看下面代碼:

// ReactMount.js
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
  // 實例化組件
  var componentInstance = instantiateReactComponent(nextElement);
  // 調用 batchedUpdates 方法
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  );
}

這段代碼是在首次渲染組件時會執行的一個方法,可以看到它內部調用了一次 batchedUpdates 方法(將 isBatchingUpdates 設為 true),這是因為在組件的渲染過程中,會按照順序調用各個生命周期(鉤子)函數。如果在函數裡面調用 setState,則看下列代碼:

if (!batchingStrategy.isBatchingUpdates) {
  // 立即更新組件
  batchingStrategy.batchedUpdates(enqueueUpdate, component)
  return
}
// 批量更新,則先把組件塞入 dirtyComponents 隊列
dirtyComponents.push(component)

則所有的更新都能夠進入 dirtyComponents 裡去,即 setState 走的異步更新

「React 合成事件」

當我們在組件上綁定了事件之後,事件中也有可能會觸發 setState。為了確保每一次 setState 都有效,React 同樣會在此處手動開啟批量更新。看下面代碼:

// ReactEventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
  try {
    // 處理事件:batchedUpdates會將 isBatchingUpdates設為true
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}

isBatchingUpdates 這個變量,在 React 的生命周期函數以及合成事件執行前,已經被 React 改為 true,這時我們所做的 setState 操作自然不會立即生效。當函數執行完畢後,事務的 close 方法會再把 isBatchingUpdates 改為 false。

就像最上面的例子,整個過程模擬大概是:

handleClick = () => {
  // isBatchingUpdates = true
  this.setState({
    count: this.state.count + 1,
  })
  console.log(this.state.count) // 1

  this.setState({
    count: this.state.count + 1,
  })
  console.log(this.state.count) // 1

  this.setState({
    count: this.state.count + 1,
  })
  console.log(this.state.count) // 1
  // isBatchingUpdates = false
}

而如果有 setTimeout 介入後

handleClick = () => {
  // isBatchingUpdates = true
  this.setState({
    count: this.state.count + 1,
  })
  console.log(this.state.count) // 1

  this.setState({
    count: this.state.count + 1,
  })
  console.log(this.state.count) // 1

  setTimeout(() => {
    // setTimeout異步執行,此時 isBatchingUpdates 已經被重置為 false
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 3
  })
  // isBatchingUpdates = false
}

isBatchingUpdates是在同步代碼中變化的,而 setTimeout 的邏輯是異步執行的。當 this.setState 調用真正發生的時候,isBatchingUpdates 早已經被重置為 false,這就使得 setTimeout 裡面的 setState 具備了立刻發起同步更新的能力。

batchedUpdates 方法

看到這裡大概就可以了解 setState 的同步異步機制了,接下來讓我們進一步體會,可以把 React 的batchedUpdates拿來試試,在該版本中此方法名稱被置為unstable_batchedUpdates即不穩定的方法。

import React, { Component } from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1

    setTimeout(() => {
      batchedUpdates(() => {
        this.setState({
          count: this.state.count + 1,
        })
        console.log(this.state.count) // 2
      })
    })
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>加1</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

如果調用batchedUpdates方法,則 isBatchingUpdates變量會被設置為 true,由上述得為 true 走的是批量更新策略,則 setTimeout 裡面的方法也變成異步更新了,所以最終列印值為 2,與本文第一道題結果一樣。

總結

setState 同步異步的表現會因調用場景的不同而不同:在 React 鉤子函數及合成事件中,它表現為異步;而在 setTimeout/setInterval 函數,DOM 原生事件中,它都表現為同步。這是由 React 事務機制和批量更新機制的工作方式來決定的。

在 React16 中,由於引入了 Fiber 機制,源碼多少有點不同,但大同小異,之後我也會寫 React16 原理的文章,敬請關注!


歡迎關注我掘金帳號和Github技術博客:

掘金:https://juejin.im/user/1257497033714477Github:https://github.com/Jacky-Summer覺得對你有幫助或有啟發的話歡迎 star,你的鼓勵是我持續創作的動力~如需在微信公眾號平臺轉載請聯繫作者授權同意,其它途徑轉載請在文章開頭註明作者和文章出處。❞

相關焦點

  • react中關於hook介紹及其使用
    >在上面的這個例子中,我們設置變量方式採用的就是const [count, setCount] = useState(0)這種方式,其中的0就是給count賦初值為0,如果想要給count賦值為一個空對象,那麼只需要const [count, setCount] = useState({}),這樣的方式就行了,那麼這樣你在用count時
  • React系列(一) -邂逅React開發
    來存儲組件內的狀態3.當應用的狀態發生改變時,通過setState來修改狀態,狀態發生變化時,UI會自動發生更新1.3.其實呢,這三個庫是各司其職的,目的就是讓每一個庫只單純做自己的事情:在React的0.14版本之前是沒有react-dom這個概念的,所有功能都包含在react裡。為什麼要進行拆分呢?原因就是react-native。react包中包含了react和react-native所共同擁有的核心代碼。
  • 精通react/vue組件設計之配合React Portals實現一個(Drawer)組件
    通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.
  • React 狀態管理探索
    科普 Redux 相關知識什麼是 Immutable,為什麼要用它科普所涉及的 Event Sourcing、CQRS、Flux相關知識Redux 這個磨人精在項目中的問題為什麼我們要用 redux-saga、redux-thunk 等寫慣了 hooks,能不能用 hooks 寫狀態管理,引入 hox阿寬手把手帶你看
  • 尚矽谷前端視頻周 | React新版教程發布,聽著上頭,學到痴迷!
    001.React-react簡介002.React-hello_react案例003.React-虛擬DOM的兩種創建方式004.React-虛擬DOM與真實DOM005.React-jsx語法規則006.React-jsx小練習007.React-組件與模塊008.React-
  • React SSR 同構入門與原理
    本文將分以下兩部分去講述:同構思路分析,讓你對同構有一個概念上的了解;手寫同構框架,深入理解同構原理。執行命令: create-react-app react-csr 創建一個 React SPA 單頁面應用項目 。執行命令: npm run start 啟動項目。
  • 傻傻搞不懂React的Mixin、HOC、Render Props
    之前,我們來捋捋react狀態邏輯復用相關知識點,這會幫助你理解hooksReact 裡,組件是代碼復用的基本單元,基於組合的組件復用機制相當優雅。幾個月之後,你可能希望將該state移動到父組件,以便與其兄弟組件共享。你會記得更新這個mixin來讀取props而不是state嗎?如果此時,其它組件也在使用這個mixin呢?
  • TypeScript 備忘錄:如何在 React 中完美運用?
    : typeof initialState, action: ACTIONTYPE) {  switch (action.type) {    case "increment":      return { count: state.count + action.payload };    case "decrement":      return { count
  • react
    react 3d模型 There is something in me that is amazed but beautiful 3D interfaces..react-3d-carousel .prev:before { content: url("chevron_left_white.png");}.react-3d-carousel .next:before { content: url
  • 如何在 React 中完美運用 TypeScript ?
    React.FC<AppProps> = ({ message, children }) => { return ( <> {children} <div>{message}</div> </> )};複製代碼Hooks@types/react
  • 精通react/vue組件設計之實現一個輕量級可擴展的模態框組件
    選手來說,如果沒用typescript,建議大家都用PropTypes, 它是react內置的類型檢測工具,我們可以直接在項目中導入. vue有自帶的屬性檢測方式,這裡就不一一介紹了.先來看看實現效果吧:編輯搜圖這裡筆者使用了react hooks的useState這個API,來設置彈窗可見性的state,modal默認不可見。具體邏輯如下:let [isHidden, setHidden] = useState(!
  • React學習系列二,堅持!
    並且通過這一層單獨抽象的邏輯讓 React 有了無限的可能,就比如 react native 的實現。組件要注意到,在 React 當中元素和組件是兩個不同的概念,我們需要明確的是,組件是構建在元素的基礎之上的。React 官方對組件的定義,是指在 UI 界面中,可以被獨立劃分的、可復用的、獨立的模塊。
  • 基於jsoneditor二次封裝一個可實時預覽的json編輯器組件react版
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言做為一名前端開發人員,掌握vue/react/angular等框架已經是必不可少的技能了,我們都知道,vue或react等MVVM框架提倡組件化開發
  • React的7種代碼異味「譯」
    props 複製為 state如何更好地將 props 作為 state 的初始值。在其他語言中,枚舉是一種定義變量的方式,該變量只允許設置為預定義的常量值集合,雖然在JavaScript 中不存在枚舉,但我們可以使用字符串作為枚舉:function Component() { const [state, setState] = useState('idle') const fetchSomething = (
  • 如何設計 React 代碼結構?
    import React, { useState } from 'react';import { saveMyInformationToSanta } from '../..] = useState(''); const [hasFireplace, setHasFireplace] = useState(null); const [naughtyOrNice, setNaughtyOrNice] = useState(null); const [letterToSanta, setLetterToSanta
  • ES6 的 Set 與 Map深入理解
    let set = new Set()set.add('one')set.add('two')set.size // 2與 ES5 中對象模擬實現不同的是,Set 集合會對添加進來的元素調用set.add(5)set.add('5')let key1 = {}let key2 = {}set.add(key1)set.add(key2)set.size // 4
  • Full-state recognition function of ticket gate shows greater...
    Recently, a station staff member showed us the full-state recognition function of ticket gate set on the official APP of Nanning Rail Transit at Yuxianglu Station
  • The Prostate:咖啡中的特殊化合物有望抑制前列腺癌
    2019年3月20日 訊 /生物谷BIOON/ --日前,一項發表在國際雜誌The Prostate上的研究報告中,來自日本金澤大學等機構的科學家們通過研究首次發現,咖啡中的特殊化合物或能有效抑制前列腺癌的生長,這是研究人員進行的一項試點研究,目前僅在細胞培養物和小鼠模型機體中的耐藥癌細胞中進行了相關研究,並未在人類機體中進行臨床試驗。
  • 《精通react/vue組件設計》之實現一個健壯的警告提示(Alert)組件
    選手來說,如果沒用typescript,建議大家都用PropTypes, 它是react內置的類型檢測工具,我們可以直接在項目中導入. vue有自帶的屬性檢測方式,這裡就不一一介紹了.基於react實現一個Alert組件2.1.