您现在的位置是:网站首页> 编程资料编程资料

React渲染机制超详细讲解_React_

2023-12-09 324人已围观

简介 React渲染机制超详细讲解_React_

准备工作

为了方便讲解,假设我们有下面这样一段代码:

function App(){ const [count, setCount] = useState(0) useEffect(() => { setCount(1) }, []) const handleClick = () => setCount(count => count++) return ( 
勇敢牛牛, 不怕困难{count}
) } ReactDom.render(, document.querySelector('#root'))

在React项目中,这种jsx语法首先会被编译成:

React.createElement("App", null) or jsx("App", null)

这里不详说编译方法,感兴趣的可以参考:

babel在线编译

新的jsx转换

jsx语法转换后,会通过creatElementjsx的api转换为React element作为ReactDom.render()的第一个参数进行渲染。

在上一篇文章Fiber中,我们提到过一个React项目会有一个fiberRoot和一个或多个rootFiberfiberRoot是一个项目的根节点。我们在开始真正的渲染前会先基于rootDOM创建fiberRoot,且fiberRoot.current = rootFiber,这里的rootFiber就是currentfiber树的根节点。

if (!root) { // Initial mount root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate); fiberRoot = root._internalRoot; }

在创建好fiberRootrootFiber后,我们还不知道接下来要做什么,因为它们和我们的函数组件没有一点关联。这时React开始创建update,并将ReactDom.render()的第一个参数,也就是基于创建的React element赋给update

var update = { eventTime: eventTime, lane: lane, tag: UpdateState, payload: null, callback: element, next: null };

有了这个update,还需要将它加入到更新队列中,等待后续进行更新。在这里有必要讲下这个队列的创建流程,这个创建操作在React有多次应用。

var sharedQueue = updateQueue.shared; var pending = sharedQueue.pending; if (pending === null) { // mount时只有一个update,直接闭环 update.next = update; } else { // update时,将最新的update的next指向上一次的update, 上一次的update的next又指向最新的update形成闭环 update.next = pending.next; pending.next = update; } // pending指向最新的update, 这样我们遍历update链表时, pending.next会指向第一个插入的update。 sharedQueue.pending = update; 

我将上面的代码进行了一下抽象,更新队列是一个环形链表结构,每次向链表结尾添加一个update时,指针都会指向这个update,并且这个update.next会指向第一个更新:

上一篇文章也讲过,React最多会同时拥有两个fiber树,一个是currentfiber树,另一个是workInProgressfiber树。currentfiber树的根节点在上面已经创建,下面会通过拷贝fiberRoot.current的形式创建workInProgressfiber树的根节点。

到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成fiber树和dom树,并最终渲染到页面中。相关参考视频讲解:进入学习

render阶段

这个阶段并不是指把代码渲染到页面上,而是基于我们的代码画出对应的fiber树和dom树。

workloopSync

function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } }

在这个循环里,会不断根据workInProgress找到对应的child作为下次循环的workInProgress,直到遍历到叶子节点,即深度优先遍历。在performUnitOfWork会执行下面的beginWork

beginWork

简单描述下beginWork的工作,就是生成fiber树。

基于workInProgress的根节点生成fiber节点并将这个节点作为根节点的child,然后基于fiber节点生成

fiber节点并作为fiber节点的child,如此循环直到最下面的牛牛文本。

注意, 在上面流程图中,updateFunctionComponent会执行一个renderWithHooks函数,这个函数里面会执行App()这个函数组件,在这里会初始化函数组件里所有的hooks,也就是上面实例代码的useState()

当遍历到牛牛文本时,它的下面已经没有了child,这时beginWork的工作就暂时告一段落,为什么说是暂时,是因为在completeWork时,如果遍历的fiber节点有sibling会再次走到beginWork

completeWork

当遍历到牛牛文本后,会进入这个completeWork

在这里,我们再简单描述下completeWork的工作, 就是生成dom树。

