资讯专栏INFORMATION COLUMN

一个关于React.Component.setState的问题

BoYang / 2252人阅读

摘要:不保证这个状态的更新是立即执行的。这个问题导致如果开发者在之后立即去访问可能访问的不是最新的状态。不应该被直接更改,而是应该新建一个来表示更新后的状态。实验采用基于控制变量法的对照试验。至于的问题,留给读者自己吧。

React组件重新渲染的条件是:
B.只要调用this.setState()就会发生重新渲染。
C.必须调用this.setState()且传递不同于当前this.setState()的参数,才会引发重新渲染。

本文将从三方面说明这个问题为什么选择C。或者说为什么 setState 在传递不同当前 this.State 的参数,才会引发组件重新渲染。

结论

我还是想选择B

引用规范

TL;DR

下面是 React 官方对于 setState 的说明,翻译的作者是我。在这段文章中,对setState说明了两点。

setState是异步的。

setState会(always)导致重新渲染,当且仅当shouldComponentUpdate()返回了false的时候不会。

读者可以直接进入实验部分。

React原文中关于setState的说明:

setState(updater[, callback])

setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.

setState() 会将当前组件的 state 的更改全部推入队列,并且通知 React 这个组件和他的孩子们需要更新这些状态并重新渲染。这是开发者经常使用的用来更新 UI 的方法(不管是在事件响应中还是处理服务端的返回)。

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.

setState()当作一个更新的_请求_而不是一个更新的函数。为了更好的性能,React 可能会延迟这些更新,将几个组件的更新合并在一起执行。React不保证这个状态的更新是立即执行的。

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

setState()并不是立即更新这些组件,而是可能延后批量更新。这个问题导致如果开发者在setState()之后立即去访问this.state可能访问的不是最新的状态。然而,开发者还是可以使用一些方法来访问到最新的state的。比如在组件生命周期的componentDidUpdate,或者在setState的回调函数中。当然了如果你需要依赖之前的状态来更新当前的状态,看一看这个updater

setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.

setState() 肯定总是一直毫无疑问的会导致render函数被重新调用[1],除非shouldComponentUpdate()返回了false。如果开发者使用了可变的变量或者更新的逻辑无法在shouldComponentUpdate()中编写,那为了减少无意义的重新渲染,应该仅仅在确定当前的新状态和旧状态不一样的时候调用setState()。【希望读者不要误会,React是让开发者自己做这个比较。不是React替你做好了的。】

[1].(我们把这种行为叫做重新渲染)

The first argument is an updater function with the signature:

setState()可以接受两个参数,第一个参数叫做updater的函数,函数的签名如下:

(prevState, props) => stateChange

prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props. For instance, suppose we wanted to increment a value in state by props.step:

prevState是组件之前的状态(引用关系)。prevState不应该被直接更改,而是应该新建一个Object来表示更新后的状态。举个例子:如果开发者想要更新state中的counter给它加一。应该按照下面的做法。

this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});

Both prevState and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with prevState.

React 保证 updater 接受的 prevStateprops 都是最新的。并且updater 的返回是被浅拷贝merge进入老状态的。

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead.

setState()的第二个参数是可选的回调函数。在state更新完成后他会被执行一次。总体上来说,React官方更推荐在componentDidUpdate()中来实现这个逻辑。

You may optionally pass an object as the first argument to setState() instead of a function:

开发者还可以在第一次参数的位置不传入函数,而是传入一个对象。

setState(stateChange[, callback])

This performs a shallow merge of stateChange into the new state, e.g., to adjust a shopping cart item quantity:

像上面这种调用方式中,stateChange会被浅拷贝进入老状态。例如开发者更新购物车中的商品数量的代码应该如下所示:

this.setState({quantity: 2})

This form of setState() is also asynchronous, and multiple calls during the same cycle may be batched together. For example, if you attempt to increment an item quantity more than once in the same cycle, that will result in the equivalent of:

这种形式的setState()也是异步的,而且在一个周期内的多次更新会被批量一起更新。如果你想更新状态里面的数量,让他一直加一加一。代码就会如下所示

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

Subsequent calls will override values from previous calls in the same cycle, so the quantity will only be incremented once. If the next state depends on the previous state, we recommend using the updater function form, instead:

这样队列的调用会重写之前的更新,所以最后数量仅仅会被更新一次。在这种新状态依赖老状态数据的情况下,React官方推荐大家使用函数。如下所示:

this.setState((prevState) => {
  return {quantity: prevState.quantity + 1};
});
实验验证

设计实验来验证React官方说法的正确性。实验采用基于控制变量法的对照试验。

基于 React16( 引入了 Fiber 架构)和 React 0.14 分别进行实验。至于React 15的问题,留给读者自己吧。

编写如下组件代码:

class A extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            a:1
        }
        this._onClick = this.onClick.bind(this);
    }
    onClick(){
        this.setState({a:2}) // 替换点
    }
    render(){
        console.log("rerender");
        return(
            

a: {this.state.a}

{Math.random()}

); } }

如果需要可以读者自行粘贴重新复现实验。

更新的标准:界面中显示的随机数是否发生了变化。当然也可以观察Console中是否出现了rerender

React 0.14.5 实验结果如下所示:

条件 不编写shouldComponentUpdate()方法 return false; return true;
setState({}) 更新 不更新 更新
setState(null) 更新 不更新 更新
setState(undefined) 更新 不更新 更新
setState(this.state) 更新 不更新 更新
setState(s=>s) 更新 不更新 更新
setState({a:2}) 更新 不更新 更新

