React 核心源码侧重点在于16.8架构,而 React18 架构更加侧重的是服务端 SSR 相关的处理,也就是说16.8之后整体架构思想是类似的。
本文将对比 React15,以及16.8之后的版本来对 React 核心源码进行解析,包含fiber、双缓存、JSX编译、diff算法等内容。
React 框架特性
-
快速响应
-
单向数据流 ui = render(data)
如何做到快速响应?
这个问题可以转化成什么会导致响应慢,主要有两点:
-
CPU卡顿
-
IO卡顿
怎么解决这两个问题?
CPU卡顿
浏览器刷新频率一般是60Hz,所以我们每一帧就是 1000ms / 60Hz = 16.6ms
如果我们每一帧的执行时间大于16.6ms,那么用户就会感受到卡顿,也就是掉帧。
我们的 JS 线程和 GUI 线程是互斥的,所以 JS 执行和样式的布局和绘制不能同时进行,一旦 JS 执行时间超过16.6ms,就不会执行布局和绘制。
对此,React 采用了分片的方法,将耗时长的任务分为许多小片,在每一帧当中预留 5ms 的时间给 JS 线程,再执行其他任务(渲染等),也就实现了非阻塞渲染。
let yieldInterval = 5;
当然,也可以根据刷新率来改变yieldInterval
forceFrameRate = function(fps) {
if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not unsupported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
// reset the framerate
yieldInterval = 5;
}
};
我们使用React框架时,是通过 createRoot 来开启分片的能力,也就是 concurrent mode,这是16.8提供的能力。
ReactDOM.createRoot(rootEl).render(<App/>);
IO卡顿
IO卡顿是由于网络请求延迟造成的卡顿,React 中可以通过显示 loading,加载完成后隐藏来解决,也就是Suspense
,用户会看见骨架屏,而不是留一个白屏给用户。这样做增加了用户体验。
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
我们也可以根据任务的优先级来处理,优先响应优先级高的任务。
总之,我们快速响应的核心就是将同步的长任务转变为可中断的异步任务。
React15
React15的架构只有同步任务,它有两个核心概念:
-
Reconciler 协调器
-
Renderer 渲染器
Reconciler
协调器Reconciler负责找出变化的组件,this.setState、this.forceUpdate、ReactDOM.render 这些API都会触发我们的页面更新,就会调用 Reconciler。
Reconciler的工作如下:
-
调用函数组件或是类组件的 render 方法,将JSX转化为虚拟DOM
-
将虚拟DOM和上次更新时的虚拟DOM对比
-
通过对比找出需要改变的虚拟DOM
-
通知Renderer将变化后的虚拟DOM渲染到页面上
Renderer
React 支持跨平台,不同的平台有不同的Renderer,除了我们最熟悉的 ReactDOM 以外还有 ReactNative、ReactArt等,负责将虚拟DOM渲染到页面上。
缺点
mount组件时,会调用mountComponent
方法,它会根据标签调用mountWrapper
,而mountWrapper
又会调用mountComponent
方法,也就是进行了递归的过程。
mountComponent: function(...) {
...
switch (this._tag) {
case 'audio':
case 'form':
case 'iframe':
case 'img':
case 'link':
case 'object':
case 'source':
case 'video':
this._wrapperState = {
listeners: null,
};
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'input':
ReactDOMInput.mountWrapper(this, props, hostParent);
props = ReactDOMInput.getHostProps(this, props);
transaction.getReactMountReady().enqueue(trackInputValue, this);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'option':
ReactDOMOption.mountWrapper(this, props, hostParent);
props = ReactDOMOption.getHostProps(this, props);
break;
case 'select':
ReactDOMSelect.mountWrapper(this, props, hostParent);
props = ReactDOMSelect.getHostProps(this, props);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'textarea':
ReactDOMTextarea.mountWrapper(this, props, hostParent);
props = ReactDOMTextarea.getHostProps(this, props);
transaction.getReactMountReady().enqueue(trackInputValue, this);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
}
assertValidProps(this, props);
...
switch (this._tag) {
case 'input':
transaction.getReactMountReady().enqueue(inputPostMount, this);
if (props.autoFocus) {
transaction
.getReactMountReady()
.enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'textarea':
transaction.getReactMountReady().enqueue(textareaPostMount, this);
if (props.autoFocus) {
transaction
.getReactMountReady()
.enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'select':
if (props.autoFocus) {
transaction
.getReactMountReady()
.enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'button':
if (props.autoFocus) {
transaction
.getReactMountReady()
.enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'option':
transaction.getReactMountReady().enqueue(optionPostMount, this);
break;
}
return mountImage;
}
同理,update 组件时会触发updateComponent
方法,也会进行递归。
如果递归层级很深,那么执行时间超过16ms时,用户交互就会卡顿。
并且React15采用的递归方式不支持中断。
React16
相比React15,React16增加了Scheduler调度器,负责让高优先级的任务先进入Reconciler执行。
对于优先级调度,很多人最先会想到requestIdleCallback
这个API,然而因为它不兼容safari,并且触发频率不稳定,因此React实现了自己的requestIdleCallback
polyfill
。
React16还实现了workLoopConcurrent
,用可中断的异步任务来代替递归遍历的方式,通过shouldYield
来判断是否需要中断。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
这里的workInProgress
在Fiber中赘述
在React16中,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记。
那么,异步可中断是如何实现的呢?
这就是React16架构的核心——Fiber。
Fiber
Fiber是一种数据结构,每个Fiber对应一个element,保存它的组件类型、优先级、调度的上下文等信息。
-
静态节点的信息
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
-
链接其他Fiber节点的指针
this.return = null; // 父节点
this.child = null; // 子节点
this.sibling = null; // 右边第一个兄弟节点
this.index = 0;
this.ref = null;
-
动态属性,调度的上下文
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.effectTag = NoEffect;
this.subtreeTag = NoSubtreeEffect;
this.deletions = null;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
-
调度的优先级
this.lanes = NoLanes;
this.childLanes = NoLanes;
-
指向下一次更新时对应的Fiber
this.alternate = null;
双缓存
如何更新DOM,我们React16给出的方法是双缓存,我们在内存中绘制当前的Fiber,绘制完毕后替换上一帧的Fiber,我们把当前渲染的Fiber成为current Fiber,而内存中的是workInProgress Fiber,也就是WIP Fiber。
这两个Fiber的alternate
指针互相指向对方。
JSX
JSX 和 Fiber节点不是一个概念,JSX不包含调度的优先级,上下文等内容,并且JSX是createElement
的语法糖,要经过babel编译为createElement
方法。
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 将 config 处理后赋值给 props
...
}
const childrenLength = arguments.length - 2;
...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 标记这是个 React Element
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
React执行流程
状态更新
在React中状态更新会创建一个Update
对象,保存更新状态的相关信息,在render阶段的beginWork
中会获取到新的state。
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null, // 指向下一个Update
};
Fiber节点中存在updateQueue
,他负责维护更新的上下文
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null, // 环形链表的形式存储更新时的Update对象
},
effects: null,
};
state的变化在render阶段产生与上次更新不同的JSX对象,通过Diff算法产生effectTag,在commit阶段渲染在页面上,同时,渲染完成后workInProgress Fiber树变为current Fiber树,整个更新流程结束。
render阶段
在render阶段,Scheduler和Reconciler进行工作,生成Fiber并且构建成render树。
无论是同步遍历还是异步遍历,采用的都是递归的思路。
“递”会调用beginWork
,它是一个深度优先遍历的过程,根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来,如果遍历到叶子结点,就会进入“归”的过程。
“归”会调用completeWork
,当执行完completeWork
,会判断当前Fiber节点右边是否存在兄弟节点,如果存在,就执行兄弟节点的“递”的过程,如果不存在,就执行父节点的“归”的过程。
beginWork
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else {
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
if (getIsHydrating() && isForkedChild(workInProgress)) {
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case ...
}
}
可以看到beginWork
会根据current
是否为空来判断组件是应该mount
还是update
。
-
mount
时,会根据传入的WIP的tag的不同来创建不同类型子的Fiber节点。 -
update
时会判断是否可以复用current节点,这样就能克隆current.child
到workInProgress.child
。
diff算法
update时,需要使用一套算法来判断current Fiber和WIP Fiber中哪些节点是可以复用的,哪些是需要改变的。
diff算法需要处理以下一些情况:
-
不同类型的元素:销毁旧节点,创建新节点
-
同一类型,属性不同的元素:更新组件的props
那么,我们的diff算法要比较的是什么呢?
和DOM相关的节点有以下几种:
-
current Fiber
-
JSX对象
-
DOM本身
-
workInProgress Fiber
我们通过比较current Fiber和JSX对象,生成workInProgress Fiber,最终用workInProgress Fiber替换current Fiber。
diff算法的入口是reconcileChildFibers
函数,他会根据JSX对象的类型做不同的处理。
function reconcileChildFibersImpl(
returnFiber: Fiber, // 当前节点的父节点
currentFirstChild: Fiber | null, // 当前节点
newChild: any, // 新元素
lanes: Lanes,
): Fiber | null {
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
...
}
// 针对数组,要进行多节点的处理
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
);
}
if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
const context: ReactContext<mixed> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
// 文本节点
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
针对不同的节点,它会调用不同的函数处理:
单节点
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 通过child !== null来判断在current Fiber中是否存在节点,如果不存在就新增
while (child !== null) {
// 首先判断key是否相同
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// 如果是单节点,就要删除其余的兄弟节点
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;
}
} else {
// 如果是普通的element节点
if (child.elementType === elementType) {
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;
}
}
// 没有匹配到就删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
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;
}
}
先判断key是否相同,如果相同判断type是否相同,如果都相同就复用节点;
如果key相同,type不同,添加删除标记;
key不同,也添加删除标记。
多节点
是否可以使用类似Vue2的双端diff算法?
不可以,因为React的Fiber使用的是单链表的数据结构,不能用指针。而JSX对象,newChildren是个数组。
所以React整体思路使用两轮遍历,第一轮处理要更新的节点,第二轮处理非更新的节点。
第一轮流程如下:
-
遍历newChildren,将
newChildren[i]
和oldFiber
比较,也就是上面的单节点比较,判断节点是否可复用。 -
如果可复用,i++,判断
oldFiber.sibling
是否可复用。 -
如果不能复用:
-
key不同,跳出循环,第一轮结束;
-
key相同,type不同,标记oldFiber为deletion,继续遍历。
-
-
newChildren或者oldFiber遍历完,跳出遍历。
第一轮完成后,会有以下几种情况:
-
newChildren和oldFiber都没有遍历完,进行第二轮遍历;
-
newChildren遍历完,oldFiber没有遍历完,遍历剩下的oldFiber,添加标记,deletion;
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
-
newChildren没有遍历完,oldFiber遍历完,遍历剩下的newChildren,添加标记,placement;
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
-
newChildren和oldFiber都遍历完,此时直接进行update,diff结束。
第二轮遍历:
根据oldFiber创建map
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber,
): Map<string | number, Fiber> {
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild: null | Fiber = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
接着遍历newChildren,根据map查找对应的节点,但是我们还需要知道节点是否移动。
我们需要lastPlacedIndex
来标记最后一个可复用节点在oldFiber中的位置,用oldIndex
指向遍历到的可复用节点,如果oldIndex < lastPlacedIndex
,就要将可复用节点向右移动。
lastPlacedIndex
初始为0,每遍历一个可复用的节点,如果oldIndex >= lastPlacedIndex
,则lastPlacedIndex = oldIndex
。
completeWork
而completeWork
的工作就是根据不同的Fiber.tag
进行不同的处理,会打一些标记(更新/删除/新增),也就是之前介绍的,Fiber节点中的这些属性:
this.effectTag = NoEffect;
this.subtreeTag = NoSubtreeEffect;
this.deletions = null;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
会生成形如 firstEffect -> nextEffect -> ... -> nextEffect -> lastEffect 的 effectList,保存了我们变化的内容。
最终会将 effectList 交给 commit 阶段去处理。
commit阶段
一些生命周期或者hooks(useEffect)将在commit阶段执行。
commit阶段可以分为三个部分:
-
before mutation阶段(执行DOM操作前)
-
mutation阶段(执行DOM操作)
-
layout阶段(执行DOM操作后)
before mutation
do {
// 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// root指 fiberRootNode
// root.finishedWork指当前应用的rootFiber
const finishedWork = root.finishedWork;
// 凡是变量名带lane的都是优先级相关
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置Scheduler绑定的回调函数
root.callbackNode = null;
root.callbackId = NoLanes;
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 重置优先级相关变量
markRootFinished(root, remainingLanes);
// 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。
if (rootsWithPendingDiscreteUpdates !== null) {
if (
!hasDiscreteLanes(remainingLanes) &&
rootsWithPendingDiscreteUpdates.has(root)
) {
rootsWithPendingDiscreteUpdates.delete(root);
}
}
// 重置全局变量
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
}
// 将effectList赋值给firstEffect
// 由于每个fiber的effectList只包含他的子孙节点
// 所以根节点如果有effectTag则不会被包含进来
// 所以这里将有effectTag的根节点插入到effectList尾部
// 这样才能保证有effect的fiber都在effectList中
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effectTag
firstEffect = finishedWork.firstEffect;
}
mutation
遍历effectList,针对其中每个节点调用commitMutationEffects
函数,根据不同的标记(更新/删除/新增)进行不同的操作。
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根据 ContentReset effectTag重置文字节点
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 并 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
layout
此时已经渲染完成,会触发一些生命周期,能够拿到渲染完成后的结果了。
同样也是while循环遍历effectList
root.current = finishedWork;
nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
nextEffect = null;
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 调用生命周期钩子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 赋值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
优先级
React出于对用户体验的考量,对于不同的交互分配了不同的优先级,主要有以下几种优先级:
switch (priorityLevel) {
case ImmediatePriority:
// Times out immediately
timeout = -1;
break;
case UserBlockingPriority:
// Eventually times out
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
// Never times out
timeout = maxSigned31BitInt;
break;
case LowPriority:
// Eventually times out
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
// Eventually times out
timeout = normalPriorityTimeout;
break;
}
React会调用Scheduler中的runWithPriority
方法,根据优先级排列,并且通过定时器去触发对应的事件:
function unstable_runWithPriority<T>(
priorityLevel: PriorityLevel,
eventHandler: () => T,
): T {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
}
var previousPriorityLevel = currentPriorityLevel;
currentPriorityLevel = priorityLevel;
try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
}
}
lane模型
lane模型是React中表示优先级的机制,它使用31位二进制,位数越小的优先级越高,某些相邻的位具有相同的优先级。
export const NoLanes: Lanes = /* / 0b0000000000000000000000000000000;
export const NoLane: Lane = / / 0b0000000000000000000000000000000;
export const SyncLane: Lane = / / 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = / / 0b0000000000000000000000000000010;
export const InputDiscreteHydrationLane: Lane = / / 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = / / 0b0000000000000000000000000011000;
const InputContinuousHydrationLane: Lane = / / 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = / / 0b0000000000000000000000011000000;
export const DefaultHydrationLane: Lane = / / 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = / / 0b0000000000000000000111000000000;
const TransitionHydrationLane: Lane = / / 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = / / 0b0000000001111111110000000000000;
const RetryLanes: Lanes = / / 0b0000011110000000000000000000000;
export const SomeRetryLane: Lanes = / / 0b0000010000000000000000000000000;
export const SelectiveHydrationLane: Lane = / / 0b0000100000000000000000000000000;
const NonIdleLanes = / / 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = / / 0b0001000000000000000000000000000;
const IdleLanes: Lanes = / / 0b0110000000000000000000000000000;
export const OffscreenLane: Lane = / */ 0b1000000000000000000000000000000;
同时,优先级还具有“批”的概念,例如Transition中有多个连续的1,表示占用多条通道。
使用了二进制位,就会方便我们的优先级计算,因为在计算机中位运算是很快的。
// 判断a b是否有交集
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}
// 计算b这个lanes是否是a对应的lanes的子集,只需要判断a与b按位与的结果是否为b:
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
return (set & subset) === subset;
}
// 将两个lane或lanes的位合并只需要执行按位或操作:
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}