你真的瞭解 setState 嗎?

這是我參與 8 月更文挑戰的第 3 天,活動詳情查看: 8月更文挑戰javascript

本文全部示例html

setState 算是 React 裏被使用的最高頻的 api,但你真的瞭解 setState 嗎?好比下面這段代碼,你能清楚的知道輸出什麼嗎?java

import { Component } from 'react'
export class stateDemo extends Component {
    state = {
        count: 0
    }
    componentDidMount() {
        this.setState({ count: this.state.count + 1 })
        console.log(this.state.count)
        this.setState({ count: this.state.count + 1 })
        console.log(this.state.count)
        setTimeout(() => {
            this.setState({ count: this.state.count + 1 })
            console.log(this.state.count)
            this.setState({ count: this.state.count + 1 })
            console.log(this.state.count)
        }, 0)
    }

    render() {
        return null
    }
}

export default stateDemo
複製代碼

要完全弄懂這道題,就不得不聊 setState 的異步更新,另外輸出結果也要看當前處於哪一種模式下。react

咱們先從 setState 的用法提及,以便全面掌握git

一、爲何須要 setState

雖然咱們一直在用 setState,可有沒想過爲何 React 裏會有該 apigithub

React 是經過管理狀態來實現對組件的管理,即 UI = f(state) f 就是咱們的代碼,最主要的就是 this.setState ,調用該函數後 React 會使用更新的 state 從新渲染此組件及其子組件,即達到了 UI 層的變動。web

二、什麼是 setState

setState 是 React 官方提供的更新 state 的方法,經過調用 setState,React 會使用最新的 state 值,並調用 render 方法將變化展示到視圖。面試

React v16.3 版本以前,調用 setState 方法會依次觸發如下生命週期函數api

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

那麼 state 在哪一個生命週期裏會更新爲最新的值?數組

import React, { Component } from 'react'
export default class stateDemo2 extends Component {
    state = {
        count: 0
    }
    shouldComponentUpdate() {
        console.info('shouldComponentUpdate', this.state.count) // shouldComponentUpdate 0
        return true
    }
    componentWillUpdate() {
        console.info('componentWillUpdate', this.state.count) // componentWillUpdate 0
    }
    increase = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        console.info('render', this.state.count) // render 1
        return (
            <div> <p>{this.state.count}</p> <button onClick={this.increase}>累加</button> </div>
        )
    }
    componentDidUpdate() {
        console.info('componentDidUpdate', this.state.count) // componentDidUpdate 1
    }
}
複製代碼

能夠看到,直到 render 執行時,state 的值才變動爲最新的值,在此以前,state 一直保持爲更新前的狀態。

見示例庫裏的 stateDemo2.js

React v16.3 版本以後,調用 setState 方法會依次觸發如下生命週期函數

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

確切的說,應該是 v16.4 版本以後,v16.3 版本 setState 並不會觸發 getDerivedStateFromProps 函數

那麼 state 在哪一個生命週期裏會更新爲最新的值?

import React, { Component } from 'react'

export default class stateDemo3 extends Component {
    state = {
        count: 0
    }
    static getDerivedStateFromProps(props, state) {
        console.info('getDerivedStateFromProps', state.count) // getDerivedStateFromProps 1
        return { ...state }
    }
    shouldComponentUpdate() {
        console.info('shouldComponentUpdate', this.state.count) // shouldComponentUpdate 0
        return true
    }
    increase = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        console.info('render', this.state.count) //render 1
        return (
            <div> <p>{this.state.count}</p> <button onClick={this.increase}>累加</button> </div>
        )
    }
    getSnapshotBeforeUpdate() {
        console.info('getSnapshotBeforeUpdate', this.state.count) //getSnapshotBeforeUpdate 1
        return null
    }
    componentDidUpdate() {
        console.info('componentDidUpdate', this.state.count) //componentDidUpdate 1
    }
}

複製代碼

