|
准备工作
为了方便讲解,假设我们有下面这样一段代码:- function App(){
- const [count, setCount] = useState(0)
- useEffect(() => {
- setCount(1)
- }, [])
- const handleClick = () => setCount(count => count++)
- return (
- <div>
- 勇敢牛牛, <span>不怕困难</span>
- <span onClick={handleClick}>{count}</span>
- </div>
- )
- }
- ReactDom.render(<App />, document.querySelector('#root'))
复制代码 在React项目中,这种jsx语法首先会被编译成:- React.createElement("App", null)
- or
- jsx("App", null)
复制代码 这里不详说编译方法,感兴趣的可以参考:
babel在线编译
新的jsx转换
jsx语法转换后,会通过或的api转换为作为的第一个参数进行渲染。
在上一篇文章中,我们提到过一个React项目会有一个和一个或多个。是一个项目的根节点。我们在开始真正的渲染前会先基于DOM创建,且- fiberRoot.current = rootFiber
复制代码 ,这里的就是fiber树的根节点。- if (!root) {
- // Initial mount
- root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
- fiberRoot = root._internalRoot;
- }
复制代码 在创建好和后,我们还不知道接下来要做什么,因为它们和我们的函数组件没有一点关联。这时React开始创建,并将的第一个参数,也就是基于创建的赋给。- var update = {
- eventTime: eventTime,
- lane: lane,
- tag: UpdateState,
- payload: null,
- callback: element,
- next: null
- };
复制代码 有了这个,还需要将它加入到更新队列中,等待后续进行更新。在这里有必要讲下这个队列的创建流程,这个创建操作在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;
复制代码 我将上面的代码进行了一下抽象,更新队列是一个环形链表结构,每次向链表结尾添加一个时,指针都会指向这个,并且这个会指向第一个更新:
上一篇文章也讲过,React最多会同时拥有两个树,一个是fiber树,另一个是fiber树。fiber树的根节点在上面已经创建,下面会通过拷贝的形式创建fiber树的根节点。
到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成树和树,并最终渲染到页面中。相关参考视频讲解:进入学习
render阶段
这个阶段并不是指把代码渲染到页面上,而是基于我们的代码画出对应的树和树。
workloopSync
- function workLoopSync() {
- while (workInProgress !== null) {
- performUnitOfWork(workInProgress);
- }
- }
复制代码 在这个循环里,会不断根据workInProgress找到对应的child作为下次循环的workInProgress,直到遍历到叶子节点,即深度优先遍历。在会执行下面的。
beginWork
简单描述下的工作,就是生成树。
基于的根节点生成的节点并将这个节点作为根节点的,然后基于的节点生成的节点并作为的节点的,如此循环直到最下面的文本。
注意, 在上面流程图中,会执行一个函数,这个函数里面会执行这个函数组件,在这里会初始化函数组件里所有的,也就是上面实例代码的。
当遍历到牛牛文本时,它的下面已经没有了,这时的工作就暂时告一段落,为什么说是暂时,是因为在时,如果遍历的节点有会再次走到。
completeWork
当遍历到牛牛文本后,会进入这个。
在这里,我们再简单描述下的工作, 就是生成树。
基于节点生成对应的节点,并且将这个节点作为父节点,将之前生成的节点插入到当前创建的节点。并会基于在生成的不完全的fiber树向上查找,直到。在这个向上的过程中,会去判断是否有,如果有会再次走,没有就继续向上。这样到了根节点,一个完整的树就生成了。
额外提一下,在中有这样一段代码- if (flags > PerformedWork) {
- if (returnFiber.lastEffect !== null) {
- returnFiber.lastEffect.nextEffect = completedWork;
- } else {
- returnFiber.firstEffect = completedWork;
- }
- returnFiber.lastEffect = completedWork;
- }
复制代码 解释一下,代表当前这个节点是有副作用的,需要将这个节点加入到父级的链表中。
commit阶段
这个阶段的主要工作是处理副作用。所谓副作用就是不确定操作,比如:插入,替换,删除DOM,还有hook的回调函数都会被作为副作用。
commitWork
准备工作
在前,会将在中生成的fiber树赋值给的属性。- var finishedWork = root.current.alternate; // workInProgress fiber树
- root.finishedWork = finishedWork; // 这里的root是fiberRoot
- root.finishedLanes = lanes;
- commitRoot(root);
复制代码 在上面我们提到,如果一个节点有副作用会被记录到父级的的。
在下面代码中,如果树有副作用,会将节点作为第一个副作用,并且将形成闭环。- 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之前的阶段,遍历链表,执行- 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(注意,这里是调度,并不会马上执行)。主要工作是创建一个:- var newTask = {
- id: taskIdCounter++,
- callback: callback, //上面代码传入的回调函数
- priorityLevel: priorityLevel,
- startTime: startTime,
- expirationTime: expirationTime,
- sortIndex: -1
- };
复制代码 它里面有个逻辑会判断和, 如果,会把这个任务加入到定时任务队列,反之会加入任务队列,并- task.sortIndex = expirationTime
复制代码 。
mutation
简单描述mutation阶段的工作就是负责dom渲染。
区分,进行不同的操作,比如:重置文本,重置ref,插入,替换,删除dom节点。
和mutation之前阶段一样,也是遍历链表,执行方法。- do { // mutation dom渲染
- invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);
- } while (nextEffect !== null);
复制代码 看下的主要工作:- function commitMutationEffects(root, renderPriorityLevel) {
- // TODO: Should probably move the bulk of this function to commitWork.
- while (nextEffect !== null) { // 遍历EffectList
- setCurrentFiber(nextEffect);
- // 根据flags分别处理
- var flags = nextEffect.flags;
- // 根据 ContentReset flags重置文字节点
- if (flags & ContentReset) {...}
- // 更新ref
- if (flags & Ref) {...}
- var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
- switch (primaryFlags) {
- case Placement: // 插入dom
- {...}
- case PlacementAndUpdate: //插入dom并更新dom
- {
- // Placement
- commitPlacement(nextEffect);
- nextEffect.flags &= ~Placement; // Update
- var _current = nextEffect.alternate;
- commitWork(_current, nextEffect);
- break;
- }
- case Hydrating: //SSR
- {...}
- case HydratingAndUpdate: // SSR
- {...}
- case Update: // 更新dom
- {...}
- case Deletion: // 删除dom
- {...}
- }
- resetCurrentFiber();
- nextEffect = nextEffect.nextEffect;
- }
- }
复制代码 按照我们的示例代码,这里会走,首先是- commitPlacement(nextEffect)
复制代码 方法,在一串判断后,最后会把我们生成的树插入到DOM节点中。- function appendChildToContainer(container, child) {
- var parentNode;
- if (container.nodeType === COMMENT_NODE) {
- parentNode = container.parentNode;
- parentNode.insertBefore(child, container);
- } else {
- parentNode = container;
- parentNode.appendChild(child); // 直接将整个dom作为子节点插入到root中
- }
- }
复制代码 到这里,代码终于真正的渲染到了页面上。下面的方法是执行和有关的东西,这里不做重点,后面文章安排,我们只要知道这里是执行上一次更新的。
fiber树切换
在讲阶段之前,先来看下这行代码- root.current = finishedWork // 将`workInProgress`fiber树变成`current`树
复制代码 这行代码在mutation和layout阶段之间。在mutation阶段, 此时的fiber树还是指向更新前的树, 这样在生命周期钩子内获取的DOM就是更新前的, 类似于和的钩子是在阶段执行的,这样就能获取到更新后的DOM进行操作。
layout
简单描述layout阶段的工作:
和mutation之前阶段一样,也是遍历链表,执行方法。- do { // 调用生命周期和hook相关操作, 赋值ref
- invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
- } while (nextEffect !== null);
复制代码 来看下方法:- function commitLayoutEffects(root, committedLanes) {
- while (nextEffect !== null) {
- setCurrentFiber(nextEffect);
- var flags = nextEffect.flags;
- // 调用生命周期或钩子函数
- if (flags & (Update | Callback)) {
- var current = nextEffect.alternate;
- commitLifeCycles(root, current, nextEffect);
- }
- {
- // 获取dom实例,更新ref
- if (flags & Ref) {
- commitAttachRef(nextEffect);
- }
- }
- resetCurrentFiber();
- nextEffect = nextEffect.nextEffect;
- }
- }
复制代码 提一下,的回调会在方法中执行,而的回调会在中的方法进行调度。从这里就可以看出和的区别:
- 的上次更新销毁函数在阶段销毁,本次更新回调函数是在dom渲染后的阶段同步执行;
- 在阶段会创建调度任务,在阶段会将销毁函数和回调函数加入到
- pendingPassiveHookEffectsUnmount
复制代码 和- pendingPassiveHookEffectsMount
复制代码 队列中,最终它的上次更新销毁函数和本次更新回调函数都是在阶段后异步执行; 可以明确一点,他们的更新都不会阻塞dom渲染。
layout之后
还记得在阶段的这几行代码吗?- // 创建任务并加入任务队列,会在layout阶段之后触发
- scheduleCallback(NormalPriority$1, function () {
- flushPassiveEffects();
- return null;
- });
复制代码 这里就是在调度,在阶段之后会执行这个回调函数,此时会处理的上次更新销毁函数和本次更新回调函数。
总结
看完这篇文章, 我们可以弄明白下面这几个问题:
- React的渲染流程是怎样的?
- React的beginWork都做了什么?
- React的completeWork都做了什么?
- React的commitWork都做了什么?
- useEffect和useLayoutEffect的区别是什么?
- useEffect和useLayoutEffect的销毁函数和更新回调的调用时机?
到此这篇关于React渲染机制超详细讲解的文章就介绍到这了,更多相关React渲染机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:https://www.jb51.net/article/266712.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|