第一部分 react架构
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
Scheduler(调度器)
-
Scheduler
首先是来做任务调度的所有的任务在一个调度的生命周期 内部 都有一个过期时间,与调度的优先级 ,但是 调度的优先级最后也会转化为过期时间是不过是任务过期时间是有长有短的 ,过期时间越短代表 越饥饿 ,优先级也就是越高,,但如果已经过期了的任务也会被认为是饥饿任务, -
时间切片原理
时间切片原理 模拟的是 浏览器的APi requestIdleCallback ,
[link] https://juejin.cn/post/6883282239182864397 --讲解 javascript 宏任务和微任务
一个task(宏任务) – 队列中全部job(微任务) – requestAnimationFrame – 浏览器重排/重绘 – requestIdleCallback
requestIdleCallback 是在 “浏览器重绘/重排” 后 若是 当前帧还有空余时间,浏览器并没有提供其他API能够在同样的时机(浏览器重排/重绘后)调用以模拟其实现。唯一能精准控制调用时机的API是requestAnimationFrame,他能让我们在“浏览器重排/重绘”之前执行JS。这也是为什么我们通常用这个API实现JS动画 —— 这是浏览器渲染前的最后时机,所以动画能快速被渲染。 即 Scheduler 的切片时间 是通过
宏任务 task实现、但是在react中 Scheduler 将需要被执行的函数作为MessageChannel 的回调执行,
若当宿主环境下 不支持MessageChannel ,则使用setTimeout 在react的render阶段 ,开启Concurrent mode 时 ,则会通过 Scheduler 提供的shouldYield 方法来判断是否需要中断遍历function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()){ performUnitOfWork(workInProgress); } } 是否中断渲染 ,主要是看每一个任务的剩余时间 是否用完。在 Scheduler 中为初始剩余的时间定义 为5ms 源码位置 ==packages/scheduler/src/forks/SchedulerPostTask.js==
-
首先在react中 Scheduler 是独立的包所以它的优先级也是独立于react 的 在Scheduler源码中对外暴露了一个方法unstable_runWithPriority 方法接受两个参数* 优先级参数,回调函数
ImmediatePriority
UserBlockingPriority
NormalPriority
LowPriority
IdlePriority
在scheduler 内部可以看出有5种优先级。凡事涉及到优先级调度的地方,都会使用unstable_runWithPriority方法 -
优先级的意义
在packages/scheduler/src/Scheduler.js—279 行 看出 unstable_scheduleCallback 是对外暴露出的重要方法。此方法是用于某个优先级的注册回调函数 -
不同优先级的任务的排序
优先级意味着任务的过期时间,在一个大型react项目,在某种时间存在 许多的优先级 和任务 对应不同的过期时间。且任务分为 已就绪任务 未就绪任务
在scheduler 存在两个队列:
timerQueue : 保存未就绪的任务
taskQueue : 保存已就绪的任务
1.每当有新的未就绪的任务被注册,我们将其插入 timerQueue 并且根据开始时间重新排列 timerQueue 中的任务顺序。
当timerQueue中有任务就绪,即startTime <= currentTime 我们就将其取出并且加入taskQueue 为了能在O(1) 复杂度找到两个队列中时间最早的那个任务,scheduler使用小顶堆 实现了优先队列。
packages/scheduler/src/SchedulerMinHeap.js —优先队列实现的位置
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
// react 中优先队列的实现方法
type Heap = Array<Node>;
type Node = {|
id: number,
sortIndex: number,
|};
export function push(heap: Heap, node: Node): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
export function peek(heap: Heap): Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
export function pop(heap: Heap): Node | null {
const first = heap[0];
if (first !== undefined) {
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null;
}
}
function siftUp(heap, node, i) {
let index = i;
while (true) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (parent !== undefined && compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// If the left or right node is smaller, swap with the smaller of those.
if (left !== undefined && compare(left, node) < 0) {
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
在学习 scheduler ,就可以理解时间切片 当shouldYield 为true ,performUnitOfWork被中断后是如何重新启动的
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// continuationCallback是函数
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
// 将当前任务清除
pop(taskQueue);
}
}
advanceTimers(currentTime)
当注册时间的回调 函数返回值 continuationCallback 为function ,会将continuationCallback作为当前任务的回调函数
// 源码中调度的处理
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
双缓存机制(react-fiber)
-
当我们使用canvas 绘制动画 ,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏闪烁
需要在内存中 绘制当前帧的画面 绘制完毕之后直接用当前帧来代替前一帧的画,用于省去两帧替换的计算时间 就会消除白屏的时间
这种在内存中构建并且替换的技术 则为双缓存技术。
双缓存fiber 树
在 react中最多同时会存在两课fiber树。当前展示的fiber-Dom树 —— current-fiber树 正在内存中构建的fiber树 —— workInProgress -fiber
current-fiber树的fiber节点 称之为 current-fiber,
workInProgress -fiber的fiber节点称之为workInProgress -fiber,它们节点 之间有 alternate (指针)属性连接。currentFiber.alternate === workInProgressFiber; workInProgressFiber.alternate === currentFiber fiberRoot 表示 Fiber 数据结构对象,是 Fiber 数据结构中的最外层对象 rootFiber 表示组件挂载点对应的 Fiber 对象,比如 React 应用中默认的组件挂载点就是 id 为 root 的 div fiberRoot 包含 rootFiber,在 fiberRoot 对象中有一个 current 属性,存储 rootFiber rootFiber 指向 fiberRoot,在 rootFiber 对象中有一个 stateNode 属性,指向 fiberRoot 在 React 应用中 FiberRoot 只有一个,而 rootFiber 可以有多个,因为 render 方法是可以调用多次的 fiberRoot 会记录应用的更新信息,比如协调器在完成工作后,会将工作成果存储在 fiberRoot 中
react 根节点 通过current节点指针在不同的Fiber树的rootFiber间来切换Fiber树的切换
-
具体学习一下 组件挂载mount 和 组件更新 update时构建/替换
mount 阶段{function App() { const [num,add] = useState(0); return ( <p onclick={() =>add(num + 1)}>{num}<p> ) } ReactDOM.render(<App/>, document.getElementById('root'));
}
首次执行ReactDOM.render 则会创建rootFiberNode和rootFiber 其中rootFiberNode是这个应用的根节点 rootFiber是App所在组件树的根节点
但是整个应用只有一个根节点 那就是rootFiberNode,
rootFiberNode.current = rootFiber;
由于是首屏渲染,页面中还没有任何DOM。所以rootFiber.child null,即current Fiber树为空。update阶段 {
组件更新时 触发状态改边 这边会启用render阶段并构建一棵workInProgressFiber树
其中有许多workInProgressFiber的 fiber的创建是可以复用current fiber的节点数据
这个就是 react核心 diff算法 实现的部分,
react 核心diff算法实现原理
-
首先在react官网diff算法介绍
-
在这简单说明一下几个概念:一个Dom节点在某一时刻,最多有4个节点,和其他的相关。
1. current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点。 2. workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点 3. DOM节点本身。 4. JSX对象。即ClassComponent的render方法的返回结果,或FunctionComponent的调用结果。JSX对象中包含描述DOM节点的信息。 Diff算法的本质是对比1和4,生成2。
-
在查阅资料得知 diff算法 在性能的上也是存在诸多问题 如下:
由于Diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n是树中元素的数量 若是在 中使用才算法 ,那么展示1000个元素所需要的计算量是非常大的,这样对于成本内存代价太大。
- 为了降低算法的复杂度,react 的diff算法 预设了三个限制
1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定
diff算法的实现
- 首先我们从入口函数着手 :reconcileChildFibers ,此该函数会根据newChild(即JSX对象)类型调用不同的处理函数。
- 下面贴上源码:
//根据 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
// object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
if (isObject) {
switch (newChild.$$typeof) {
// 调用 reconcileSingleElement 处理
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
}
}
// 调用 reconcileSingleTextNode 处理
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 调用 reconcileChildrenArray 处理
***多节点的处理方案***
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
// If the new child is undefined, and the return fiber is a composite
// component, throw an error. If Fiber return types are disabled,
// we already threw above.
switch (returnFiber.tag) {
case ClassComponent: {
if (__DEV__) {
const instance = returnFiber.stateNode;
if (instance.render._isMockFunction) {
// We allow auto-mocks to proceed as if they're returning null.
break;
}
}
}
// Intentionally fall through to the next case, which handles both
// functions and classes
// eslint-disable-next-lined no-fallthrough
case FunctionComponent: {
const Component = returnFiber.type;
invariant(
false,
'%s(...): Nothing was returned from render. This usually means a ' +
'return statement is missing. Or, to render nothing, ' +
'return null.',
Component.displayName || Component.name || 'Component',
);
}
}
}
// Remaining cases are all treated as empty.
// 其余的情况都被视为空的
// 以上都没有命中,删除节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
// 最后返回此函数
return reconcileChildFibers;
}
我们可以从同级的节点数量将Diff分为两类:
当newChild类型为object、number、string,代表同级只有一个节点 -单节点
当newChild类型为Array,同级有多个节点 --- 多节点
- 先来看看单一节点 以obj为例子会进入 reconcileSingleElement
- 我们看看源码中 Dom节点是否可以复用是怎么实现的
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// // 首先判断 是否存在对应DOM节点
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
// 上一次更新存在DOM节点,接下来判断是否可复用
if (child.key === key) {
// key相同,接下来比较type是否相同
switch (child.tag) {
case Fragment: {
if (element.type === REACT_FRAGMENT_TYPE) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
break;
}
case Block:
if (enableBlocksAPI) {
let type = element.type;
if (type.$$typeof === REACT_LAZY_TYPE) {
type = resolveLazyType(type);
}
if (type.$$typeof === REACT_BLOCK_TYPE) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
if (
((type: any): BlockComponent<any, any>)._render ===
(child.type: BlockComponent<any, any>)._render
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.type = type;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
}
// We intentionally fallthrough here if enableBlocksAPI is not on.
// eslint-disable-next-lined no-fallthrough
default: {
if (
child.elementType === element.type ||
// type相同则表示可以复用 返回复用的fiber
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
break;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 若是不同则标记删除fiber 节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
从代码可以看出,React通过先判断key是否相同,如果key相同则判断type是否相同,只有都相同时一个DOM节点才能复用。
这里有个细节需要关注下:
当child !== null且key相同且type不同时执行deleteRemainingChildren将child及其兄弟fiber都标记删除。
当child !== null且key不同时仅将child标记删除。
- 多节点的处理方案 :见上 注释的部分源码 ; 先就这样 等一些 时间在补充 ~~~~ 注: 本人也是学习阶段 勿喷谢谢。