React 16 实验结果如下所示:

条件 不编写shouldComponentUpdate()方法 return false; return true;
setState({}) 更新 不更新 更新
setState(null) 不更新 不更新 不更新
setState(undefined) 不更新 不更新 不更新
setState(this.state) 更新 不更新 更新
setState(s=>s) 更新 不更新 更新
setState({a:2}) 更新 不更新 更新

可见对于setState()来说,React 在不同版本的表现不尽相同。

React 0.14中可能更符合只要调用setState()就会进行更新。

React 16.3.2中只有在传递null和undefined的时候才不会更新,别的时候都更新。

源码说明 React 16中是这样的:

https://github.com/facebook/r...

1. const payload = update.payload;
2. let partialState;
3. if (typeof payload === "function") {
4.       partialState = payload.call(instance, prevState, nextProps);
5. } else {
6.       // Partial state object
7.       partialState = payload;
8. }
9. if (partialState === null || partialState === undefined) {
10.   // Null and undefined are treated as no-ops.
11.   return prevState;
12.}
13.// Merge the partial state and the previous state.
14.return Object.assign({}, prevState, partialState);
React 14中是这样的:
证有容易,证无难,所以我要顺着整条链路的源码的展示一遍。

TL;DR

var nextState = assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
   var partial = queue[i];
   assign(nextState, typeof partial === "function" ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;

流程中没有任何比较操作。

1.调用

setState({})

2.原型方法

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

3.入队方法

enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, "setState");
    if (!internalInstance) {
      return;
    }
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);
    enqueueUpdate(internalInstance);
  },

internalInstance 是一个 ReactCompositeComponentWrapper,大概就是包装着ReactComponent实例的一个对象。

4.入队

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

5.更具当前的批量策略来决定更新方法

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

6.可以看到直到这里都没人管这个东西到底更新的是什么。
7.剩下的事情基本就是垃圾回收处理现场的事情了。
8.处理完之后会

ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);

9.请求更新队列,进行更新

var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction"s wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

10.更新

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;

  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator);

  for (var i = 0; i < len; i++) {
    // If a component is unmounted before pending changes apply, it will still
    // be here, but we assume that it has cleared its _pendingCallbacks and
    // that performUpdateIfNecessary is a noop.
    var component = dirtyComponents[i];

    // If performUpdateIfNecessary happens to enqueue any new updates, we
    // shouldn"t execute the callbacks until the next render happens, so
    // stash the callbacks first
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}

11.最重要的来了

performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
    }

    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    }
  },

12.更新

updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
    //... props context 更新
    var nextState = this._processPendingState(nextProps, nextContext);

    var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);

    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
    } else {
      // If it"s determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },

13.计算state

_processPendingState: function (props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;

    if (!queue) {
      return inst.state;
    }

    if (replace && queue.length === 1) {
      return queue[0];
    }

    var nextState = assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      assign(nextState, typeof partial === "function" ? partial.call(inst, nextState, props, context) : partial);
    }

    return nextState;
  },

14.就这样了。

var nextState = assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
   var partial = queue[i];
   assign(nextState, typeof partial === "function" ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;

15.流程中没有任何比较操作。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94773.html

相关文章

  • React入门0x006: State

    摘要:概述这一章讲,是的核心,也算是的核心思想都很核心啊。但是接着我们又搞了一个定时器,每秒执行一直函数,将修改为最新的时间。就完成了视图的更新。参数一是要更新的数据,我们只需要传输我们要更新的数据即可,对于不需要更新的数据,则不需要理睬。 0x000 概述 这一章讲state,state是MVVM的核心,也算是React的核心思想......都很核心啊。 0x001 问题 在上一章节的栗子...

    TNFE 评论0 收藏0
  • 关于个人开源项目(vue app)一些总结

    摘要:关于个人开源项目的一些总结项目地址项目简介此项目名叫。网站目前实现了登录注册日历导入文件考勤导出缺勤名单等核心功能。这对于小型项目来说并没有什么问题。编译后的大小关于文件上传与导出功能文件上传导出可以说是此项目最关键的点了。 关于个人开源项目(vue app)的一些总结 项目地址 https://github.com/BYChoo/record 项目简介 此项目名叫:Record。是以...

    since1986 评论0 收藏0
  • 关于个人开源项目(vue app)一些总结

    摘要:关于个人开源项目的一些总结项目地址项目简介此项目名叫。网站目前实现了登录注册日历导入文件考勤导出缺勤名单等核心功能。这对于小型项目来说并没有什么问题。编译后的大小关于文件上传与导出功能文件上传导出可以说是此项目最关键的点了。 关于个人开源项目(vue app)的一些总结 项目地址 https://github.com/BYChoo/record 项目简介 此项目名叫:Record。是以...

    高胜山 评论0 收藏0
  • 2017年五月前端面试题目总结

    摘要:持续心累的找工作阶段算是结束了,不同公司对面试的知识侧重点不同,整体的感受就是大公司可能更偏向一些基础或者原理布局一些经典算法方面。现将我在面试过程遇到的问题总结下。目前先传题目答案整理好之后再发布出来。 持续心累的找工作阶段算是结束了,不同公司对面试的知识侧重点不同,整体的感受就是:大公司可能更偏向一些JS基础或者原理、html布局、一些经典算法方面。小公司的面试更加侧重对经验和细节...

    warkiz 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<