react setState 进阶

775 阅读9分钟

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 queue

image.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

image.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 多个不同的值,在更新时会对其进行合并批量更新。

1、合成事件中的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>
    )
  }
}

2、生命周期函数中的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>
    )
  }
}

3、原生事件中的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>
    )
  }
}

4、setTimeout中的setState

基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。

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

5、批量更新

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

setState为什么是异步的

1. 保证内部的一致性

即使state是同步更新,props也不是。(你只有在父组件重新渲染时才能知道props)

2. 性能优化

将state的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗。

原理解释

面试官:“react中setState是同步的还是异步?”
我:“异步的,setState不能立马拿到结果。”

面试官:“那什么场景下是异步的,可不可能是同步,什么场景下又是同步的?”
我:“......”

setState并不是真正意义上的异步操作,它只是模拟了异步的行为

为什么这么说。可以通过下面的例子
不是真正意义上的异步操作

class App extends Component {
  state = {
    count: 0
  };

  componentDidMount() {
    // 生命周期中调用
    this.setState({ count: this.state.count + 1 });
    console.log("lifecycle: " + this.state.count);
    setTimeout(() => {
      // setTimeout中调用
      this.setState({ count: this.state.count + 1 });
      console.log("setTimeout: " + this.state.count);
    }, 0);
    document.getElementById("div2").addEventListener("click", this.increment2);
  }

  increment = () => {
    // 合成事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("react event: " + this.state.count);
  };

  increment2 = () => {
    // 原生事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("dom event: " + this.state.count);
  };

  render() {
    return (
      <div className="App">
        <h2>couont: {this.state.count}</h2>
        <div id="div1" onClick={this.increment}>
          click me and count+1
        </div>
        <div id="div2">click me and count+1</div>
      </div>
    );
  }
}

探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClickonChange这些都是合成事件。

那么以上4种方式调用setState(),后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套。

在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。

源码分析

总结

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

对于3 可以结合下面的例子:

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

结合上面分析的,钩子函数中的 setState 无法立马拿到更新后的值,所以前两次都是输出0,当执行到 setTimeout 里的时候,前面两个state的值已经被更新,由于 setState 批量更新的策略, this.state.val 只对最后一次的生效,为1,而在 setTimmout 中 setState 是可以同步拿到更新结果,所以 setTimeout 中的两次输出2,3,最终结果就为 0, 0, 2, 3 。

想一下,如何将上面的代码中前两次连续 + 1 都执行呢?

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

  componentDidMount() {
    this.setState((preState) => {
        console.log(preState.val)   // 0
        return { val: preState + 1 }
    })
     
    this.setState((preState) => {
        console.log(preState.val)   // 1
        return { val: preState.val + 1 }
    })

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

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

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