浅谈 setState 更新机制

了解 React 同学想必对setState函数是再熟悉不过了,setState也会经常作为面试题,考察前端求职者对 React 的熟悉程度。

在此我也抛一个问题,阅读文章前读者可以先想一下这个问题的答案。

给 React 组件的状态每次设置相同的值,如setState({count: 1})。React 组件是否会发生渲染?如果是,为什么?如果不是,那又为什么?

一、场景复现
针对上述问题,先进行一个简单的复现验证。

如图所示,App 组件有个设置按钮,每次点击设置按钮,都会对当前组件的状态设置相同的值{count: 1},当组件发生渲染时渲染次数会自动累加一,代码如下所示:

App 组件

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 全局变量,用于记录组件渲染次数
let renderTimes = 0;

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }

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

  render() {
    renderTimes += 1;

    return (
      <div>
        <h3>场景复现:</h3>
        <p>每次点击“设置”按钮,当前组件的状态都会被设置成相同的数值。</p>
        <p>当前组件的状态: {this.state.count}</p>
        <p>
          当前组件发生渲染的次数:
          <span style={{ color: 'red' }}>{renderTimes}</span>
        </p>
        <div>
          <button onClick={this.handleClick}>设置</button>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

实际验证结果如下所示,每次点击设置按钮,App 组件均会发生重复渲染。

二、性能优化
那么该如何减少 App 组件发生重复渲染呢?之前在 React 性能优化——浅谈 PureComponent 组件与 memo 组件 一文中,详细介绍了PureComponent的内部实现机制,此处可利用PureComponent组件来减少重复渲染。

实际验证结果如下所示,优化后的 App 组件不再产生重复渲染。

但这有个细节问题,可能大家平时工作中并未想过:

利用 PureComponent 组件可减少 App 组件的重复渲染,那么是否代表 App 组件的状态没有发生变化呢?即引用地址是否依旧是上次地址呢?

废话不多说,我们针对这一问题进行下测试验证,代码如下:

APP 组件

import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

// 全局变量,用于记录组件渲染次数
let renderTimes = 0;
// 全局变量,记录组件的上次状态
let lastState = null;

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
    lastState = this.state; // 初始化,地址保持一致
  }

  handleClick = () => {
    console.log(`当前组件状态是否是上一次状态:${this.state === lastState}`);

    this.setState({ count: 1 });
    // 更新上一次状态
    lastState = this.state;
  };

  render() {
    renderTimes += 1;

    return (
      <div>
        <h3>场景复现:</h3>
        <p>每次点击“设置”按钮,当前组件的状态都会被设置成相同的数值。</p>
        <p>当前组件的状态: {this.state.count}</p>
        <p>
          当前组件发生渲染的次数:
          <span style={{ color: 'red' }}>{renderTimes}</span>
        </p>
        <div>
          <button onClick={this.handleClick}>设置</button>
        </div>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

在 APP 组件中,我们通过全局变量lastState来记录组件的上次状态。当点击设置按钮时,会比较当前组件状态与上一次状态是否相等,即引用地址是否一样?

在 console 窗口中我们发现,虽然 PureComponent组件减少了 App 组件的重复渲染,但是 App 组件状态的引用地址却发生了变化,这是为什么呢?

下面我们将带着这两个疑问,结合 React V16.9.0 源码,聊一聊setState的状态更新机制。解读过程中为了更好的理解源码,会对源码存在部分删减。

三、setState 状态更新机制
在解读源码的过程中,整理了一份函数setState调用关系流程图,如下所示:

从上图可以看出,函数setState调用关系主要分为以下两个部分:

将要更新的状态添加到更新队列中;
产生一个调度任务。调度任务会遍历更新队列并计算出最终要更新的状态,将其更新到组件实例中,然后完成组件渲染操作。
下面针对这两个部分,结合源码,进行下详细阐述。

3.1 入更新队列
3.1.1 setState 函数定义
摘自ReactBaseClasses.js文件。

Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, ‘setState’);
};
1
2
3
函数setState包含两个参数partialState和callback,其中partialState表示待更新的部分状态,callback则为状态更新后的回调函数。

3.1.2 enqueueSetState 函数定义
摘自ReactFiberClassComponent.js文件。

enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);

// 创建一个update对象
const update = createUpdate(expirationTime, suspenseConfig);
// payload存放的是要更新的状态,即partialState
update.payload = payload;

// 如果定义了callback,则将callback挂载在update对象上
if (callback !== undefined && callback !== null) {
update.callback = callback;
}