能夠看到新增的兩個生命週期函數 getDerivedStateFromPropsgetSnapshotBeforeUpdate 獲取到的 state 都是新值

見示例庫裏的 stateDemo3.js

三、setState 用法

3.一、setState(stateChange[, callback])

第一個參數是一個對象,會將傳入的對象淺層合併到 ;第二個參數是個可選的回調函數

例如,調整購物車商品數:

this.setState({quantity: 2})
複製代碼

在回調函數參數裏,能夠獲取到最新的 state 值,但推薦使用 componentDidUpdate

3.二、setState(updater, [callback])

第一個參數是個函數,(state, props) => stateChange,第二個參數同上是個可選的回調函數

例如:

this.setState((state, props) => {
  return {counter: state.counter + props.step};
});
複製代碼

updater 函數中接收的 state 和 props 都保證爲最新。updater 的返回值會與 state 進行淺合併。

四、state 的不可變性

咱們要嚴格遵行 state 是不可變的原則,即不能夠直接修改 state 變量,例如底下的作法就是不可取的

this.state.count ++
this.state.count ++
this.setState({})
複製代碼

這樣是實現了同步更改 state 的目的,但違背了 state 是不可變的原則

4.一、基本數據類型

this.setState({
  count: 1,
  name: 'zhangsan',
  flag: true
})
複製代碼

4.二、對象類型

使用 ES6 的 Object.assign 或解構賦值

this.setState({
    // person: Object.assign({}, this.state.person, { name: 'lisi' })
    person:{...this.state.person,age:22}
})
複製代碼

見示例庫裏的 stateDemo4.js

4.三、數組類型

  • 追加選項: 使用 concat 或者解構賦值
this.setState((prevState) => {
    return {
        // hobbys: prevState.hobbys.concat('writing')
        hobbys:[...prevState.hobbys,'writing']
    }
})
複製代碼
  • 截取選項: 使用 slice
this.setState({
    hobbys: this.state.hobbys.slice(0, 2)
})
複製代碼
  • 插入選項: 使用 slice 克隆一份,而後用 splice 插入選項
this.setState((prevState) => {
    let currentState = prevState.hobbys.slice() // 先克隆一份
    currentState.splice(1, 0, 'basketball')
    return {
        hobbys: currentState
    }
})
複製代碼
  • 過濾選項: 使用 filter
this.setState({
    hobbys: this.state.hobbys.filter((item) => item.length < 5)
})
複製代碼

注意,不能直接使用 push pop splice shift unshift 等,由於這些方法都是在原數組的基礎上修改,這樣違反不可變值

見示例庫裏的 stateDemo4.js

五、setState 究竟是異步仍是同步?

Promise.then()setTimeout 是異步執行.,從 js 執行來講,setState 確定是同步執行。

這裏討論的同步和異步並非指 setState 是否異步執行,而是指調用 setState 以後 this.state 可否當即更新。

先給出答案:

  • legacy 模式中,即經過 ReactDOM.render(<App />, rootNode) 建立的,在合成事件和生命週期函數裏是異步的,在原生事件和 setTimeoutpromise 等異步函數是同步的
  • blocking 模式中,即經過 ReactDOM.createBlockingRoot(rootNode).render(<App />) 建立的,任何場景下 setState 都是異步的
  • concurrent 模式中,即經過 ReactDOM.createRoot(rootNode).render(<App />) 建立的,任何場景下 setState 都是異步的

模式的說明詳看官網 但因爲後兩種模式目前處於實驗階段,因此咱們先重點分析下 legacy 模式,後面源碼分析時,會說明下爲何其餘兩個模式都是異步的。

5.1 合成事件和生命週期函數裏是異步的

import React, { Component } from 'react'

export default class stateDemo5 extends Component {
  state = {
    count:0
  }

  componentDidMount() {
    this.setState({
      count:this.state.count+1
    })
    console.info("didMount count:",this.state.count) // didMount count: 0
  }
  handleChangeCount = () => {
    this.setState({
      count:this.state.count+1
    })
    console.info("update count:",this.state.count) // update count: 1
  }
  render() {
    return (
      <div> {this.state.count} <button onClick={this.handleChangeCount}>更改</button> </div>
    )
  }
}
複製代碼

