前言:为了搞清楚react到底是个什么样的“框架”以及它的内部机制,因此开始阅读react源码。在阅读源码的时候,为了梳理流程,根据自己的理解以及网上的一些资料做了一些(大致流程的)笔记。笔记是当时的理解,写的时候,仍然有很多不理解的地方待后续完善。而写出来的部分,可能会有很多理解不到位甚至是错误的地方,一旦有新的理解或者发现了错误,会补充与修正。
react源码版本:16.8.6
react调度更新主要有几种方式:ReactDom.render、setState、forceUpdate;
ReactDom.render则是通过调用updateContainer去执行更新;后两者则分别通过调用enqueueSetState、enqueueForceUpdate去更新。这三个函数很相似,所以只要看其中一个就可以了。
看一下updateContainer函数源码:
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot, // container, 通过render传过来的FiberRoot实例对象
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
const current = container.current; // Fiber实例对象
const currentTime = requestCurrentTime(); // 获取currentTime
const expirationTime = computeExpirationForFiber(currentTime, current);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback,
);
}
可以看到它做了这几件事:
① 获取了Fiber对象;
② 计算了currentTime;
③ 计算expirationTime;
④ 调用updateContainerAtExpirationTime并取它的返回值返回;
获取currentTime调用了requestCurrentTime这个函数,看一下这个函数的代码:
代码路径:packages/react-reconciler/src/ReactFiberScheduler.js
// Expiration times are computed by adding to the current time (the start
// time). However, if two updates are scheduled within the same event, we
// should treat their start times as simultaneous, even if the actual clock
// time has advanced between the first and second call.
// In other words, because expiration times determine how updates are batched,
// we want all updates of like priority that occur within the same event to
// receive the same expiration time. Otherwise we get tearing.
let currentEventTime: ExpirationTime = NoWork;
export function requestCurrentTime() {
if (workPhase === RenderPhase || workPhase === CommitPhase) {
// We're inside React, so it's fine to read the actual time.
return msToExpirationTime(now());
}
// We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoWork) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
}
// This is the first update since React yielded. Compute a new start time.
currentEventTime = msToExpirationTime(now());
return currentEventTime;
}
这里面有三种情况:
然后看一下是怎么计算开始时间的,这里用了一个转化函数msToExpirationTime,将毫秒转化为expirationTime,函数参数是now(),看一下这个now是什么,跳转到:packages/react-reconciler/src/SchedulerWithReactIntegration.js
let initialTimeMs: number = Scheduler_now();
// If the initial timestamp is reasonably small, use Scheduler's `now` directly.
// This will be the case for modern browsers that support `performance.now`. In
// older browsers, Scheduler falls back to `Date.now`, which returns a Unix
// timestamp. In that case, subtract the module initialization time to simulate
// the behavior of performance.now and keep our times small enough to fit
// within 32 bits.
// TODO: Consider lifting this into Scheduler.
export const now =
initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs;
这个initialTimeMs就是模块的初始时间,且初始化了之后就不会变;而这里的Scheduler_now(),会有两种情况,在高版本浏览器中会返回performance.now()的值,在低版本浏览器中会返回Date.now()的值;前者表示从开始时间到调用它所经过的毫秒数,它精确到微秒;后者表示自1970年1月1日 00:00:00 UTC到当前时间的毫秒数。
这里在取当前时间的时候做了一个限制,如果initialTimeMs小于10秒,那么就直接返回初始时间,如果大于10秒,就重新获取当前时间,并减去初始时间。这么做有两个原因:① 是为了让now的值始终小于Math.pow(2, 30) - 1,该值是最大整数限制(查看maxSigned31BitInt.js);② 在低版本浏览器不支持的情况下,模拟performance.now的行为。因此可以看到,now返回的是毫秒数,表示从应用开始到现在经历了多长时间,而且这个值每次获取都比上一次的数值大;
再来看一下msToExpirationTime这个函数:
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;
// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
这个公式只有一个变量ms,其他都是常量,因此传入的ms越大,msToExpirationTime的返回值越小。
因此通过requestCurrentTime拿到的currentTime只有2种情况:要么比之前大,要么与之前相同。
计算出currentTime之后,就是调用computeExpirationForFiber函数来获取expirationTime了,看一下computeExpirationForFiber的代码:
export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
): ExpirationTime {
if ((fiber.mode & ConcurrentMode) === NoContext) {
return Sync;
}
if (workPhase === RenderPhase) {
// Use whatever time we're already rendering
return renderExpirationTime;
}
// Compute an expiration time based on the Scheduler priority.
let expirationTime;
const priorityLevel = getCurrentPriorityLevel();
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority:
// TODO: Rename this to computeUserBlockingExpiration
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority: // TODO: Handle LowPriority
// TODO: Rename this to... something better.
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Never;
break;
default:
invariant(false, 'Expected a valid priority level');
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
// This is a trick to move this update into a separate batch
expirationTime -= 1;
}
return expirationTime;
}
这里面有好几个分支,根据workPhase以及优先级分别返回不同的结果。这里面需要看的是computeInteractiveExpiration和computeAsyncExpiration这两个函数,作用分别是计算交互事件的expirationTime和异步任务的expirationTime,来看一下:
代码路径:packages/react-reconciler/src/ReactFiberExpirationTime.js
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
可以看到,这两个函数的逻辑其实类似,所以只分析一个。
computeAsyncExpiration函数里面又调用了computeExpirationBucket函数,传入上面计算出来的currentTime以及两个常量。computeExpirationBucket的返回值比较复杂,可以简化一下:
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
// 简化
return 1073741822 - ceiling((1073741822 - currentTime + 500), 25);
//进一步简化
1073741822 - ((((1073741822 - currentTime + 500)/ 25) | 0) + 1) * 25;
| 0 的意思是取整(不作四舍五入),所以整个返回值的意思是:取得(1073741822 - currentTime + 500)/ 25(简称A值)的整数部分再加1,最后乘以25。这就有了一个问题,为什么要取整。react这么做,其实是为了合并更新任务,提高效率。如果在一个地方多次调用setState,那么他们的currentTime虽然可能极其接近,但是也不相同,于是计算出来的expirationTime也不同,就要进行不同的更新任务,那么效率就会变低,于是react就让合并比较时间比较接近的更新任务,即:让它们的expirationTime保持一致。
这里可以看到A值的小数部分被去掉了,可以这样说,它除以25之后的余数部分是小于25的。可以假设某一个A值,刚好它可以整除25,那么从A到A+24之间的数,取整拿到的结果都是一样的,因此((((1073741822 - currentTime + 500)/ 25) | 0) + 1) * 25得到的结果也是一样的。这就大大减少了更新任务的触发,也大大提高了效率。
再回到计算不同expirationTime的函数computeExpirationForFiber里,可以看到一共有这几种expirationTime:
a. 0(NoWork)
b. 1(Never)
c. 1073741823(Sync)
d. computeInteractiveExpiration计算结果
e. computeAsyncExpiration计算结果
f. 当前的expirationTime-1
其中d跟e两个依赖于currentTime的变化,currentTime越大,expirationTime越大。
计算完expirationTime,再就是调用updateContainerAtExpirationTime,它主要的流程就是在函数里面调用scheduleRootUpdate,看一下它的源码,这里隐去了一些提示代码:
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime, // expirationTime就是之前计算得到的结果
callback: ?Function,
) {
// createUpdate返回值是一个对象,部分属性:
// expirationTime: expirationTime,
// tag: 0 | 1 | 2| 3, 指定的更新类型
// payload: null, 承载的参数,即更新内容
// callback: null, 更新之后对应的回调函数,如render,setState对应的回调
const update = createUpdate(expirationTime);
// Caution: React DevTools currently depends on this property
// being called "element".
// 第一次渲染,payload为element
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
flushPassiveEffects();
// 将更新放入对应Fiber的队列
enqueueUpdate(current, update);
// 调度
scheduleWork(current, expirationTime);
return expirationTime;
}
enqueueUpdate代码(添加了注释):
// 这个函数有两个大判断
// 前面一个主要是执行createUpdateQueue或者cloneUpdateQueue
// 后面一个判断主要是执行appendUpdateToQueue
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// Update queues are created lazily.
const alternate = fiber.alternante;
let queue1;
let queue2;
if (alternate === null) {
// 初次执行渲染
// There's only one fiber.
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
// 创建queue
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// 已经渲染更新过至少一次了
// else部分主要是判断Fiber对象的updateQueue与alternate对象下的updateQueue对象是否为空
// 如果都为空,则分别创建,如果有一个为空,则克隆不为空的那一个,如果都不为空,则不做处理
// There are two owners.
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// Neither fiber has an update queue. Create new ones.
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(
alternate.memoizedState,
);
} else {
// Only one fiber has an update queue. Clone to create a new one.
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// Only one fiber has an update queue. Clone to create a new one.
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// Both owners have an update queue.
}
}
}
// 这部分判断的作用主要是将update挂到queue1和queue2中
if (queue2 === null || queue1 === queue2) {
// There's only a single queue.
appendUpdateToQueue(queue1, update);
} else {
// There are two queues. We need to append the update to both queues,
// while accounting for the persistent structure of the list — we don't
// want the same update to be added multiple times.
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// One of the queues is not empty. We must add the update to both queues.
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// Both queues are non-empty. The last update is the same in both lists,
// because of structural sharing. So, only append to one of the lists.
appendUpdateToQueue(queue1, update);
// But we still need to update the `lastUpdate` pointer of queue2.
queue2.lastUpdate = update;
}
}
}
前面的工作都做好了,接下来开始调度…
/scheduleWork
React源码解析笔记—调度更新(二)