[React] 解析 React 框架底层源码(上)
温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释
目录
1. Visual Dom | JSX | Fiber
2. React 初次并发渲染
3. React Update
4. React Diff
5. useState | useReducer
Visual Dom | JSX | Fiber
JSX
JSX
是 ECMAScript
的语法拓展,我们可以用 JSX 语法
来更加灵活,自如,声明式的表达 React UI组件
,JSX
最终都会被编译器转译成为 ECMAScript
。
相信这也是大家喜欢 React 的原因。
旧转换
React.createElement("h1", { id: "title", }, "hello");
新转换
javascript
import { jsx as _jsx } from "react/jsx-runtime";
_jsx("h1", {
id: "title",
children: "hello"
}, "title");
Visual Dom
Visual Dom
是一个普通的 JavaScript
对象。Visual Dom
出现主要是用来解决频繁操作真实 DOM
, 从而在写法上,性能上,抽象表现上带来的一系列问题。
有了 Visual Dom
之后,有以下几个优点:
跨平台 --
Visual Dom
是对UI
及其交互 的一层抽象描述,不像DOM
一样和平台强相关。所以可以让这层抽象描述可以用在Android、iOS、VR
。增量更新 -- 可以实现精准的定量更新,通过
Diff
算法,尽可能的复用节点,从而尽可能少的操作/改变真实Dom
。- 处理兼容性 -- 处理浏览器的版本对
API
的兼容/polyfill 和支持。 - 安全性 --
Visual Dom
内容经过 XSS 处理,可以防范 XSS 攻击。 - 轻量级 -- 虚拟
DOM
属性,继承的层级 都比 原生少很多。
Visual Dom
缺点 :
- 虚拟 DOM 的创建和消费需要消耗额外的内存。
- 虚拟 DOM 首次渲染其实并不一定会更快。
Fiber
为什么会出现 Fiber
?
Fiber
之前的 React
的工作架构,就是一气呵成,从根节点出发,遍历整个应用程序,独占浏览器的线程资源。随着前端应用程序的规模不断扩大,之前的工作架构遇到了性能问题,或者说是性能瓶颈。性能瓶颈产生的原因是:
主流屏幕的刷新率为 60HZ
(1s 刷新 60次), 也就是浏览器每刷新一帧,花费时间为 16.8ms, 为了保证浏览器的每一帧都能够流畅刷新,那么必须保证浏览器 1 帧的时间不能超过 16.8ms, 超过了这个时间,浏览器不能流畅的刷新下一帧,浏览器就会卡帧,丢帧,用户会感觉到明显卡顿和响应丢失。
所以对于之前的 React 工作架构来说,执行 React 代码时,会一直独占浏览器的线程资源,会导致 浏览器 1 帧的时间超过 16.8ms, 导致卡帧,掉帧,丢顿, 并且 Javascript 引擎和页面渲染引擎在同一个渲染线程, GUI 渲染和 Javascript 执行两者是互斥的。 如果某个任务执行时间过长,浏览器会推迟渲染。除此之外,浏览器线程资源一直被 JavaScript 独占,用户的响应事件,动画会丢失,卡顿。
所以 Fiber 的出现,就是为了解决上述问题。
Fiber
是什么?
Fiber
本质就是一个普通的 JavaScript
对象,也是 React
的最小执行单元。Fiber
架构就是由 一个个的 Fiber
对象组成的数据结构。
Fiber
能做什么?
通过Fiber
架构/数据结构或者说 Fiber
的出现,React
的协调过程由一气呵成变成可被中断式调度。React
可以适时地让出浏览器线程资源,让浏览器及时地响应用户的交互,渲染 Dom
,执行动画。每次执行完一个 Fiber
执行单元, React
就会检查浏览器的 1 帧 现在还剩多少时间,如果没有时间就将控制权让出去。
Fiber
的缺点?
最小执行单元是一个 Fiber
,如果单个 Fiber
的计算太复杂,还是会大量占据浏览器一帧的时间。尽管有高优先级打断低优先级,解决饥饿问题等策略。但是硬件设备的瓶颈,软件层面只能是尽可能的优化,尽可能的缓解。
React 初次并发渲染
为什么叫并发渲染?
我们知道同步渲染是 React 早期版本的默认行为。在这种模式下,React 会按照组件的更新顺序,逐步执行整个渲染过程,直到渲染完成。所有的计算和更新会一次性执行,直到当前任务完全完成为止。
并发渲染是 React 16 及以后版本引入的一种新特性,旨在提升渲染过程的响应性和流畅度。在并发渲染模式下,React 会将渲染任务分成小块,通过时间分片(time slicing)逐步执行,避免一次性执行长时间任务,从而避免卡顿。
问题:下面这段 React 代码的初次渲染流程是怎样的?
let App = <h1>Hello World</h1>;
const root = createRoot(document.getElementById("root"));
root.render(<App />);
第一:调用 createRoot
函数, 传入 root
节点。创建 FiberRoot 节点。
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks
);
return new ReactDOMRoot(root);
}
第二:调用 render
函数,在 updateContainer
函数当中 创建 RootFiber
节点。与 FiberRoot
建立如下图所示的关系。
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
updateContainer(children, root, null, null);
};
第三:在 updateContainer
函数中,创建 根节点的 update 对象。调用 enqueueUpdate函数
入队。
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function
): Lane {
const update = createUpdate(eventTime, lane);
// 根节点的 update 对象 element 就是 App函数的虚拟 Dom
update.payload = { element };
const root = enqueueUpdate(current, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, current, lane, eventTime);
entangleTransitions(root, current, lane);
}
return lane;
}
第四:然后再 updateContainer
函数中调用 scheduleUpdateOnFiber
-> ensureRootIsScheduled
函数
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number
) {
ensureRootIsScheduled(root, eventTime);
}
第五:ensureRootIsScheduled
函数,以一个 schedulerPriorityLevel
(初次渲染,优先级是 16
)优先级开始调度执行 performConcurrentWorkOnRoot
。Scheduler
返回一个 newCallbackNode
赋值给 根 Fiber 的 callbackNode
属性。 这是可以并发的关键。
schedulerPriorityLevel
是根据lanesToEventPriority(nextLanes)
函数,将React lane
模型中的优先级转换成了React 事件优先级
, 最后再转换成React Scheduler 优先级
。为什么要转换 ? 因为React Scheduler
只有 5个优先级,但是React lane
模型 有 31 个 优先级,要想用这 5 个 Scheduler 优先级调度 这 31 个 lane 优先级的任务, 就应该先将 31 lane 合并成 5 个React 事件优先级
, 最后转换成 5 个React Scheduler 优先级
。function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root) ); root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; }
第六:在浏览器一帧的空闲时间中(5ms),
performConcurrentWorkOnRoot
函数被调度执行。这里先会判断是否有条件去进行时间切片
。如果应该时间切片则并发渲染,那么调用renderRootConcurrent
函数, 否则,同步渲染调用renderRootSync
函数。
初次渲染,走时间切片并发渲染。 renderRootConcurrent
函数。因为初次渲染优先级是 16,不是同步优先级,时间片没有过期,不包括阻塞线程任务。
function performConcurrentWorkOnRoot(root, didTimeout) {
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
ensureRootIsScheduled(root, now());
return null;
}
第七: renderRootConcurrent
函数中去 调用 workLoopSync
函数。while 循环中 调用 performUnitOfWork
函数。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
performUnitOfWork(workInProgress);
}
}
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent();
}
第八:performUnitOfWork
函数 当中就是我们熟悉的 beginWork
completeWork
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
第九:进入 beginWork
初次挂载阶段, 根据不同的标签去对应挂载不同的 mount 逻辑。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes
);
}
}
}
第十:在不同的挂载逻辑当中执行 reconcilChildren
构建 子 fiber。初次渲染走的是,mountChildFibers
函数,任务就是构建子fiber树。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
第十一:当前工作单元的 fiber
的 reconciler
完毕, 假设 5ms 的时间片到期 退出 while 循环。 退出 workLoopConcurrent
函数。
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
第十二: 返回到 performConcurrentWorkOnRoot
函数中,由于 reconciler
过程还未完成,root.callbackNode === originalCallbackNode
成立, 给 Schduler 返回 performConcurrentWorkOnRoot
函数, Schduler 判断是函数之后,不会将该任务弹出优先级队列 ,而是继续下一帧时间继续执行 performConcurrentWorkOnRoot
,直至 构建完整棵 fiber树,返回 null, Schduler
判断是 null ,之后将该任务弹出优先级队列。
if (root.callbackNode === originalCallbackNode) {
if (
workInProgressSuspendedReason === SuspendedOnData &&
workInProgressRoot === root
) {
root.callbackPriority = NoLane;
root.callbackNode = null;
return null;
}
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
第十三:fiber 节点的 beginwork
结束后会走 completeUnitOfWork
-> completeWork
函数
function performUnitOfWork(unitOfWork: Fiber): void {
next = beginWork(current, unitOfWork, renderLanes);
if (next === null) {
completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
}
completeWork
函数中 还是判断不同的标签,进行 不同的 complete
逻辑, 我们这里用原生标签来举例。
对于 HostComponent
来说,complete
时 会调用 updateHostComponent
函数
React 18.2 之后, 大家在 completeWork 阶段 熟悉的 effectsLists 都被删掉了,都改成了 bubbleProperties 函数。这个我们放到更新的文章里面去说。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(current, workInProgress, type, newProps);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
// This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
}
}
}
第十四: updateHostComponent
函数当中挂载 props
, 并且通过 appendAllChildren
函数 将所有子节点 DOM
全部插入到自己的 DOM 当中。这样也就意味着,进行最后的提交阶段时,只需要将 App 函数组件的第一个 child 插入到 div#root 容器中就可以完成挂载。因为其余节点在 completeWork
函数中就已经插入到了父 DOM 当中去了。
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props
) {
appendAllChildren(newInstance, workInProgress, false, false);
};
第十四:在 commit
时, 判断子树上有副作用 或者 根节点上有副作用,就可以提交,否则不提交。
初次渲染只有 根节点的第一个 child 有副作用,其余没有副作用 ,可以提交。 在 mutation
阶段 调用 commitMutationEffects
函数。
if (subtreeHasEffects || rootHasEffect) {
commitMutationEffects(root, finishedWork, lanes);
}
为什么其余子节点没有副作用 而根节点的第一个
child
有副作用 ? 因为reconcileChildren
时,其余节点走到是mountChildFibers
函数,而根节点的第一个child
走的是reconcileChildFibers
函数。为什么根节点的第一个
child
走的是reconcileChildFibers
函数? 这是因为挂载的时候,rootFiber
已经有current
属性了。所以current !== null
, 走reconcileChildFibers
函数,并在根节点的第一个child
节点上 追加副作用:|= PLACEMENT
。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
commitMutationEffects
函数中,通过 recursivelyTraverseMutationEffects
函数,递归提交子节点的作用, commitReconciliationEffects
函数递归提交自己的副作用。 将有副作用的的节点,根据副作用标签进行增删改查。比如本例当中:从根节点开始递归提交,递归到 根节点的第一个 child <h1></h1>
时, 有副作用 PLACEMENT
则调用 commitPlacement(finishedWork)
函数 通过 appendChild()
插入到根节点当中,完成挂载。
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork)
}
如果是根节点的第一个 child 是一个 App 函数/类/Memo组件,则又是不同的提交逻辑……
自此完成挂载
React Update
学习源码不易,和大家一起共勉!
前文: [[React Origin Code] 2022年来聊聊React Mount-- renderRootSync](https://juejin.cn/post/7152109746571444232)当中,我们刚刚说完 React Mount
,今天我们继续来聊聊React update
,在聊update
之前,我们还需要再次回到mount
阶段,这次主要把握和关注两个概念,workInProgress树
和 current树
的双缓冲Fiber
机制。这在React update
阶段起到了至关重要的作用。
关于双缓存树的资料
下面我们以React 计数器案例来聊聊React update逻辑
createWorkInProgress
下面这段总结读完本文,可以回过头来看看:
createWorkInProgress函数
:mount
的时候只会走一次,就是创建workInProgress树
的根节点,mount
的其他节点不进到createWorkInProgress
函数,因为此时没有currentfiber
树,其他节点的current === null
,当执行到reconcileChildren
函数的时候,走的是mountChildFibers
,只有更新的时候,current !== null
,走reconcileChildrenFibers
的时候,才让每个子节点先去递归执行createWorkProgress
,复用建立workInProgressfiber
节点,建立好workInProgress树
和 current树
,并且用alternate
属性,对两棵树的节点进行联系。
beginwork前的fiber树
关于mount
阶段, beginwork
函数的作用和流程,我们在前文已经详细聊过了, 在bedginwork之前,会调用createWorkInProgress函数
。
mount
的时候,在刚进入createWorkInProgreess
的函数上打上断点,打印fiber树
。结果是这样的。
mount的时候,执行的第一次也是最后一次的createWorkInProgreess函数结果是这样的
然后从workProgreess
树的根fiber
节点开始 mount
,关于mount
的流程这篇文章很详细,mount
之后结果是这样的。
细节注意:下一次更新之前,刚刚 mount
时的workInProgress树
会被变成current树
,这里的变,也仅仅时需要改变FiberRootNode.current
指针而已。这也是双缓存机制的魅力所在。
第一次触发计数器 + 1 -> update
mount
结束后,我们触发计数器+1, 开始第一次更新,我们由上图可以知道,mount
之后,第一次更新之前只有根节点有自己的alternate,所以在createWorkInProgress
函数时,根节点fiber
可以复用current树的 fiber
,由于复用current fiber
之后其child
也被复用,所以结果是这样的。
我们发现第一次更新,除了根节点FiberRootNode之外每次先递归来到createWorkInProgress来执行,curretn.alternatte都是null,所以每次都回去createFiber去创建新的workProgress节点,第一次更新之后就会在每次createWorkInProgress的时候,当前workInprogressfiber就会和 current fiber建立alternative相互链接。然后去递归beginwork,reconcileChildren,然后再让children fiber去作为根节点去createWorkInProgress。begin这里有一个更新逻辑,每次我们来到带着cr创建好的workj fiber节点和current的时候,会有一个优化,didReceiveUpdate 如果为true会走优化逻辑。通过新旧props 对比,新旧type对比,以及是否有context的改变来对比,bailoutOnAlreadyFinishWork逻辑,会执行cloneChildFibers 返回子fiber。如果没有命中,继续根据type来updatae不同类型的标签组件。
update阶段 beginwork 和 createWorkInProgress函数调用栈的关系 reconcile也进入了不一样的逻辑,这一次current fiber节点不在是null了,会进入reconcileChildrenFibers而不是 mountChildrenFibers这时shouldTraceEffect 会变为true给一些需要新插入的节点打上effectTag Placement, 或者是删除节点打上 delection
beginwork后创建建立双缓存Fiber树的结果是这样的:
completework老朋友了,workInProgress树回溯阶段,从下至上,对props 进行diff来 删除或者来收集变化属性到fiber节点的updateQueue当中,第i属性为变化的属性,i+ 1属性是属性变化的值,将需要更新的节点打上effectTag, update,收集到effectList的 链表当中,complete 回溯到根节点的时候,从根节点出发的 firstEffect属性 就指向 第一个需要comit 变化的fiber,next下一个,一直到lastEffect最后,最后commitWork 根节点。commit阶段更新渲染视图.这样做的好处是不用再像renderer阶段一样深度优先遍历,都从根节点出发去遍历到每一个子节点去看看fiber节点是否发生变化,来打上相对应的effecttag。
第二次触发计数器 + 1 -> update
第二次更新很关键,因为这时内存当中有两颗react fiber树 一棵树是w,一棵树是 c,而且之前建立了alternate联系,这一次建立新fiber的时候 除了新添加的节点,其他节点都在第一次更新的基础上增加了altenate属性,所以这一次每次不用在重新创建,workINprogrss fiber节点了,我们复用wok 的 al c来 属性来获取fiber节点。这一次和以往不同的地方还有,
React Diff
希望可以帮助到,与我同样纠结在此的前端程序员们。
大家都知道,React
真正的Diff
逻辑是在reconcileChildFibers
,这没有任何问题,那么图片当中的两行代码是什么?与Diff
有什么关系,事实上 reconcileChildFibers
被当作闭包函数返回了出来,被React用于两个方面,第一个方面是初次挂载渲染,第二个方面是更新Diff渲染。那我们接下来就从Reconcile阶段的初次渲染
和更新Diff
两个方面来说React Diff
。
初次渲染
我们先对初次渲染做总结,之后看代码会很有感觉:React的初次渲染在reconcile阶段,主要的任务是从根节点FiberRoot
开始深度优先遍历多叉树结构的vdom
,映射构建单层fiber链表结构和整个fiber多叉树结构(及构建fiber对象的属性,包括但不限于index, key, type,internate, flags……),并且构建fiber
结构的child
(第一个孩子), return
(父亲), sibling
(兄弟)关系。
下面是reconcile构建的过程源码
这里我们只是简单聊聊reconcile阶段的初次渲染,暂时不涉及到React的scheduler
和commitWork
阶段。有人可能会问,文章到了这里没出现Diff的过程,那么初次渲染和Diff有什么关系呢?其实答案很简单,大家也都知道,有了初次的渲染,我们下次更新才有Diff的对象呀! 接下来我们就来和大家聊聊更新阶段的Diff
。
更新Diff渲染
我们先对更新Diff渲染
做总结,之后看代码会很有感觉:React的更新Diff渲染在reconcile阶段,主要的任务是将不同类别(type)旧的fiber
节点和新的vdom
结构进行Diff
,可以分为:对普通文本,数字节点进行reconcile
,对单节点进行reconcile
,对多节点进行reconcile
普通string, number节点的 reconcile
进入这个函数,已经确定
newChildren
是文本节点了。这里判断
oldFiber
是不是文本节点。如果是文本节点,可以复用节点,就使用
useFiber
来复用创建新的文本fiber
节点。如果不是文本节点,不可以复用节点,删除当前层级的
oldFiber
链表,只创建一个新的文本fiber
节点。
单节点的
reconcile
进入这个函数,已经确定
newChildren
是单节点了。这个时候判断
oldFiber
的key
和type
,看看可不可以复用。可以复用,删除
oldFiber
链表剩余的链表节点。不可以复用,删除
oldFiber
链表的全部节点,创建新的fiber
节点。
多节点的 reconcile
多节点是Diff
的精髓,我们还是先总结,等一下看代码会非常有感觉,多节点的diff的主要流程是:三次循环遍历,一次判断,完成oldFiber链表和新的vdom结构的diff,从而完成oldfiber链表节点的删除和移动和newfiber节点的增加,以构造新的fiber结构。
第一次循环遍历
如果第一次循环遍历的顺利的话,大家看for循环的退出条件,也就是
newChldren
都可以复用oldFIber
链表节点或者是oldFiber
有的链表节点,newChldren
都可以复用。那么newChldren
就可以顺利建立起child, sibling的联系,所以在顺利的情况下,第一次循环退出的条件要不就是newChldren
的长度太长了,要不就是oldFiber
的链表长度太长了。- 如果是
oldFiber
的链表长度太长了,经过第一次判断,会delete
掉多余的oldFiber
链表节点。 - 如果是
newChldren
的长度太长了,在经过第二次循环遍历会构建没有遍历到的newChldren
链表节点。 - 自此结束,如果顺利的话不会进入第三次循环。
- 如果是
如果第一次遍历循环不顺利的话,即只要发现一个不能复用的节点,就立马退出第一次循环。从不能复用的节点开始,将所有的
oldfiber
的链表节点,依次加入到map
结构当中。然后进入第三次循环,继续遍历
newChldren
链表,遍历到一个newChldren, jsx
节点,就拿着key
去由oldFiber
节点构成的map
当中去找 可以复用的key
。- 没有找到,构建新的
fiber
节点,flag
为PLACEMENT
。 - 如果
newChldren
在map
当中找到对应key
的fiber
了,说明可以复用oldfiber
节点。 - 复用分为两种:一种是移动复用,一种是更新复用。
- 如果是更新复用:那么就
updateElement(oldChildrenVdom, newChildrenVdom)
复用构建fiber节点,然后在map
当中删除复用的oldFiber
。 - 如果是移动复用:那么就利用
placedIndex
索引来移动节点,flag
isMOVE
。这里我当时最大的疑问就是怎么样通过placedIndex
来移动的。源码当中的index < placedIndex
为什么会被标注为移动? 原因很简单就是两个节点的相对位置发送了变化,比如原来是a -> d
现在在是d -> a
, 我们从现在的d -> a
开始出发遍历,遍历到d
,发现在原来的1
位置,此时placeIndex
更新为1
,当遍历到a
的时候,我们发现a
在原来的0
位置,如果相对位置没有发送变化,这里a
的位置应该在b
的后面。大于1 (placeIdex)
,但是现在是< 1
意味着a
到了b
前面,所以相对位置发送了变化,标记为移动。然后在map
当中删除复用的oldFiber
。之后删除map
当中剩余的fiber
节点。 - 并循环移动和更新来构建出这一层新的
fiber
结构。
- 没有找到,构建新的
一次判断
- 和上文第一次判断那里说的一样,当
newFiber
链表已经全部顺利遍历完成,oldFiber
链表还有节点的时候,删除剩下的oldFiber
节点。 #### 第二次循环遍历
第二次循环遍历的两个作用,我们在文章当中已经全部展示了。
作用1:文章开头说的初次渲染。
作用2:第一次循环遍历顺利的时候,当
oldFiber
链表节点已经遍历完成后,newFiber
还有的时候,继续遍历newFiber
来创建fiber
,建立联系。第三次循环遍历
上文从代码的角度聊了节点的移动策略,下面我们以dom的角度来表达移动diff的过程。 >旧节点 a -> b -> c -> d -> e -> f (key为自己) > > 新节点 a -> c -> b -> e -> g (key为自己) > > --- > 遍历新节点到c发现,b不能符合立马跳出第一次循环,将剩下的旧节点放到了map当中(key为item)。 > {"b": b, "c": c, "d": d, "e": e, "f": f}, 遍历新节点c去map当中去找,找到了 placeIndex =2, 更新节节点不移动,删除map当中的c,遍历到b找到了,index < placeIndx -> 1 < 2 ,所以标记b为移动节点,同理e更新,g在map当中找不到,标记为增加。 > > --- > > 遍历完之后要去更新dom了,这里要注意,先删除真实dom节点中要移动和删除的节点,删除之后真实dom变为 a -> c -> e,这时b移动的时候,索引为原来的mountIndex 为2,判断2上有没有元素,这里a -> c -> e 索引为2是e,所以执行parentDom.insertBefore() 将b插入到e之前,完成移动,执行到g新增的时候,索引为原来的mountIndex为4,判断4上有没有元素,这里a -> c -> b -> e,索引为4没有执行parentDom.appendChild(), 完成新增,最终结果 a -> c -> b -> e -> g,移动,更新完毕。
一点总结
我的两点理解,希望可以帮助到与我同样纠结在此的前端程序员们
diff
过程是一层一层的oldFiber
和newFiber
去diff。移动
不是dom
节点的平移,本质上还是复用,只不过不用重新document.createElement()
,利用fiber结构
上的引用stateNode
去将真实DOM,去insertBefore
或者appendChild
来完成的移动。
useState | useReducer
代码都来自 React 18 源码, 大家可以放心食用
附加面试题:
- 为什么不能在条件和循环里使用Hooks?
- 为什么不能在函数组件外部使用Hooks?
- React Hooks的状态保存在了哪里?
useReducer 原理
问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。
const reducer = (state, action) => {
if (action.type === "add") return state + 1;
else return state;
};
function Counter() {
const [number, dispatch0] = useReducer(reducer, 0);
const [number1, dispatch1] = useReducer(reducer, 0);
return (
<div
onClick={() => {
dispatch({ type: "add" });
dispatch1({ type: "add" });
}}
>
{number}
</div>
);
}
> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点
useReducer mount 挂载阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);
第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();
第三: 调用 Counter 函数 的 useReducer 函数
export function useReducer(reducer, initialArg) {
return ReactCurrentDispatcher.current.useReducer(reducer, initialArg);
}
第四:挂载阶段 ReactCurrentDispatcher.current.useReducer
实则是调用了 mountReducer
,
第五:在 mountReducer
中调用,mountWorkInProgressHook
函数,创建 useReducer
的 Hook
对象,构建 fiber.memoizedState
也就是 Hook 链表, 然后将 dispatchAction
bind 绑定之后传入 queue
和 fiber
为参数,这点很重要。并且初始化 Hook 的更新对象的队列 queue。
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
如果之后再有 useState useReducer,最终mout阶段的成果是
自此 useReducer 挂载阶段执行完毕
useReducer update 更新阶段
第一:通过 dispatch(action) 触发更新,dispatch
就是 dispatchAction.bind(null, fiber, queue)
返回的绑定函数。所以相当于调用了 dispatchAction(fiber, queue, action)
。
第二: 更新时 dispatchAction 调用 scheduleUpdateOnFiber
, enqueueConcurrentHookUpdate
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
enqueueConcurrentHookUpdate
函数 中的 enqueueUpdate
将 hook
更新时产生的对象 update,放入 queue.pending
当中,例如 一个 reducer 的 多次 dispatch, update
会组成队列。
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
}
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);
第五:在 renderWithHooks 当中调用 Counter 函数
let children = Component();
第六: 调用 Counter 函数 的 useReducer 函数
export function useReducer(reducer, initialArg) {
return ReactCurrentDispatcher.current.useReducer(reducer, initialArg);
}
第七:更新阶段 ReactCurrentDispatcher.current.useReducer
实则是调用了 updateReducer
第八:在 updateReducer
中调用 updateWorkInProgressHook
函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber
(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber
(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现),一个比较重要的复用就是去复用老 Hook 的更新队列 queue
,因为 dispatchAction.bind
绑定的就是 currentFiber
(老 Fiber), 通过尽可能的复用来创建新的 Hook 对象,构建 fiber.memoizedState
也就是新的 Hook 链表。
注意:这里也就是为什么不能再循环和判断当中使用 Hook 的重要原因。 一句话:要保持严格的顺序一致。
读到这儿我们发现,无论是
Mout Hook
还是updateHook
都有严格的顺序,如果顺序乱了,更新阶段就不会正确复用到在currentFiber
Hook 链表当中的 Hook 的更新队列 queue,也就不能通过更新得到正确的state
. 再严重些,useState的更新逻辑,对应的 currentHook 是 useEffect Hook, 无法兼容复用,导致报错。
function updateWorkInProgressHook(): Hook {
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
return workInProgressHook;
}
第九:在 updateReducer
中调用,遍历整个更新队列 queue.pending
,取出 update
对象的 action 通过 newState = reducer(newState, action)
; 返回 新状态 和 queue.dispatch(还是 dispatchAction.bind(null, Currentfiber, queue)
。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
// Process this update.
const action = update.action;
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
自此 useReducer 更新完毕
useState 原理
问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。
function Counter() {
const [number, setNumber] = useState(reducer, 0);
return (
<div
onClick={() => {
setNumber({ type: "add" });
}}
>
{number}
</div>
);
}
> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点
useState mount 挂载阶段
let children = Component();
第三: 调用 Counter 函数的第一个 useState 函数
export function useEffect(create, deps) {
return ReactCurrentDispatcher.current.useState(create, deps);
}
第四:挂载阶段 ReactCurrentDispatcher.current.useState
实则是调用了 moutState
, moutState
中调用,mountWorkInProgressHook
函数,创建 useState
的 Hook
对象,构建 fiber.memoizedState
也就是 Hook 链表, 然后将 dispatchAction
bind 绑定之后传入 queue
和 fiber
为参数,这点很重要。并且初始化 Hook 的更新对象的队列 queue。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
如果之后再有 useState useReducer,最终mount阶段的成果如下图:
自此 useState 挂载阶段执行完毕
useState update 更新阶段
第一:通过 dispatch(action) 触发更新,dispatch
就是 dispatchAction.bind(null, fiber, queue)
返回的绑定函数。所以相当于调用了 dispatchAction(fiber, queue, action)
。
第二: 更新时 dispatchAction 调用 scheduleUpdateOnFiber
, enqueueConcurrentHookUpdate
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
enqueueConcurrentHookUpdate
函数 中的 enqueueUpdate
将 hook
更新时产生的对象 update,放入 queue.pending
当中,例如 一个 useState 的多次 setStae, update
会组成队列。
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
}
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);
第五:在 renderWithHooks 当中调用 Counter 函数
let children = Component();
第六: 调用 Counter 函数 的 useState 函数
export function useState(reducer, initialArg) {
return ReactCurrentDispatcher.current.useState(reducer, initialArg);
}
第七:更新阶段 ReactCurrentDispatcher.current.useState
实则是调用了 updateReducer
useState 更新的时候,调用的还是 updateReducer, 说明 useState 本质就是 useReducer. 也是 useReducer 的语法糖。 将 basicStateRecuer 作为 reducer 函数。
function updateState<S>() {
return updateReducer(basicStateReducer, (initialState: any));
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
第八:在 updateReducer
中调用 updateWorkInProgressHook
函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber
(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber
(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现),一个比较重要的复用就是复用老 Hook 的更新队列 queue
,因为 dispatchAction.bind
绑定的就是 currentFiber
(老 Fiber), 通过尽可能的复用来创建新的 Hook 对象,构建 fiber.memoizedState
也就是 新的 Hook 链表。
function updateWorkInProgressHook(): Hook {
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
return workInProgressHook;
}
第九:在 updateReducer
中调用,遍历整个更新队列 queue.pending
,取出 update
对象的 action 通过 newState = reducer(newState, action)
; 返回 新状态 和 queue.dispatch(还是 dispatchAction.bind(null, Currentfiber, queue)
。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
// Process this update.
const action = update.action;
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
自此 useState 更新完毕