// …省略…

// 将update对象添加至更新队列中
enqueueUpdate(fiber, update);
// 添加调度任务
scheduleWork(fiber, expirationTime);
},

函数enqueueSetState会创建一个update对象,并将要更新的状态partialState、状态更新后的回调函数callback和渲染的过期时间expirationTime等都会挂载在该对象上。然后将该update对象添加到更新队列中,并且产生一个调度任务。

若组件渲染之前多次调用了setState,则会产生多个update对象,会被依次添加到更新队列中,同时也会产生多个调度任务。

3.1.3 createUpdate 函数定义
摘自 ReactUpdateQueue.js文件。

export function createUpdate(
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
): Update<> {
let update: Update<
> = {
expirationTime,
suspenseConfig,

// 添加TAG标识,表示当前操作是UpdateState,后续会用到。
tag: UpdateState,
payload: null,
callback: null,

next: null,
nextEffect: null,

};

return update;
}

函数createUpdate会创建一个update对象,用于存放更新的状态partialState、状态更新后的回调函数callback和渲染的过期时间expirationTime。

3.2 setState 状态更新机制
从上图可以看出,每次调用setState函数都会创建一个调度任务。然后经过一系列函数调用,最终会调起函数updateClassComponent。

图中红色区域涉及知识点较多,与我们要讨论的状态更新机制关系不大,不是我们此次的讨论重点,所以我们先行跳过,待后续研究(挖坑)。

下面我们就简单聊下组件实例的状态是如何一步步完成更新操作的。

3.2.1 getStateFromUpdate 函数
摘自 ReactUpdateQueue.js文件。

function getStateFromUpdate(
workInProgress: Fiber,
queue: UpdateQueue,
update: Update,
prevState: State,
nextProps: any,
instance: any,
): any {
switch (update.tag) {

// ....省略 ....

// 见3.3节内容,调用setState会创建update对象,其属性tag当时被标记为UpdateState
case UpdateState: {
  // payload 存放的是要更新的状态state
  const payload = update.payload;
  let partialState;

  // 获取要更新的状态
  if (typeof payload === 'function') {
    partialState = payload.call(instance, prevState, nextProps);
  } else {
    partialState = payload;
  }

  // partialState 为null 或者 undefined,则视为未操作,返回上次状态
  if (partialState === null || partialState === undefined) {
    return prevState;
  }

  // 注意:此处通过Object.assign生成一个全新的状态state, state的引用地址发生了变化。
  return Object.assign({}, prevState, partialState);
}

// .... 省略 ....

}

return prevState;
}
getStateFromUpdate 函数主要功能是将存储在更新对象update上的partialState与上一次的prevState进行对象合并,生成一个全新的状态 state。

注意:

Object.assign 第一个参数是空对象,也就是说新的 state 对象的引用地址发生了变化。
Object.assign 进行的是浅拷贝,不是深拷贝。
3.2.2 processUpdateQueue 函数
摘自 ReactUpdateQueue.js文件。

export function processUpdateQueue(
workInProgress: Fiber,
queue: UpdateQueue,
props: any,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
// …省略…

// 获取上次状态prevState
let newBaseState = queue.baseState;

/**

  • 若在render之前多次调用了setState,则会产生多个update对象。这些update对象会以链表的形式存在queue中。
  • 现在对这个更新队列进行依次遍历,并计算出最终要更新的状态state。
    */
    let update = queue.firstUpdate;
    let resultState = newBaseState;
    while (update !== null) {
    // …省略…
/**
 * resultState作为参数prevState传入getStateFromUpdate,然后getStateFromUpdate会合并生成
 * 新的状态再次赋值给resultState。完成整个循环遍历,resultState即为最终要更新的state。
 */
resultState = getStateFromUpdate(
  workInProgress,
  queue,
  update,
  resultState,
  props,
  instance,
);
// ...省略...

// 遍历下一个update对象
update = update.next;

}

// …省略…

// 将处理后的resultState更新到workInProgess上
workInProgress.memoizedState = resultState;
}

React 组件渲染之前,我们通常会多次调用setState,每次调用setState都会产生一个 update 对象。这些 update 对象会以链表的形式存在队列 queue 中。processUpdateQueue函数会对这个队列进行依次遍历,每次遍历会将上一次的prevState与 update 对象的partialState进行合并,当完成所有遍历后,就能算出最终要更新的状态 state,此时会将其存储在 workInProgress 的memoizedState属性上。

