资讯专栏INFORMATION COLUMN

关于this.setState( )中的数据延迟问题

Lorry_Lu / 3030人阅读

摘要:判断当前是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。将组件的暂存队列中的进行合并,获得最终要更新的对象,并将队列置为空。执行生命周期,根据返回值判断是否要继续更新。

this.setState( )方法是React.js中最常见的一种方法,利用它可以控制各种状态变化,达到页面各种交互效果,但是,我们在React开发中偶尔会发现,明明已经通过this.setState( )方法处理过某个state的值,但是在后续的方法里,log打印出来仍然是之前的值,或者,第一次获取到原来的值,第二次才能获取到设置之后的新值,让人误以为是因为电脑或浏览器性能问题造成的"延迟"问题。

执行过程

为了理解这个问题,我们首先来看一下setState这个过程中发生了什么:

setState传入的partialState参数存储在当前组件实例的state暂存队列中。

判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。

如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。

调用事务的waper方法,遍历待更新组件队列依次执行更新。

执行生命周期componentWillReceiveProps

将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。

执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。

执行生命周期componentWillUpdate

执行真正的更新,render重新渲染。

执行生命周期componentDidUpdate

官方解释

首先思考为什么会出现这种情况,在facebook给出的官方文档中我们可以看到这么一段话:

setState(updater[, callback])

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( ) 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( ) 更类似于是一种请求而不是立即更新组件的命令

为了更好的性能,React会延迟调用它,不会保证state的变更会立即生效,而是会批量推迟更新

官方承认会存在隐患

建议在componentDidUpdate中执行或利用回调函数(setState(updater, callback))

举个简单例子:

constructor(props) {
  super(props);
  this.state = {
    num: 1
  };
}

componentDidMount = () => {
  this.setState({ num: this.state.num + 1 });
  console.log(this.state.num);   // 1
}

这是因为this.setState( )本身是异步的,程序异步运行,可以提高程序运行的效率,不必等一个程序跑完,再跑下一个程序,特别当这两个程序是无关的时候。React会去合并所有的state变化,在前一个方法未执行完时,就先开始运行后一个方法。但是实际操作中,为了能实时获取后一个状态值,需要一些解决的办法。

利用全局属性

尝试一下换个写法,利用全局属性的办法而不是用state的方式去获取数据:

constructor(props) {
  super(props);
  this.num = 1;
}

componentDidMount = () => {
  this.num = this.num + 1;
  console.log(this.num);   // 2
}

这其实是一种取巧的方式,写法方便,原理简单,但是并不十分推荐,因为它并不符合React中关于有状态组件的设计理念,存在有可能无法触发刷新的风险(虽然在我的开发过程从没有发生这样的事),所以还是希望大家优先使用下面的方法。

利用回调函数

回调函数众所周知,就是某个函数执行完毕后执行的函数,利用它可以确保在this.setState( )整个函数执行完成之后去获取this.state.xxx的值:

constructor(props) {
  super(props);
  this.state = {
    num: 1
  };
}

componentDidMount = () => {
  this.setState({ num: this.state.num + 1 }, () => {
    console.log(this.state.num);   // 2
  });
  console.log(this.state.num);   // 1
}

控制台按顺序先后打印出两个结果:

1
2
利用setTimeout( )

首先简单回顾一下,利用setTimeout( )模拟一下前文提到的Javascript中的异步:

foo = () => {
  console.log("11111111");
  setTimeout(function(){
    console.log("22222222");
  },1000);
};
bar = () => {
  console.log("33333333");  
}
foo();
bar();
// 11111111
// 33333333
// 22222222

所以,在上述代码块中,在前一方法(foo)执行时,后一方法(bar)也可以执行。符合异步的基本概念,程序并不按顺序执行。在foo函数中执行到setTimeout的时候,函数会跳出,并先执行bar( )方法,这样就模拟了一个异步的效果。这里顺便再提一下前面说的,setState方法通过一个队列机制实现state更新,当执行setState的时候,会将需要更新的state合并之后放入状态队列,而不会立即更新,通过下面的例子可见。

constructor(props) {
  super(props);
  this.state = {
    num: 1,
  };
}
componentWillMount = () => {
  this.setState({
    num: this.state.num + 1,
  });
  console.log(this.state.num);
  this.setState({
    num: this.state.num + 1,
  });
  console.log(this.state.num);
}

