三、调度Scheduler
scheduling(调度)是fiber reconciliation的一个过程,主要决定应该在何时做什么?在stack reconciler中,reconciliation是“一气呵成”,对于函数来说,这没什么问题,因为我们只想要函数的运行结果,但对于UI来说还需要考虑以下问题:
并不是所有的state更新都需要立即显示出来,比如屏幕之外的部分的更新;
并不是所有的更新优先级都是一样的,比如用户输入的响应优先级要比通过请求填充内容的响应优先级更高;
理想情况下,对于某些高优先级的操作,应该是可以打断低优先级的操作执行的,比如用户输入时,页面的某个评论还在reconciliation,应该优先响应用户输入。
比如18版本里concurrent 模式 (默认情况下未启用) 提示一些不安全的生命周期主要是它被打断了可能会被执行多次。如:
总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务。
低优先级任务由requestIdleCallback处理;
高优先级任务,如动画相关的由requestAnimationFrame处理;
requestIdleCallback可以在多个空闲期调用空闲期回调,执行任务;
requestIdleCallback方法提供deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;
但是由于requestIdleCallback有以下两个问题就采用了messageChannel模拟实现了requestIdleCallback。
1)兼容性;
2)50ms 渲染问题;(可能在一些任务很长时这个回调不会执行)
|— task queue —|— micro task —|— raf —|— render —|— requestIdleCallback – -|
requestIdleCallback是宏任务,messageChannel也宏任务。
为什么没有? generator ?因为它是有状态的,无法从中间中断。
为什么没有? setTimeout ?因为setTimeout有4-5ms的延时。
模拟了requestIdleCallback行为:
/**
* schedule —> 把我的任务放进一个队列里,然后以某一种节奏进行执行;
*
*/
// task 的任务队列
const queue = [];
const threshold = 1000 / 60;
const transtions = [];
let deadline = 0;
// 获取当前时间, bi date-now 精确
const now = () => performance.now(); // 时间 ,精确
// 从任务queue中,选择第一个 任务
const peek = arr => arr.length === 0 ? null : arr[0];
// schedule —> 把我的任务放进一个队列里,然后以某一种节奏进行执行;
export function schedule (cb) {
queue.push(cb);
startTranstion(flush);
}
// 此时,是否应该交出执行权
function shouldYield() {
return navigator.scheduling.isInputPending() || now() >= deadline;
}
// 执行权的切换
function startTranstion(cb) {
transtions.push(cb) && postMessage();
}
// 执行权的切换
const postMessage = (() => {
const cb = () => transtions.splice(0, 1).forEach(c => c());
const {
port1, port2 } = new MessageChannel();
port1.onmessage = cb;
return () => port2.postMessage(null);
})()
// 模拟实现 requestIdleCallback 方法
function flush() {
// 生成时间,用于判断
deadline = now() + threshold;
let task = peek(queue);
// 我还没有超出 16.666ms 同时,也没有更高的优先级打断我
while(task && !shouldYield()) {
const {
cb } = task;
const next = cb();
// 相当于有一个约定,如果,你这个task 返回的是一个函数,那下一次,就从你这里接着跑
// 那如果 task 返回的不是函数,说明已经跑完了。不需要再从你这里跑了
if(next && typeof next === "function") {
task.cb = next;
} else {
queue.shift()
}
task = peek(queue);
}
// 如果我的这一个时间片,执行完了,到了这里。
task && startTranstion(flush)
}
一旦reconciliation过程得到时间片,就开始进入work loop。work loop机制可以让react在计算状态和等待状态之间进行切换。为了达到这个目的,对于每个loop而言,需要追踪两个东西:下一个工作单元(下一个待处理的fiber);当前还能占用主线程的时间。第一个loop,下一个待处理单元为根节点。
每个工作单元(fiber)执行完成后,都会查看是否还继续拥有主线程时间片,如果有继续下一个,如果没有则先处理其他高优先级事务,等主线程空闲下来继续执行
react17版本有时间切片ric,但是没有使用。18版本里才使用了。
宏任务微任务执行示例
四、diff算法
react diff算法最好时是O(n), 最差的话,是 O(mn),而传统的diff算法是O(n^3)。
react 是如何将 diff 算法的复杂度降下来的?
其实就是在算法复杂度、虚拟 dom 渲染机制、性能中找了?个平衡,react 采?了启发式的算法,做了如下最优假设:
a. 如果节点类型相同,那么以该节点为根节点的 tree 结构,?概率是相同的,所以如果类型不同,可以直接「删除」原节点,「插?」新节点;
b. 跨层级移动? tree 结构的情况?较少?,或者可以培养?户使?习惯来规避这种情况,遇到这种情况同样是采?先「删除」再「插?」的?式,这样就避免了跨层级移动
c. 同?层级的?元素,可以通过 key 来缓存实例,然后根据算法采取「插?」「删除」「移动」的操作,尽量复?,减少性能开销
d. 完全相同的节点,其虚拟 dom 也是完全?致的;
react为什么不去优化diff算法?
因为新版本下,diff算法不是约束性能瓶颈的问题了。
为什么要有key?
在?较时,会以 key 和 type 是否相同进??较,如果相同,则直接复制
vue diff算法和react diff算法相同/不同点:
共同点:
vue和diff算法,都是不进行跨层级比较,只做同级比较
不同点:
1.vue进行diff时,调用patch打补丁函数,一边比较一边给真实的dom打补丁,vue对比节点时,当节点元素类型相同,类名不同时,认为是不同的元素,删除重新创建,而react认为是同类型的节点,进行修改操作
2.vue列表对比的时候,采用从两端到中间的方式,旧集合和新集合两端各存在两个指针,两两进行比较,每次对比结束后,指针向队列中间移动;react则是从左往右一次对比,利用元素的index和lastindex进行比较
3.当一个集合把最后一个节点移动到最前面,react会把前面的节点依次向后移动,而Vue只会把最后一个节点放在最前面,这样的操作来看,Vue的diff性能是高于react的。
四、模拟实现react流程
react.js
const normalize = (children = []) => children.map(child => typeof child === 'string' ? createVText(child): child)
export const NODE_FLAG = {
EL: 1, // 元素 element