3.2.3 updateClassInstance 函数
摘自 ReactFiberClassComponent.js文件。

function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any,
newProps: any,
renderExpirationTime: ExpirationTime,
): boolean {
// 获取当前实例
const instance = workInProgress.stateNode;

// …省略…

const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
let updateQueue = workInProgress.updateQueue;

// 如果更新队列不为空,则处理更新队列,并将最终要更新的state赋值给newState
if (updateQueue !== null) {
processUpdateQueue(
workInProgress,
updateQueue,
newProps,
instance,
renderExpirationTime,
);
newState = workInProgress.memoizedState;
}

// …省略…

/**

  • shouldUpdate用于标识组件是否要进行渲染,其值取决于组件的shouldComponentUpdate生命周期执行结果,
  • 亦或者PureComponent的浅比较的返回结果。
    */
    const shouldUpdate = checkShouldComponentUpdate(
    workInProgress,
    ctor,
    oldProps,
    newProps,
    oldState,
    newState,
    nextContext,
    );

if (shouldUpdate) {
// 如果需要更新,则执行相应的生命周期函数
if (typeof instance.UNSAFE_componentWillUpdate === ‘function’ ||
typeof instance.componentWillUpdate === ‘function’) {
startPhaseTimer(workInProgress, ‘componentWillUpdate’);
if (typeof instance.componentWillUpdate === ‘function’) {
instance.componentWillUpdate(newProps, newState, nextContext);
}
if (typeof instance.UNSAFE_componentWillUpdate === ‘function’) {
instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
}
stopPhaseTimer();
}
// …省略…
}

// …省略…

/**

  • 不管shouldUpdate的值是true还是false,都会更新当前组件实例的props和state的值,
  • 即组件实例的state和props的引用地址发生变化。也就是说即使我们采用PureComponent来减少无用渲染,
  • 但并不代表该组件的state或者props的引用地址没有发生变化!!!
    */
    instance.props = newProps;
    instance.state = newState;

return shouldUpdate;
}

从上述代码可以看出,updateClassInstance函数主要实现了以下几个功能:

遍历更新队列,产生一个全新的 state,并将其更新至组件实例的 state 上;
返回是否要进行更新的标识 shouldUpdate,该值的运行结果取决于shouldComponentUpdate生命周期函数执行结果或者PureComponent的浅比较结果;
如果 shouldUpdate 的值为true,则执行相应生命周期函数componentWillUpdate;
此时要特别注意以下几点:

组件实例的状态 state 发生变化,即引用地址发生变化;
即使采用PureComponent或者shouldComponentUpdate来减少无用渲染,但组件实例的 props 或者 state 的引用地址也依旧发生了变化。
代码解读到此处,想必大家对之前提到的两个疑问都有了答案吧。

3.2.4 updateClassComponent 函数
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime,
) {
// 获取组件实例
const instance = workInProgress.stateNode;

// …省略…

let shouldUpdate;

/**

    1. 完成组件实例的state、props的更新;
    1. componentWillUpdate、shouldComponentUpdate生命周期函数执行完毕;
    1. 获取是否要进行更新的标识shouldUpdate;
      */
      shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderExpirationTime,
      );

/**

    1. 如果shouldUpdate值为false,则退出渲染;
    1. 执行render函数
      */
      const nextUnitOfWork = finishClassComponent(
      current,
      workInProgress,
      Component,
      shouldUpdate,
      hasContext,
      renderExpirationTime,
      );

// 返回下一个任务单元
return nextUnitOfWork;
}

从上述代码可以看出,updateClassComponent函数主要实现了以下几个功能:

完成组件实例的 state、props 的更新;
执行 componentWillUpdate、shouldComponentUpdate等生命周期函数;
完成组件实例的渲染;
返回下一个待处理的任务单元;
四、小结
经过上章的代码解读,相信大家应该对函数setState应该有了全新的认识。之前提到的两个疑问,应该都有了自己的答案。在此我简单小结一下:

每次调用函数setState,react 都会将要更新的状态添加到更新队列中,并产生一个调度任务。调度任务在执行的过程中会做两个事情:

遍历更新队列,计算出全新的状态 state,更新到组件实例中;
根据标识shouldUpdate来决定是否对组件实例进行重新渲染,而标识shouldUpdate的值则取决于PureComponent组件浅比较结果或者生命周期函数shouldComponentUpdate执行结果;
利用PureComponent组件可以减少组件实例的重复渲染,但组件实例的状态由于被赋予了一个全新的状态,所以引用地址发生了变化。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值