# setState进阶分析

学习途径coderwhy老师

# 【基本使用】

我们来看一个简单的使用setState的例子:

import React, { Component } from 'react'

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: "Hello World"
    }
  }
  render() {
    return (<div>
        <h2>{this.state.message}</h2>
        <button onClick={e => this.changeText()}>改变文本</button>
</div>)
  }

  changeText() {
    this.setState({
      message:'你好啊,李银河'
    })
  }
}

这里的setState方法和stateprops一样的都是从ReactComponent继承过来的。

下面是React中的源码

//Component的原型上的方法setState()
Component.prototype.setState = function(partialState, callback) {
  //这个方法中可以传入两个参数
  //1.参数一:对象||function||null
  //2.参数二:一个回调函数,这个回调函数会在视图更新后执行
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

格式如下:setState(partialState, callback)

setState接受两个参数

  • 第一个参数是:需要更新到的状态,一般是个对象{ }
  • 第二个参数是:一个回调函数,这个回调函数会在state合并且视图更新后会执行;

也可以这样巧用setState:

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

# 【异步更新】

state={
  message:'Hello World'
}
changeText() {
  this.setState({
    message: "你好啊,李银河"
  })
  console.log(this.state.message); // Hello World
}

上面代码中,我们在setState之后,立刻获取state中的对应的值,并不能立刻获取到更新后的数据

可见setState异步的操作。而且setState的异步还不是一般的异步操作,是react专门通过算法实现的,使用一段代码中setState多次更新同一项属性,只会被执行一次。

# 为什么setState设计为异步呢?

  • setState设计为异步,可以显著的提升性能

    • 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;(这里有前端框架最初的设计时的思想,为了提高性能,减少操作dom的次数,通过算法计算少次多量地进行操作dom)
  • 如果同步更新了state,但是还没有执行render函数,那么**stateprops不能保持同步**;

    • stateprops不能保持一致性,会在开发中产生很多的问题;
    • props是在render函数是执行的时候传入组件中)

# 那么如何可以获取到更新后的值呢?

# 1】方式一:

使用setState()第二个参数(回调函数中获取)

changeText() {
  this.setState({
    message: "你好啊,李银河"
  }, () => {//此回调函数会在视图更新的时候被调用执行
    console.log(this.state.message); // 你好啊,李银河
  });
}

# 2】方式二:

在组件生命周期中进行获取

componentDidUpdate(prevProps, provState, snapshot) {
  //渲染更新之后进行获取。
  console.log(this.state.message);
}

# 【非异步情况】

疑惑:setState一定是异步更新的吗?

# 验证一:

setTimeout中的更新

changeText() {
  setTimeout(() => {
    this.setState({
      message: "你好啊,李银河"
    });
    console.log(this.state.message); // 你好啊,李银河
  }, 0);
}

# 验证二:

原生DOM事件

componentDidMount() {
  const btnEl = document.getElementById("btn");
  btnEl.addEventListener('click', () => {
    this.setState({
      message: "你好啊,李银河"
    });
    console.log(this.state.message); // 你好啊,李银河
  })
}

# 其实分成两种情况:

  • 在组件生命周期或React合成事件***中,setState是***异步;(异步操作,但特定情况下只会执行一次,性能被优化)
  • setTimeout或者原生dom事件***中,setState是***同步;(完全就是同步函数)

# 复习一下事件循环event loop

setTimeout()和原生dom事件都是属于js线程中的异步操作

  • 这两个函数里面传入的回调函数都会在相应的时候被加入到消息队列
  • 然后代码中同步函数执行完的时候再被调用执行

# 源码剖析:

React中其实是通过一个函数来确定异步还是同步的:enqueueSetState部分实现(react-reconciler/ReactFiberClassComponent.js

enqueueSetState(inst, payload, callback) {
  const fiber = getInstance(inst);
  // 会根据React上下文计算一个当前时间
  const currentTime = requestCurrentTimeForUpdate();
  const suspenseConfig = requestCurrentSuspenseConfig();
  // 这个函数会返回当前是同步还是异步更新(准确的说是优先级)
  const expirationTime = computeExpirationForFiber(
    currentTime,
    fiber,
    suspenseConfig,
  );

  const update = createUpdate(expirationTime, suspenseConfig);
  
  ...
}

computeExpirationForFiber函数的部分实现:

  • Sync是优先级最高的,即创建就更新;
  currentTime: ExpirationTime,
  fiber: Fiber,
  suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
  const mode = fiber.mode;
  if ((mode & BlockingMode) === NoMode) {
    return Sync;
  }

  const priorityLevel = getCurrentPriorityLevel();
  if ((mode & ConcurrentMode) === NoMode) {
    return priorityLevel === ImmediatePriority ? Sync : Batched;
  }

# 【setState合并】

# 数据的合并:

假如我们有这样的数据:

this.state = {
  name: "rayhomie",
  message: "Hello World"
}

我们需要更新message:

  • 我通过setState去修改message,是不会对name产生影响的;
changeText() {
  this.setState({
    message: "你好啊,李银河"
  });
}

为什么不会产生影响呢?源码中其实是有对原对象新对象进行合并的:

  • 事实上就是使用 Object.assign(target, ...sources) 来完成的;

# 多个setState合并:

# 1】多次使用setState更新state中相同属性:

比如我们还是有一个counter属性,记录当前的数字:

  • 如果进行如下操作,那么counter会变成几呢?答案是1;
  • 为什么呢?因为它会对多个state进行合并
state={
  counter:0
}  
increment() {
    this.setState({
      counter: this.state.counter + 1
    });

    this.setState({
      counter: this.state.counter + 1
    });

    this.setState({
      counter: this.state.counter + 1
    });
  }

其实在源码的processUpdateQueue中有一个do...while循环,就是从队列中取出多个state进行合并的;

# 如何可以做到,让counter最终变成3呢?
increment() {
  this.setState((state, props) => {
    return {
      counter: state.counter + 1
    }
  })

  this.setState((state, props) => {
    return {
      counter: state.counter + 1
    }
  })

  this.setState((state, props) => {
    return {
      counter: state.counter + 1
    }
  })
  }

为什么传入一个回调函数就可以变出3呢?

原因是:多个state进行合并时,每次遍历,都会执行一次函数;(也就是说,只要***使用了setState就会肯定会执行他里面的回调函数***)

Last Updated: 9/6/2020, 3:35:11 PM