博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【React深入】setState的执行机制
阅读量:6680 次
发布时间:2019-06-25

本文共 4885 字,大约阅读时间需要 16 分钟。

一.几个开发中经常会遇到的问题

以下几个问题是我们在实际开发中经常会遇到的场景,下面用几个简单的示例代码来还原一下。

1.setState是同步还是异步的,为什么有的时候不能立即拿到更新结果而有的时候可以?

1.1 钩子函数和React合成事件中的setState

现在有两个组件

componentDidMount() {    console.log('parent componentDidMount');  }  render() {    return (      
); }

组件内部放入同样的代码,并在Setstate1中的componentDidMount中放入一段同步延时代码,打印延时时间:

componentWillUpdate() {    console.log('componentWillUpdate');  }  componentDidUpdate() {    console.log('componentDidUpdate');  }  componentDidMount() {    console.log('SetState调用setState');    this.setState({      index: this.state.index + 1    })    console.log('state', this.state.index);        console.log('SetState调用setState');    this.setState({      index: this.state.index + 1    })    console.log('state', this.state.index);  }

下面是执行结果:

image

说明:

  • 1.调用setState不会立即更新
  • 2.所有组件使用的是同一套更新机制,当所有组件didmount后,父组件didmount,然后执行更新
  • 3.更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。

1.2 异步函数和原生事件中的setstate

setTimeout中调用setState(例子和在浏览器原生事件以及接口回调中执行效果相同)

componentDidMount() {    setTimeout(() => {      console.log('调用setState');      this.setState({        index: this.state.index + 1      })      console.log('state', this.state.index);      console.log('调用setState');      this.setState({        index: this.state.index + 1      })      console.log('state', this.state.index);    }, 0);  }

执行结果:

image

说明:

  • 1.在父组件didmount后执行
  • 2.调用setState同步更新

2.为什么有时连续两次setState只有一次生效?

分别执行以下代码:

componentDidMount() {    this.setState({ index: this.state.index + 1 }, () => {      console.log(this.state.index);    })    this.setState({ index: this.state.index + 1 }, () => {      console.log(this.state.index);    })  }
componentDidMount() {    this.setState((preState) => ({ index: preState.index + 1 }), () => {      console.log(this.state.index);    })    this.setState(preState => ({ index: preState.index + 1 }), () => {      console.log(this.state.index);    })  }

执行结果:

11
22

说明:

  • 1.直接传递对象的setstate会被合并成一次
  • 2.使用函数传递state不会被合并

二.setState执行过程

由于源码比较复杂,就不贴在这里了,有兴趣的可以去githubclone一份然后按照下面的流程图去走一遍。

1.流程图

setState

图不清楚可以点击

  • partialStatesetState传入的第一个参数,对象或函数
  • _pendingStateQueue:当前组件等待执行更新的state队列
  • isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用
  • dirtyComponent:当前所有处于待更新状态的组件队列
  • transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close
  • FLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法

2.执行过程

对照上面流程图的文字说明,大概可分为以下几步:

  • 1.将setState传入的partialState参数存储在当前组件实例的state暂存队列中。
  • 2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。
  • 3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。
  • 4.调用事务的waper方法,遍历待更新组件队列依次执行更新。
  • 5.执行生命周期componentWillReceiveProps
  • 6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。
  • 7.执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。
  • 8.执行生命周期componentWillUpdate
  • 9.执行真正的更新,render
  • 10.执行生命周期componentDidUpdate

三.总结

1.钩子函数和合成事件中:

react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。

按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent

当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState

2.异步函数和原生事件中

由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

3.partialState合并机制

我们看下流程中_processPendingState的代码,这个函数是用来合并state暂存队列的,最后返回一个合并后的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;  },

我们只需要关注下面这段代码:

_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);

如果传入的是对象,很明显会被合并成一次:

Object.assign(  nextState,  {index: state.index+ 1},  {index: state.index+ 1})

如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。

4.componentDidMount调用setstate

在componentDidMount()中,你 可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。谨慎使用这一模式,因为它常导致性能问题。在大多数情况下,你可以 在constructor()中使用赋值初始状态来代替。然而,有些情况下必须这样,比如像模态框和工具提示框。这时,你需要先测量这些DOM节点,才能渲染依赖尺寸或者位置的某些东西。

以上是官方文档的说明,不推荐直接在componentDidMount直接调用setState,由上面的分析:componentDidMount本身处于一次更新中,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。

当然在componentDidMount我们可以调用接口,再回调中去修改state,这是正确的做法。

当state初始值依赖dom属性时,在componentDidMountsetState是无法避免的。

5.componentWillUpdate componentDidUpdate

这两个生命周期中不能调用setState

由上面的流程图很容易发现,在它们里面调用setState会造成死循环,导致程序崩溃。

6.推荐使用方式

在调用setState时使用函数传递state值,在回调函数中获取最新更新后的state

转载地址:http://qjiao.baihongyu.com/

你可能感兴趣的文章
java面试-深入理解JVM(二)——揭开HotSpot对象创建的奥秘
查看>>
跟小静读CLR via C#(13)-浅谈事件
查看>>
图像滤镜艺术---乐高像素拼图特效
查看>>
读书笔记--101个shell脚本 之#9
查看>>
Redis 内存管理与事件处理
查看>>
iOS安全–IOS应用调试检测以及反调试
查看>>
备份策略
查看>>
Exchange 2013之(八)边缘传输服务器高可用
查看>>
网站静态化处理
查看>>
热烈祝贺道普测评荣获2017全国大学生软件测试大赛最佳组织奖
查看>>
yarn模式运行spark作业所有属性详解
查看>>
从JVM角度看Java多态
查看>>
loadrunner如何监控windows,以及重点指标分析
查看>>
内置数据结构(1)
查看>>
2017云计算热点回顾:拓展云边界 决胜智能云
查看>>
linux下使用lftp的小结
查看>>
面试敲门砖
查看>>
python将日志导入数据库代码案例2
查看>>
ORA-12705: Cannot access NLS data files or invalid environment specified
查看>>
用 Python 脚本实现对 Linux 服务器的监控
查看>>