能夠看到在 componentDidMount 生命週期函數與 handleChangeCount 合成事件裏,setState 以後,獲取到的 state 的值是舊值。

見示例庫裏的 stateDemo5.js

5.1.一、setState 合併處理

採用這種設置 state 方式,也會出現合併的現象:

import React, { Component } from 'react'

export default class stateDemo6 extends Component {
  state = {
    count:0
  }
  handleChangeCount = () => {
    this.setState({
      count:this.state.count+1
    },() => {
      console.info("update count:",this.state.count)
    })
    this.setState({
      count:this.state.count+1
    },() => {
      console.info("update count:",this.state.count)
    })
    this.setState({
      count:this.state.count+1
    },() => {
      console.info("update count:",this.state.count)
    })
  }
  render() {
    return (
      <div> {this.state.count} <button onClick={this.handleChangeCount}>更改</button> </div>
    )
  }
}
複製代碼

輸出控制檯信息以下:

update count: 1
update count: 1
update count: 1
複製代碼

本質上等同於 Object.assign

Object.assign(state,
  {count: state.count + 1},
  {count: state.count + 1},
  {count: state.count + 1}
 )
複製代碼

即後面的對象會覆蓋前面的,因此只有最後的 setState 纔是有效

見示例庫裏的 stateDemo6.js

那麼要怎麼弄纔不會合併呢?

setState 的第一個參數設置爲函數形式:

import React, { Component } from 'react'

export default class stateDemo7 extends Component {
  state = {
    count:0
  }
  handleChangeCount = () => {
    this.setState(prevState => {
      return {
        count:prevState.count+1
      }
    },() => {
      console.info("update count:",this.state.count)
    })
    this.setState(prevState => {
      return {
        count:prevState.count+1 
      }
    },() => {
      console.info("update count:",this.state.count)
    })
    this.setState(prevState => {
      return {
        count:prevState.count+1 
      }
    },() => {
      console.info("update count:",this.state.count)
    })
  }
  render() {
    return (
      <div> {this.state.count} <button onClick={this.handleChangeCount}>更改</button> </div>
    )
  }
}

複製代碼

輸出控制檯信息以下:

update count: 3
update count: 3
update count: 3
複製代碼

函數式 setState 工做機制相似於:

[
    {increment: 1},
    {increment: 1},
    {increment: 1}
].reduce((prevState, props) => ({
    count: prevState.count + props.increment
}), {count: 0})
// {count: 3}
複製代碼

見示例庫裏的 stateDemo7.js

5.2 在原生事件和 setTimeout 裏是同步的

import React, { Component } from 'react'

export default class stateDemo8 extends Component {
  state = {
    count:0
  }
  componentDidMount() {
    document.querySelector("#change").addEventListener("click", () => {
      this.setState({
        count: this.state.count + 1,
      });
      console.log("update count1:", this.state.count); // update count1: 1
    });
  }
  handleChangeCount = () => {
      setTimeout(() => {
        this.setState({
          count: this.state.count + 1,
        });
        console.log("update count2:", this.state.count); // update count2: 1
      }, 0);
  }
  render() {
    return (
      <div> <p>{this.state.count}</p> <button id="change">更改1</button> <button onClick={this.handleChangeCount}>更改2</button> </div>
    )
  }
}

複製代碼

能夠看到原生的事件(經過 addEventListener 綁定的),或者 setTimeout 等異步方式更改的 state 是同步的。

見示例庫裏的 stateDemo8.js

六、源碼解讀

網上的根據 isBatchingUpdates 變量的值來判斷是同步仍是異步的方式,實際上 react 16.8 以前的代碼實現。 我這邊是 React 17.0.1 源碼

  1. setState 內會調用this.updater.enqueueSetState
