[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索引来移动节点,flagisMOVE。这里我当时最大的疑问就是怎么样通过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都有严格的顺序,如果顺序乱了,更新阶段就不会正确复用到在currentFiberHook 链表当中的 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 更新完毕