基于fiber节点生成对应的dom节点,并且将这个dom节点作为父节点,将之前生成的dom节点插入到当前创建的dom节点。并会基于在beginWork生成的不完全的workInProgressfiber树向上查找,直到fiberRoot。在这个向上的过程中,会去判断是否有sibling,如果有会再次走beginWork,没有就继续向上。这样到了根节点,一个完整的dom树就生成了。

额外提一下,在completeWork中有这样一段代码

if (flags > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork; } else { returnFiber.firstEffect = completedWork; } returnFiber.lastEffect = completedWork; }

解释一下, flags > PerformedWork代表当前这个fiber节点是有副作用的,需要将这个fiber节点加入到父级fibereffectList链表中。

commit阶段

这个阶段的主要工作是处理副作用。所谓副作用就是不确定操作,比如:插入,替换,删除DOM,还有useEffect()hook的回调函数都会被作为副作用。

commitWork

准备工作

commitWork前,会将在workloopSync中生成的workInProgressfiber树赋值给fiberRootfinishedWork属性。

var finishedWork = root.current.alternate; // workInProgress fiber树 root.finishedWork = finishedWork; // 这里的root是fiberRoot root.finishedLanes = lanes; commitRoot(root);

在上面我们提到,如果一个fiber节点有副作用会被记录到父级fiberlastEffectnextEffect

在下面代码中,如果fiber树有副作用,会将rootFiber.firstEffect节点作为第一个副作用firstEffect,并且将effectList形成闭环。

var firstEffect; // 判断当前rootFiber树是否有副作用 if (finishedWork.flags > PerformedWork) { // 下面代码的目的还是为了将这个effectList链表形成闭环 if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { // 这个rootFiber树没有副作用 firstEffect = finishedWork.firstEffect; }

mutation之前

简单描述mutation之前阶段的工作:

处理DOM节点渲染/删除后的 autoFocus、blur 逻辑;

调用getSnapshotBeforeUpdate,fiberRoot和ClassComponent会走这里;

调度useEffect(异步);

在mutation之前的阶段,遍历effectList链表,执行commitBeforeMutationEffects方法。

do { // mutation之前 invokeGuardedCallback(null, commitBeforeMutationEffects, null); } while (nextEffect !== null);

我们进到commitBeforeMutationEffects方法,我将代码简化一下:

function commitBeforeMutationEffects() { while (nextEffect !== null) { var current = nextEffect.alternate; // 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑; if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null){...} var flags = nextEffect.flags; // 调用getSnapshotBeforeUpdate,fiberRoot和ClassComponent会走这里 if ((flags & Snapshot) !== NoFlags) {...} // 调度useEffect(异步) if ((flags & Passive) !== NoFlags) { // rootDoesHavePassiveEffects变量表示当前是否有副作用 if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; // 创建任务并加入任务队列,会在layout阶段之后触发 scheduleCallback(NormalPriority$1, function () { flushPassiveEffects(); return null; }); } } // 继续遍历下一个effect nextEffect = nextEffect.nextEffect; } }

按照我们示例代码,我们重点关注第三件事,调度useEffect(注意,这里是调度,并不会马上执行)。

scheduleCallback主要工作是创建一个task

var newTask = { id: taskIdCounter++, callback: callback, //上面代码传入的回调函数 priorityLevel: priorityLevel, startTime: startTime, expirationTime: expirationTime, sortIndex: -1 };

它里面有个逻辑会判断startTimecurrentTime, 如果startTime > currentTime,会把这个任务加入到定时任务队列timerQueue,反之会加入任务队列taskQueue,并task.sortIndex = expirationTime

mutation

简单描述mutation阶段的工作就是负责dom渲染。

区分fiber.flags,进行不同的操作,比如:重置文本,重置ref,插入,替换,删除dom节点。

和mutation之前阶段一样,也是遍历effectList链表,执行commitMutationEffects方法。

do { // mutation dom渲染 invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel); } while (nextEffect !== null);

看下commitMutationEffect

-六神源码网