深入挖掘React中的state

1,605 阅读8分钟

class组件的状态

针对react中对于FunctionComponet,ClassComponent,DOM节点的基本处理和挂载已经告一段落了。

jsx原理可以查看这篇文章~,接下来我们来讨论讨论Reactclass组件中对于sate的使用,我们会来先讲讲。

  1. state的基础使用。
  2. state遇到的一些"坑"。
  3. state基础原理讨论。
  4. 我们跟随上一节的jsx原理的代码来手把手实现一套state机制。

state基础使用

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

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

state的基础使用这里就不讨论使用代码展开了,基础使用官网有一个Clock例子

state遇到的一些"坑"

react中我们都明白关于setState是用于异步批量更新,可是你真的明白这里的"异步"所谓的是什么意思吗,以及他所谓的批量什么时候才会批量,什么时候又会依次更新呢?接下来我们来看看。

需要注意的是这里的"异步更新",所谓的异步和Promise以及setTimeout这些微/宏任务是无关的。这点我们在后续会讲到,这也是Vue中异步更新策略不同之处。

处于性能的考虑,React可能会将多次setState的更新合并到一个。接下来我们深入去探讨react什么时候会合并多次更新,什么时候并不会合并多次更新。

"问题"分析

基础用法

接下来我们来看这样一个代码:


interface ICountState {
  number: number;
}

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

  // 在事件处理函数中setState的调用会批量异步执行
  handleClick = (event: React.MouseEvent) => {
    // 第一次增加
    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>
    );
  }
}

const element = <Counter></Counter>;

ReactDOM.render(element, document.getElementById('root'));

这段代码中,我们定义了一个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的值,那么react官方支持传入一个callback,它接受一个参数就是上一次传入的值:

	handleClick = (event: React.MouseEvent) => {
		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是两次setState({...})执行完毕之后都是0,而后边打印的0 1是两次callback执行内部打印出来的。第一修改我们发现之前是0,将number=0+1,第二个修改依赖了之前的值,打印1

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

	handleClick = (event: React.MouseEvent) => {
		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
	};
// 依次打印 0 0 1 2

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

浅谈原理

当然提到callback形式的setState,那么我们就来简单谈谈他内部的实现机制:

const state = { number: 0 };

const queue = [];
// 当所有同步代码执行完毕
// 同时当所有setState({...})执行完毕 会执行setState(() => {})

// 我们每次调用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);

console.log(result,'result')

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

"同步更新"

当然上边我们讲到了setState是异步更新,但是我们想要setState实现同步更新,这个时候应该怎么办呢?

我们来看看这段代码:

handleClick = (event: React.MouseEvent) => {
		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下一个宏任务中他是同步更新的。

setState执行机制

对于setState的更新机制,究竟是同步还是异步。也就是所谓是否是批量更新,可以把握这个原则:

  1. 凡是React可以管控的地方,他就是异步批量更新。比如事件函数,生命周期函数中,组件内部同步代码。
  2. 凡是React不能管控的地方,就是同步批量更新。比如setTimeout,setInterval,源生DOM事件中,包括Promise都是同步批量更新。
handleClick = (event: React.MouseEvent) => {
    // 同样是同步更新 微任务同样不属于React管理的范围
		Promise.resolve().then(() => {
			this.setState({ number: this.state.number + 1 });
			console.log(this.state); // 1
			this.setState({ number: this.state.number + 1 });
			console.log(this.state); // 2
		});
	};

简单来聊聊setState的同步异步

上边我们讲到的setState的同步和异步本质上就是批量执行,和js中的异步是完全没有关系的。(这点和Vue大相庭径vue中是通过nextTick - promise - settimeout)。

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

之后我们会详细说到这里的更新机制,同时会尝试自己来实现一个setState机制。

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

同步更新:

// 标记位
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)
}

handleClick()

我们可以清楚的看到内部react是给予isBatchingUpdate这个变量去控制是否是"异步"批量更新。

当然他们的执行机制在17之间react中所有的事件都是委托到body上去处理,所以它会每次都给我们的逻辑添加一些额外的处理(比如我们业务逻辑之中上边的代码和下边的代码)。17之后是所有的事件委托到了root上进行执行的事件,其实是一样的道理。

需要主要的是react中可控的setState无论setState({})或者setState(() => {})都是批量更新的,而不可控的就是非批量更新的。

原理讨论&手动实现

上边我们来讲过了setState/state的用法,当然除了上边的用法setState还支持额外传入一个callback

setState({xxx:1},() => {
    // do some thing
})

这样的写法你可以理解为在所在setState执行完毕后,页面渲染完成之后再去执行callback。(它会在上边说到的两种setState执行完毕后->渲染页面->执行之后的callback)。

原理实现

之后我们会讨论关于reactsetState的处理以及setState/state手动实现。~