react中的setState

setState的使用

1.1. 为什么使用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() {
  }
}

关键是changeText中应该如何实现:

我们是否可以通过直接修改state中的message来修改界面呢?

点击不会有任何反应,为什么呢?
因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
我们必须通过setState来告知React数据已经发生了变化;

 changeText() {
    this.state.message = "你好啊,李银河";
  }

我们必须通过setState来更新数据:

疑惑:在组件中并没有实现setState的方法,为什么可以调用呢?
原因很简单,setState方法是从Component中继承过来的。

Component.prototype.setState = function(partialState, callback) {
  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来修改数据:

当我们调用setState时,会重新执行render函数,根据最新的State来创建ReactElement对象;
再根据最新的ReactElement对象,对DOM进行修改;

changeText() {
  this.setState({
    message: "你好啊,李银河"
  })
}
1.2. setState异步更新

我们来看下面的代码:

最终打印结果是Hello World;
可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果

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

为什么setState设计为异步呢?

setState设计为异步其实之前在GitHub上也有很多的讨论;
React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的可以参考一下;
https://github.com/facebook/react/issues/11527#issuecomment-360199710;
对其回答做一个简单的总结:
setState设计为异步,可以显著的提升性能;如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;state和props不能保持一致性,会在开发中产生很多的问题;
那么如何可以获取到更新后的值呢?

setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
格式如下:setState(partialState, callback)

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

当然,我们也可以在生命周期函数:

componentDidUpdate(prevProps, provState, snapshot) {
  console.log(this.state.message);
}
1.3. setState一定是异步?

疑惑: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是同步;
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);
  
  ...
}

enqueueSetState源码
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;
  }

1.4. setState的合并
1.4.1. 数据的合并

假如我们有这样的数据:

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

我们需要更新message:

我通过setState去修改message,是不会对name产生影响的;

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

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

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

1.4.2. 多个setState合并

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

如果进行如下操作,那么counter会变成几呢?答案是1;
为什么呢?因为它会对多个state进行合并;

 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进行合并时,每次遍历,都会执行一次函数:

传入的函数被多次执行

转载https://mp.weixin.qq.com/s/He3DoAObZafpTN2Yp09N_g

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值