// packages/react/src/ReactBaseClasses.js
Component.prototype.setState = function (partialState, callback) {
  // 省略次要代碼
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼
  1. enqueueSetState 方法中會建立 update 並調度 update
// packages/react-reconciler/src/ReactFiberClassComponent.old.js
enqueueSetState(inst, payload, callback) {
  // 經過組件實例獲取對應fiber
  const fiber = getInstance(inst);
  const eventTime = requestEventTime();
  const suspenseConfig = requestCurrentSuspenseConfig();
  // 獲取優先級
  const lane = requestUpdateLane(fiber, suspenseConfig);
  // 建立update
  const update = createUpdate(eventTime, lane, suspenseConfig);
  update.payload = payload;
  // 賦值回調函數
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }
  // 將update插入updateQueue
  enqueueUpdate(fiber, update);
  // 調度update
  scheduleUpdateOnFiber(fiber, lane, eventTime);
}
複製代碼
  1. scheduleUpdateOnFiber 方法中會根據 lane 進行不一樣的處理(重點)
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
    // 省略與本次討論無關代碼
    if (lane === SyncLane) {  // 同步任務
      if ( // 檢查當前是否是在unbatchedUpdates(非批量更新),(初次渲染的ReactDOM.render就是unbatchedUpdates)
      (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext) {
        // Register pending interactions on the root to avoid losing traced interaction data.
        schedulePendingInteractions(root, lane); 
        performSyncWorkOnRoot(root);
      } else {
        ensureRootIsScheduled(root, eventTime);
        schedulePendingInteractions(root, lane);
        if (executionContext === NoContext) {
          resetRenderTimer();
          flushSyncCallbackQueue();
        }
      }
    } else { // 異步任務
      // concurrent模式下是跳過了 flushSyncCallbackQueue 同步更新
      // ....
    } 
  }
複製代碼

能夠看出邏輯主要在判斷 lane executionContext 這兩個變量。

lane 是由 requestUpdateLane 方法返回的:

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export function requestUpdateLane(fiber: Fiber): Lane {
  // Special cases
  const mode = fiber.mode;
  if ((mode & BlockingMode) === NoMode) {
    return (SyncLane: Lane);
  } else if ((mode & ConcurrentMode) === NoMode) {
    return getCurrentPriorityLevel() === ImmediateSchedulerPriority
      ? (SyncLane: Lane)
      : (SyncBatchedLane: Lane);
  }
  // 省略其餘代碼
  return lane;
}
複製代碼

能夠看到首先判斷模式:

若是是採用 legacy 模式,則返回 SyncLane

若是是採用 concurrent ,當優先級沒達到當即執行時,則返回 SyncBatchedLane,不然返回 SyncLane

接着說下 executionContext 變量:

每次觸發事件都會調用 batchedEventUpdates$1 ,而在這方法裏會給 executionContext 賦值,並在執行完以後將 executionContext 還原

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}
複製代碼

因此:

若是是 concurrent 模式,因爲並不會去判斷 executionContext === NoContext ,因此不可能同步。

而在 legacy 模式下,當 executionContext === NoContext 時,就會同步,那麼二者什麼時候相等呢?

默認 executionContext 就是爲 NoContext

而在 react 能管控到的範圍,好比 batchedEventUpdates$1 方法裏都會將 executionContext 設置爲非 NoContext ,因此在合成事件和生命週期函數裏是異步的。

但在 react 管控不到的,好比經過 addEventListener 綁定的事件,以及異步方法 setTimeout 就是同步的。

異步方法之因此是同步是因爲當執行 setTimeout 後,react 會將 NoContext 還原,即上面的 finally 代碼處理的,因此等到 setTimeout 回調函數執行時,executionContext 等於 NoContext 了。

根據上面的分析,你們應該能夠很清晰的知道開頭那個面試題目分別會輸出什麼了吧。

答案是: 0 0 2 3

見示例庫裏的 stateDemo

相關文章
相關標籤/搜索