chst365's blog chst365's blog
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

chst365

DIV工程师
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 浏览器

  • webpack

  • TypeScript

  • 性能

  • 工程化

  • React

    • 合成事件
    • virtualdom
    • setState过程
      • 一、几个开发中会遇到的问题
        • 1.setState是同步的还是异步的,为什么有时不能立即拿到更新结果,而有的时候却可以
        • 2. 为什么有时连续两次setState只有一次生效
      • 二、setState 的执行过程
        • 1.流程图
        • 2.执行过程
      • 三、总结
        • 1.钩子函数和合成事件中:
        • 2.异步函数和原生事件中
        • 3.partialState合并机制
        • 4.componentDidMount调用setstate
        • 5.componentWillUpdate componentDidUpdate
        • 6.推荐使用方式
  • JavaScript

  • 编程题

  • React技术揭秘

  • 算法

  • 前端
  • React
chst365
2021-08-03
目录

setState过程

# 一、几个开发中会遇到的问题

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

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

现有两个组件

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

  render() {
    return (
      <div>
        <SetState2></SetState2>
        <SetState></SetState>
      </div>
    );
  }
1
2
3
4
5
6
7
8
9
10
11
12

组件内部放入同样的代码,并在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);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

执行结果:

说明:

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

# 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);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

执行结果:

说明:

  • 在父组件DidMount后执行
  • 调用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);
    })
  }
1
2
3
4
5
6
7
8
  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);
    })
  }
1
2
3
4
5
6
7
8

执行结果:

1;
1;
1
2
2;
2;
1
2

说明:

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

# 二、setState 的执行过程

# 1.流程图

说明:

  • partialState:setState传入的第一个参数,对象或函数
  • _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;
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

我们只需要关注这个代码

_assign(
  nextState,
  typeof partial === "function"
    ? partial.call(inst, nextState, props, context)
    : partial
);
1
2
3
4
5
6

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

Object.assign(
  nextState,
  { index: state.index + 1 },
  { index: state.index + 1 }
);
1
2
3
4
5

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

# 4.componentDidMount调用setstate

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

以上是官方文档的说明,不推荐直接在componentDidMount直接调用setState,由上面的分析:componentDidMount本身处于一次更新中,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。 当然在componentDidMount我们可以调用接口,再回调中去修改state,这是正确的做法。 当 state 初始值依赖dom属性时,在componentDidMount中setState是无法避免的。

# 5.componentWillUpdate componentDidUpdate

这两个生命周期中不能调用setState。 由上面的流程图很容易发现,在它们里面调用setState会造成死循环,导致程序崩溃。

# 6.推荐使用方式

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

#前端#React
上次更新: 2021/10/07, 22:35:22
virtualdom
杀手级js单行代码

← virtualdom 杀手级js单行代码→

最近更新
01
面试官
03-27
02
this&指针&作用域&闭包
03-27
03
前端
03-27
更多文章>
Theme by Vdoing | Copyright © 2019-2025 chst365 | 豫ICP备17031889号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式