React - setState 原理

一、setState 的基本特性

1. setState 获取更新状态

在 React 控制的回调函数中,setState是异步的,所以在设置setState后,是无法立即通过this.state是获取最新状态的。如果要获取最新的状态得在setState回调中获取。

this.setState({
	name: 'test'
}, ()=>{
	console.log(this.state.name);
})

2. setState 合并调用

对象式setState多次调用会发生合并,就像Object.assign的对象合并,相同的key最后一个会覆盖前面的key。

this.setState({
	number: this.state.number + 1
})
this.setState({
	number: this.state.number + 1
})

// 类似于
Object.assign(
  previousState,
  {number: this.state.number + 1},
  {number: this.state.number + 1}
)

// 只执行一次
{ ...previousState, number: this.state.number + 1 }

3. setState 同步更新

setstate在原生事件,setTimeout,setInterval,promise等异步操作中,state会同步更新。

当执行到 setTimeout 的时候,会将函数块放入宏任务列队里暂不执行,先去执行主进程代码块,等主进程执行完了, isBatchingUpdates 变为了 false ,导致最后去执行队列里的 setState 时候, 表现就会和原生事件一样,同步拿到最新的state的值。

4. setState 异步批量更新

在react生命周期和合成事件执行前后都有相应的钩子,分别是pre钩子和post钩子,pre钩子会调用batchedUpdate方法将isBatchingUpdates变量置为true,开启批量更新,而post钩子会将isBatchingUpdates置为false。

二、setState 执行过程

setstate方法

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  // 这里的this.updater就是ReactUpdateQueue
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

setState函数有两个参数:

第一个参数是需要修改的setState对象,或者是函数。

第二个参数是修改之后的回调函数。

这里的partialState会产生新的state以一种Object.assgine()的方式跟旧的state进行合并。

enqueueSetState

  enqueueSetState: function (publicInstance, partialState) {
     // 获取当前组件的instance
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

     // 将要更新的state放入一个数组里
     // _pendingStateQueue(待更新队列) 与 _pendingCallbacks(更新回调队列)
     var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

     //  将要更新的component instance也放在一个队列里
    enqueueUpdate(internalInstance);
  }

enqueueSetState 主要任务:
1、将新的state放进数组里
2、用enqueueUpdate来处理将要更新的实例对象

enqueueUpdate

function enqueueUpdate(component) {
  // 如果没有处于批量创建/更新组件的阶段,则处理update state事务
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果正处于批量创建/更新组件的过程,将当前的组件放在dirtyComponents数组中
  dirtyComponents.push(component);
}

由这段代码可以看到,当前如果正处于创建/更新组件的过程,就不会立刻去更新组件,而是先把当前的组件放在dirtyComponent里,所以不是每一次的setState都会更新组件。

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);
    }
  }
};

这里注意两点:
1、如果当前事务正在更新过程中,则使用enqueueUpdate将当前组件放在dirtyComponent里。
2、如果当前不在更新过程的话,则执行更新事务。

transaction

简单说明一下transaction对象,它暴露了一个perform的方法,用来执行anyMethod,在anyMethod执行的前,需要先执行所有wrapper的initialize方法,在执行完后,要执行所有wrapper的close方法,就辣么简单。

在ReactDefaultBatchingStrategy.js,tranction 的 wrapper有两个 FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES

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

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; 

这两个wrapper的initialize都没有做什么事情,但是在callback执行完之后,RESET_BATCHED_UPDATES 的作用是将isBatchingUpdates置为false, FLUSH_BATCHED_UPDATES 的作用是执行flushBatchedUpdates。

里面会循环所有dirtyComponent,调用updateComponent来执行所有的生命周期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最后实现组件的更新。

整体流程

  1. batchedUpdates 函数让 isBatchingUpdates = true
  2. 函数中调用setState
  3. setState 设置 enqueueSetState 和 enqueueCallback
  4. enqueueSetState 调用 enqueueUpdate
    待更新的state放进调用component的更新队列中
    enqueueUpdate 调用 component(待更新的component 放进一个更新 队列)
  5. enqueueUpdate
    判断isBatchingUpdates
    false 执行batchingStrategy.batchedUpdates
    true 将component放进dirtyComponents数组
  6. batchingStrategy.batchedUpdates
    将当前的 isBatchingUpdates 设置为true
    判断原先的 isBatchingUpdates 是否为true
    false 立即执行更新事务
    true 执行 enqueueUpdate
  7. 事务执行时
    isBatchingUpdates 设置为false
    循环所有dirtyComponent,调用updateComponent来执行所有的生命周期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最后实现组件的更新。

三、setState 相关的问题

1. 连续使用 setState,为什么不能实时改变

state.count = 0;
this.setState({count: state.count + 1}); 
this.setState({count: state.count + 1}); 
this.setState({count: state.count + 1}); 
// state.count === 1,不是 3

因为 this.setState 方法为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,

2. 为什么要 setState,而不是直接改变this.state

因为 setState 做的事情不仅仅只是修改了 this.state 的值,另外最重要的是它会触发 React 的更新机制,会进行diff,然后将 patch 部分更新到真实 dom 里。

如果你直接 this.state 的话,state 的值确实会改,但是它不会驱动 React 重新渲染。

当调用 setState 后,React 的 生命周期函数 会依次顺序执行

  • static getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

3. 为什么会出现异步的情况

为了性能优化,React 会将每次调用的 setState 放入队列中做一次性处理,在事件结束之后再产生一次重新渲染,为的就是把 Virtual DOM 和 DOM 树操作降到最小,提高应用性能。

参考链接
React setState 之后发生了什么
React setState 整理总结
深入浅出 setState 原理篇

你可能感兴趣的:(React,react.js)