深入React中的state,不要再说setState是同步异步了

state基础使用

react中组件的数据来源两个部分,一个是组件自身的state,一个是接受父组件传入的props。这两种状态的改变都会造成视图层面的更新。

当然我们需要注意的是,改变组件内部状态一定是要通过setState进行更新组件内部数据的,直接赋值的话并不会触发页面的更新的。(state是只读的)

每次渲染stateprop都是相互独立的(因为是各自函数作用域内的变量),每次独立渲染函数中的stateprop都是保持不变的常量。

每次改变state/props造成函数组件重新执行,从而每次渲染函数中的state/props都是独立的,固定的。

注意这里的固定独立这两个关键字。

  • 固定表示函数在开始执行时已经确定了本次state/props的值。
  • 独立表示每次运行函数的state/props都是各自独立作用域中的。

函数组件中当使用setState时只有以下两种情况函数组件才会重新执行:

  • 对于值类型,state 的值改变
  • 对于引用类型,state 的引用改变

既然state是只读的,当state是一个具有深嵌套层次的对象时,要避免对其进行浅拷贝(比如展开运算符)再更改数据,这会造成一些意料之外的问题。

关于setState是“同步”还是“异步”

我在这里可是加了双引号的,需要注意的是,在JavaScript中也有同步和异步,它们可是不同的概念。

React中的 “异步更新”,处于性能的考虑,setState会放入到一个更新队列中,会对 state 更新进行批处理。所谓的异步和Promise以及setTimeout这些微/宏任务是无关的。

这也是和Vue中异步更新策略不同之处,在vue3中是通过proxy来代理目标对象,当修改代理对象值的时候会触发对应的set函数从而触发更新运行对应收集的effect进行模版更新。

JavaScript同步

同步操作是指按顺序执行的任务。在一个同步操作中,当前任务必须完成,才能开始下一个任务。这意味着在一个任务完成之前,程序会被阻塞,无法执行其他任务。

JavaScript异步

异步操作允许在等待某个任务完成的同时继续执行其他任务。这样可以避免长时间的阻塞,提高应用程序的响应速度和性能。

细说 setState

代码示例:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0,
    };
  }

  // 在事件处理函数中setState的调用会批量异步执行
  handleClick = () => {
    // 第一次增加
    this.setState({
      number: this.state.number + 1,
    });
    console.log(this.state.number); // 0

    // 第二次增加
    this.setState({
      number: this.state.number + 1,
    });
    console.log(this.state.number); // 0
  };

  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}

这段代码中,定义了一个handleClick点击函数,当点击按钮的时候。在事件处理函数中执行了两次setState,并且每次setState值都依赖于上一次的state

不难想象,最终页面上会渲染出1,因为react是基于异步批量更新原则。当我们点击执行setState时,组件内部的state并没有及时更新,此时this.state.number仍然为0,所以第二次在执行setState(this.state.number + 1)就相当于setState(0+1)

最终react将这两次更新合并为一次执行并且刷新页面,state更新为1,并且页面渲染为1

我们可以看到在事件处理函数中setState方法并不会立即更新state的值,而是会等到事件处理函数结束之后。批量执行setState统一更新state进行页面渲染。

如果要在setState中依赖上一次调用setState的值,那么setState支持传入一个callback,它接受一个参数就是上一次传入的值:

	handleClick = () => {
		this.setState((state) => {
			console.log(state.number, 'number'); // 上一次是0
			return { number: state.number + 1 };
		});
		console.log(this.state.number); // 0

		// 第二次增加
		this.setState((state) => {
			console.log(state.number, 'number'); // 上一次是1
			return { number: state.number + 1 };
		});
		console.log(this.state.number); // 0
	};

打开控制台我们可以发现控制台打印0 0 0 1

同样的道理,这段代码打印0 0 1 2,相信你也能很好的理解

	handleClick = () => {
		this.setState({ number: 1 });

		this.setState((state) => {
			console.log(state.number, 'number'); // 上一次是1
			return { number: state.number + 1 };
		});

		console.log(this.state.number); // 0

		// 第二次增加
		this.setState((state) => {
			console.log(state.number, 'number'); // 上一次是2
			return { number: state.number + 1 };
		});
		console.log(this.state.number); // 0
	};

