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

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

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

结论

我还是想选择B

引用规范

TL;DR

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

  1. setState是异步的。
  2. 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},...
)

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(
            <div onClick={this._onClick}>
                <p>a: {this.state.a}</p>
                <p>{Math.random()}</p>
            </div>
        );
    }
}

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

更新的标准:界面中显示的随机数是否发生了变化。当然也可以观察 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({},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);<!--here-->
      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;
<!--here-->
    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._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,nextContext);

    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`,`this.state` and `this.context`.
      this._performComponentUpdate(nextParentElement,nextProps,nextContext,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,context) : partial);
    }

    return nextState;
  },

14.就这样了。

var nextState = assign({},context) : partial);
}
return nextState;

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom