【React進階系列】 setState機制

api解析: setState(updater, [callback])


updater: 更新數據 FUNCTION/OBJECT
callback: 更新成功後的回調 FUNCTION
// updater - Function
this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});

// update - Object
this.setState({quantity: 2})

setState的特色:


1.異步:react一般會集齊一批須要更新的組件,而後一次性更新來保證渲染的性能
2.淺合併 Objecr.assign()

setState問題與解決


  • 在使用setState改變狀態以後,馬上經過this.state去拿最新的狀態
    解決: componentDidUpdate或者setState的回調函數裏獲取
// setState回調函數
changeTitle: function (event) {
  this.setState({ title: event.target.value }, () => this.APICallFunction());
},
APICallFunction: function () {
  // Call API with the updated value
}
  • 有一個需求,須要在在onClick裏累加兩次,使用對象的方法更新,則只會加一次
    解決: 使用updater function
onClick = () => {
    this.setState({ index: this.state.index + 1 });
    this.setState({ index: this.state.index + 1 });
}

// 最後解析爲,後面的數據會覆蓋前面的更改,因此最終只加了一次.
Object.assign(
  previousState,
  {index: state.index+ 1},
  {index: state.index+ 1},
)

//正確寫法
onClick = () => {
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
}

注意:

1.不要在render()函數裏面寫setstate(),除非你本身定製了shouldComponentUpdate方法,要否則會引發無限循環
render() {
    //this.setState
    return(
        //...dom
    )
}
2.爲何不能使用第一個方式更新
react爲了實現高效render, state實際上是一個隊列,setState是將數據插入隊列中,使用第一種不會觸發渲染, react提供了setState的實例方法能夠觸發render,能夠看後面的源碼
// 1
this.state.num = 1
// 2
this.setState({
    num: this.state.num + 1
})
3.對數組和對象等引用對象操做時,使用返回新對象的方法
  array: 不要使用push、pop、shift、unshift、splice可以使用concat、slice、filter、擴展語法
  object: Object.assgin/擴展語法

setState更新機制


如圖: pending queue 和 update queuereact

clipboard.png

setState源碼


this.setState()

ReactComponent.prototype.setState = function (partialState, callback) {
  //  將setState事務放進隊列中
  //  this.updater就是ReactUpdateQueue, this是組件的實例
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

enqueueSetState()

enqueueSetState: function (publicInstance, partialState) {
     // 獲取當前組件的instance
     // 實例中有兩個很是重要的屬性:_pendingStateQueue(待更新隊列) 與 _pendingCallbacks(更新回調隊列)
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

     // 初始化待更新隊列
     var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
     // 將要更新的state放入一個數組裏
    queue.push(partialState);

     //  將要更新的component instance也放在一個隊列裏
    enqueueUpdate(internalInstance);
  }

enqueueUpdate

當前若是正處於建立/更新組件的過程,就不會馬上去更新組件,而是先把當前的組件放在dirtyComponent裏,因此不是每一次的setState都會更新組件
function enqueueUpdate(component) {
  // 若是沒有處於批量建立/更新組件的階段,則處理update state事務
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 若是正處於批量建立/更新組件的過程,將當前的組件放在dirtyComponents數組中
  dirtyComponents.push(component);
}

batchingStrategy

var ReactDefaultBatchingStrategy = {
  // 用於標記當前是否出於批量更新
  isBatchingUpdates: false,
  // 當調用這個方法時,正式開始批量更新
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 若是當前事務正在更新過程在中,則調用callback,既enqueueUpdate
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
    // 不然執行更新事務
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

transaction

initalize(空函數) -> perform(anyMethos) -> close

clipboard.png

// 將isBatchingUpdates置爲false
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 循環全部dirtyComponent,調用updateComponent來執行全部的生命週期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最後實現組件的更新
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

setState面試


面試官:「react中setState是同步的仍是異步?」
我:「異步的,setState不能立馬拿到結果。」
面試官:「那什麼場景下是異步的,可不多是同步,什麼場景下又是同步的?」

下題結果是:面試

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

// 結果就爲 0, 0, 2, 3

總結

1.setState 只在合成事件和鉤子函數中是「異步」的,在原生事件和 setTimeout 中都是同步的。
2.setState的「異步」並非說內部由異步代碼實現,其實自己執行的過程和代碼都是同步的,只是合成事件和鉤子函數的調用順序在更新以前,致使在合成事件和鉤子函數中無法立馬拿到更新後的值,形式了所謂的「異步」,固然能夠經過第二個參數 setState(partialState, callback) 中的callback拿到更新後的結果。
3.setState 的批量更新優化也是創建在「異步」(合成事件、鉤子函數)之上的,在原生事件和setTimeout 中不會批量更新,在「異步」中若是對同一個值進行屢次 setState , setState 的批量更新策略會對其進行覆蓋,取最後一次的執行,若是是同時 setState 多個不一樣的值,在更新時會對其進行合併批量更新。

一、合成事件中的setState

合成事件,react爲了解決跨平臺,兼容性問題,本身封裝了一套事件機制,代理了原生的事件,像在jsx中常見的onClick、onChange這些都是合成事件
合成事件中也有batchedUpdates方法,是經過一樣的事務完成的
class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新前的val --> 0
  }

  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

二、生命週期函數中的setState

整個生命週期中就是一個事物操做,因此標識位isBatchingUpdates = true,因此流程到了enqueueUpdate()時,實例對象都會加入到dirtyComponents 數組中
class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 輸出的仍是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

三、原生事件中的setState

原生事件是指非react合成事件,原生自帶的事件監聽 addEventListener ,或者也能夠用原生js、jq直接 document.querySelector().onclick 這種綁定事件的形式都屬於原生事件
原生事件綁定不會經過合成事件的方式處理,天然也不會進入更新事務的處理流程。setTimeout也同樣,在setTimeout回調執行時已經完成了原更新組件流程,不會放入dirtyComponent進行異步更新,其結果天然是同步的。
class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新後的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }
 
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

四、setTimeout中的setState

基於event loop的模型下, setTimeout 中裏去 setState 總能拿到最新的state值。api

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 輸出更新後的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

五、批量更新

在 setState 的時候react內部會建立一個 updateQueue ,經過 firstUpdate 、 lastUpdate 、 lastUpdate.next 去維護一個更新的隊列,在最終的 performWork 中,相同的key會被覆蓋,只會對最後一次的 setState 進行更新數組

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
 }

  render() {
    return (
      <div onClick={this.batchUpdates}>
        {`Counter is ${this.state.val}`} // 1
      </div>
    )
  }
}

引起感想來源:dom

https://juejin.im/post/5b45c5...異步

相關文章
相關標籤/搜索