由此可见当setState传入callback形式时,callback的参数是上一次state修改后的参数。所以可以在这里依赖上一次的state变化作出修改。callback的执行时机你可以将它看成为一种异步,当handleClick同步代码执行完毕后callback依次执行。但是实际他并不是传统意义上的异步。

然后看以下示例代码:

const state = { number: 0 };

const queue = [];

// 我们每次调用setState(() => {}) 其实会将callback推入react一个队列中
queue.push((state) => ({ number: state.number + 1 }));
queue.push((state) => ({ number: state.number + 1 }));

// 最终清空这个队列
const result = queue.reduce((state, action) => {
  return action(state);
}, state);

简单来说就是react内部通过一个queue的队列进行控制,在事件处理函数的结尾去依次清空队列传入上一个值。

但是我们想要setState实现同步更新,这个时候应该怎么办呢?

我们来看看这段代码:

handleClick = () => {
		setTimeout(() => {
			this.setState({ number: this.state.number + 1 });
			console.log(this.state); // 1
			this.setState({ number: this.state.number + 1 });
			console.log(this.state); // 2
		});
	};

当在setTimeout 下一个宏任务中去执行setState的时候,发现 setState是同步执行的。

其实setTimeout函数中并不属于handleClick事件中。它是下一次宏任务,在handleClick事件函数中它是批量的,但是在setTimeout下一个宏任务中他是同步更新的。

所以(仅限于React18版本之前,React18版本之后统一都是批量更新):

凡是React可以管控的地方,他就是批量更新。比如事件函数,生命周期函数中,组件内部同步代码。」

凡是React不能管控的地方,就是单个状态同步更新。比如setTimeout,setInterval,源生DOM事件中,包括Promise都是单个状态同步更新。

浅谈 setState 实现机制

setState的同步和异步本质上就是批量执行,和js中的异步是完全没有关系。(这点和Vue不一样,Vue中是通过nextTick - promise - setTimeout)。

React中的异步其实是内部通过一个变量来控制是否是同步或者异步,从而进行批量/单个更新。

先来简单看看他的原理吧。

同步单个状态更新:

// 标记位
let isBatchingUpdate = false;

let state = { number: 0 };

function setState(newState) {
  return { ...state, ...newState };
}

// 这样的话 内部就是同步的了 每次调用setState 
// 就会及时更新State的值
setState({ number:1 });
setState({ number:2 });

批量更新:

let isBatchingUpdate = false;

let queue = [];

let state = { number: 0 };

function setState(newState) {
  if (isBatchingUpdate) {
    // 批量更新 进入队列
    queue.push(newState);
  } else {
    // 否则直接更新
    return { ...state, ...newState };
  }
}

// 这样的话 内部就是同步的了 每次调用setState
// 就会及时更新State的值
setState({ number: 1 });
setState({ number: 2 });

// 在事件函数处理结尾 批量执行queue中的setState 
const result = queue.reduce((preState,newState) => {
  return { ...preState,...newState }
},state)

实质上可以理解成为每次事件处理函数handleClick执行前, react会重置标记位isBatchingUpdatetrue,表示可以进行异步批量更新。 结束之后再给关闭isBatchingUpdate变为false进行异步更新。

// 标记位

let isBatchingUpdate = false;

let queue = [];

let state = { number: 0 };

function setState(newState) {
  if (isBatchingUpdate) {
    // 批量更新 进入队列
    queue.push(newState);
  } else {
    // 否则直接更新
    return { ...state, ...newState };
  }
}

function handleClick() {
  ...
  // React会在每次函数执行前进行一次封装调用
  isBatchingUpdate = true

  // 我们在React中书写的业务逻辑函数
  setState({ number: 1 }); // 批量打印 0
  setState({ number: 2 }); // 批量打印 0
  // 我们自己书写逻辑结束

  // 同样React也会在我们逻辑结尾进行一次封装调用
  isBatchingUpdate = false
  ... // 比如清空队列
  // 在事件函数处理结尾 批量清空queue中的setState更新 
  state = queue.reduce((preState,newState) => {
    return { ...preState,...newState }
  },state)

  queue = [];
}

handleClick()

所以React内部是通过isBatchingUpdate这个变量去控制是否是"异步"批量更新。

总结

React中setState的"同步和异步"本质上就是状态单个/批量执行,和js中的同步异步是完全没有关系的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值