「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
React 中,class组件创建更新的方式有三种,分别是 ReactDOM.render、setState、forceUpdate。在《React 源码解读之 ReactDOM.render》已对 ReactDOM.render 做了介绍,本文将介绍setState和forceUpdate这两种创建更新的方式。
setState
当我们在class组件中发起一个更新时,通常会调用 setState 方法。setState 是触发组件更新的主要方式,它会将组件 state 的更改加入到更新队列中,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。
setState 的定义在React.Component 中:
// react/src/ReactBaseClasses.js
// 在原型上添加 setState 方法,使用 setState 来更新组件
// partialState 要更新的 state,可以是 Object/Function
// callback 组件更新完成后要执行的回调函数 setState({}, callback)
Component.prototype.setState = function(partialState, callback) {
// 判断 partialState 是否符合条件,如果不符合则抛出 Error
if (
typeof partialState !== 'object' &&
typeof partialState !== 'function' &&
partialState != null
) {
throw new Error(
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
}
// state 的更新机制,在 react-dom 中实现
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码
可以看到,setState 的第一个参数必须是对象或者是函数,并且不能为空,否则就会报错。然后调用 enqueueSetState 初始化 setState 。
enqueueSetState
enqueueSetState 的作用,就是在调用 setState 时,给fiber对象创建一个 update,然后将该update对象添加到updateQueue中,并进入任务调度流程。
// react-reconciler/src/ReactFiberClassComponent.new.js
// inst 即调用 this.setState 时传递进来的this,也就是 class组件实例
// payload 即调用 this.setState 时传递进来的初始state,
// 可以是一个Object对象,也可以是一个function,该function返回一个Object对象
// callback 即调用 this.setState 时传递进来的 回调函数,该回调函数是可选的
// 如果我们想在调用 setState 后立即读取 this.state,就可以使用在 callback 中获取
enqueueSetState(inst, payload, callback) {
// 通过 class组件实例获取 fiber 对象
// this._reactInternals 在 this 上通过 _reactInternals 属性存储fiber对象
const fiber = getInstance(inst);
// 获取当前时间,通过 performance.now() 或 Date.now() 获取的秒数
const eventTime = requestEventTime();
// 创建一个优先级变量(lane模型,通常称为车道模型)
const lane = requestUpdateLane(fiber);
// 创建一个 update 对象
const update = createUpdate(eventTime, lane);
// 将 stateState 传进来的要更新的对象添加到 update 上
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
// 将 setState 的第二个参数 callback 添加到 update 对象上
update.callback = callback;
}
// 将新建的 update 添加到 update链表中
enqueueUpdate(fiber, update, lane);
// 进入任务调度流程
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
// ...
},
复制代码
-
在 enqueueSetState 中,首先通过 getInstance 方法获取存储在ClassComponent实例上的fiber对象。
// 通过 class组件实例获取 fiber 对象 // this._reactInternals 在 this 上通过 _reactInternals 属性存储fiber对象 const fiber = getInstance(inst); 复制代码
getInstance 方法源码如下:
// shared/ReactInstanceMap.js
export function get(key) {
return key._reactInternals;
}
复制代码
下面是打印出来的ClassComponent的 this,可以看到,在 this 上通过 _reactInternals 属性存储着fiber对象。
-
然后计算当前时间和更新的优先级,根据当前时间和更新优先级创建一个新的update对象,并将setState传递进来的要更新的state对象和callback添加到该update对象上:
// 获取当前时间,通过 performance.now() 或 Date.now() 获取的秒数 const eventTime = requestEventTime(); // 创建一个优先级变量(lane模型,通常称为车道模型) const lane = requestUpdateLane(fiber); // 创建一个 update 对象 const update = createUpdate(eventTime, lane); // 将 stateState 传进来的要更新的state对象添加到 update 上 update.payload = payload; if (callback !== undefined && callback !== null) { // ... // 将 setState 的第二个参数 callback 添加到 update 对象上 update.callback = callback; } 复制代码
-
接下来将update对象添加到updateQueue中,updateQueue是一个环形链表:
// 将新建的 update 添加到 update链表中 enqueueUpdate(fiber, update, lane); 复制代码
-
最后调用scheduleUpdateOnFiber方法,进入任务调度流程:
// 进入任务调度流程 const root = scheduleUpdateOnFiber(fiber, lane, eventTime); 复制代码
forceUpdate
forceUpdate 将会强制让组件重新渲染。当有些变量不在 state 上,但是又想要这个变量更新;或者 state 里的某个变量层次太深,更新的时候没有自动触发render。这些时候我们可以手动调用forceUpdate强制触发render。
forceUpdate 的定义在React.Component 中:
// react/src/ReactBaseClasses.js
// 在Component的深层次改变但未调用setState时,使用该方法,强制Component更新一次,无论 props/state 是否更新
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
复制代码
可以看到,forceUpdate 调用的是 enqueueForceUpdate 来触发render。
enqueueForceUpdate
enqueueForceUpdate 的作用,就是在调用 forceUpdate 时,给fiber对象创建一个 update,然后将该update对象添加到updateQueue中,并进入任务调度流程。
// react-reconciler/src/ReactFiberClassComponent.new.js
enqueueForceUpdate(inst, callback) {
// 通过 class组件实例获取 fiber 对象
// this._reactInternals 在 this 上通过 _reactInternals 属性存储fiber对象
const fiber = getInstance(inst);
// 获取当前时间,通过 performance.now() 或 Date.now() 获取的秒数
const eventTime = requestEventTime();
// 创建一个优先级变量(lane模型,通常称为车道模型)
const lane = requestUpdateLane(fiber);
// 创建一个 update 对象
const update = createUpdate(eventTime, lane);
// 与 setState 不同的地方
// update.tag 默认为 0,即更新,将其改成 2 ,即需要强制更新
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
// 将 setState 的第二个参数 callback 添加到 update 对象上
update.callback = callback;
}
// 将新建的 update 添加到 update链表中
enqueueUpdate(fiber, update, lane);
// 进入任务调度流程
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
// ...
},
复制代码
enqueueForceUpdate 方法的流程与enqueueSetState类似,唯一的不同是修改了属性 tag 的值:
// 与 setState 不同的地方
// update.tag 默认为 0,即更新,将其改成 2 ,即需要强制更新
update.tag = ForceUpdate;
复制代码
在创建update对象时,tag属性的值默认是 0 ,即更新,在执行forceUpdate时,需要将tag属性的值改为 2 ,即需要强制更新。
在 createUpdate 方法中可以看到,tag属性的初始值是 UpdateState:
//react-reconciler/src/ReactUpdateQueue.new.js
export function createUpdate(eventTime: number, lane: Lane): Update<*> {
const update: Update<*> = {
eventTime,
lane,
// export const UpdateState = 0; // 更新
// export const ReplaceState = 1; // 替换
// export const ForceUpdate = 2; // 强制更新
// export const CaptureUpdate = 3; // 捕获性更新
tag: UpdateState, // tag属性的初始值是 UpdateState
payload: null,
callback: null,
next: null,
};
return update;
}
复制代码
由于是forceUpdate触发的更新,因此需要将tag改成ForceUpdate,以便 React 进行Update 的优先级排序。
总结
setState 是执行组件更新的主要方式,forceUpdate 是强制让组件执行更新,两者的更新流程都相似:
-
获取ClassComponent的this对象上的fiber对象
-
计算当前时间和更新优先级
-
根据当前时间和更新优先级创建update对象
-
将setState中要更新的state对象和要执行的callback添加到update对象上
-
将当前update对象加入到updateQueue队列中
-
进入任务调度流程
两者唯一的不同是执行forceUpdate时 update对象的tag属性值更改成了 ForceUpdate。update对象的tag属性值默认是UpdateState,执行forceUpdate时改成ForceUpdate,便于React进行Update优先级的排序。