render() {
  console.log(this.state.num);
  return (
); }

代码输出结果为 1,1,2

利用setTimeout方法可以解决state的异步问题,因为setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的:

componentWillMount = () => {
  setTimeout(() => {
    this.setState({
      num: this.state.num + 1,
    });
    console.log(this.state.num);  // 1
    this.setState({
      num: this.state.num + 1,
    });
    console.log(this.state.num);  // 2
  }, 0);
} 
利用componentDidUpdate( )

根据前面文档所说,在componentDidUpdate( )方法中去获取新的state值,根据React的生命周期,此时this.state已经更新。

constructor(props) {
    super(props);
    this.state = {
      num: 1
    };
}

componentWillMount = () => {
    this.setState({ num: this.state.num + 1 });
}

componentDidUpdate = () => {
    console.log(this.state.num);   // 2
}
警告

⚠️注意,很多新人在遇到这种问题时无所适从,可能会用一些投机取巧的方式,方面的全局对象是一种方式,还有一种就是绕过setState直接赋值:

this.state.num = 2   // 2

理论上讲,这种方法当然也能达到赋值目的,但将state设计成更新延缓到最后批量合并再去渲染,对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗,所以不建议上面写法。

⚠️如果在shouldComponentUpdate或者componentWillUpdate方法中调用setState,此时this._pending-StateQueue != null,就会造成循环调用,使得浏览器内存占满后崩溃。

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

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

相关文章

  • React 中 setState() 为什么是异步的?

    摘要:正文在回复中表示为什么是异步的,这并没有一个明显的答案,每种方案都有它的权衡。需要注意的是,异步更新是有可能实现这种设想的前提。 前言 不知道大家有没有过这个疑问,React 中 setState() 为什么是异步的?我一度认为 setState() 是同步的,知道它是异步的之后很是困惑,甚至期待 React 能出一个 setStateSync() 之类的 API。同样有此疑问的还有 ...

    anonymoussf 评论0 收藏0
  • 一个关于React.Component.setState问题

    摘要:不保证这个状态的更新是立即执行的。这个问题导致如果开发者在之后立即去访问可能访问的不是最新的状态。不应该被直接更改,而是应该新建一个来表示更新后的状态。实验采用基于控制变量法的对照试验。至于的问题,留给读者自己吧。 React组件重新渲染的条件是: B.只要调用this.setState()就会发生重新渲染。 C.必须调用this.setState()且传递不同于当前this.setS...

    BoYang 评论0 收藏0
  • 用RxJS和react开发mac地址输入框

    摘要:项目简介本次使用了和开发了一个地址输入框,主要实现的功能有限制输入符合条件的字符并每隔两位可以自动添加用于分割的冒号。项目屏蔽了的事件处理,同时使用来手动控制光标。继承于和因此同时具有和两者的方法。后面的和都是需要利用最新的来进行判断的。 项目简介 本次使用了RxJS和react开发了一个mac地址输入框,主要实现的功能有限制输入符合条件的字符1-9,a-f,并每隔两位可以自动添加用于...

    CastlePeaK 评论0 收藏0
  • 从 0 到 1 实现 React 系列 —— 4.优化setState和ref的实现

    摘要:异步渲染利用事件循环,延迟渲染函数的调用调用回调函数处理后跟函数的情况浅合并逻辑事件循环,关于的事件循环和的事件循环后续会单独写篇文章。 showImg(https://segmentfault.com/img/remote/1460000015785464?w=640&h=280); 看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 Rea...

    wangdai 评论0 收藏0
  • 如何优雅地在React中处理事件响应

    摘要:处理事件响应是应用中非常重要的一部分。中,处理事件响应的方式有多种。关于事件响应的回调函数,还有一个地方需要注意。不管你在回调函数中有没有显式的声明事件参数,都会把事件作为参数传递给回调函数,且参数的位置总是在其他自定义参数的后面。 React中定义一个组件,可以通过React.createClass或者ES6的class。本文讨论的React组件是基于class定义的组件。采用cla...

    buildupchao 评论0 收藏0

发表评论

0条评论

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