初级眼中的React的更新机制

589 阅读5分钟

前言

最近看了些掘金各位前端大佬的React源码文章,根据自己去源码的理解,总结了React的更新机制。也许并非是全部正确的,如果有错误或者理解偏差的地方,希望各位大佬可以在评论区提出一些自己的见解。

React的更新机制

根据我目前所了解的React的机制,我将其分为五个阶段

  • schedul空间调度阶段
  • reconcile 转化阶段
  • beginWork diff比对阶段
  • completeWork 复用节点阶段
  • commit更新阶段

schedul阶段

schedul(空间调度)在Wiondw有一个requestIdleCallbac函数,

requestIdleCallbac 官方文档介绍:

window.requestIdleCallback() 开发者能够在循环上执行后台和低级工作,而不会延迟关键事件,如动画和响应。会先按顺序执行函数然后timeout有顺序调用的执行顺序,可以执行顺序,以便在前面执行函数而乱打乱执行顺序。

requestIdleCallback 接受两个参数,一个是回调函数,一个是timeout,

 // 空闲调度
    const workLoop = (deadline: any) => {
        console.log(deadline, '空间调度的参数')
        // 是否空闲
        let shouldYield = false;
        while (nextFiberReconcileWork && !shouldYield) {
            nextFiberReconcileWork = performNextWork(
                nextFiberReconcileWork
            )
            shouldYield = deadline.timeRemaining() < 1
        }

        // 全部转化结束,进入commit阶段
        if (!nextFiberReconcileWork) {
            commitRoot()
        }
        requestIdleCallback(workLoop);
    }
\

我理解本质是,每次调用requestIdleCallback会返回一个值,这个值是剩余时间,需要执行的任务会判断剩余时间是否足以支撑这个任务完成,如果不足以支撑,则暂停。如果足以支撑则继续执行任务。

这个其实就是React Fiber 出现的意义。之前React 是通过递归来进行更新操作,这个操作是不可打断的,如果打断了,下次将重新递归。这样的机制,就会造成页面的卡顿,对用户的体验不好

根据一些文章,React的底层其实并没有采用requestIdleCallback这个函数,而是从底层自己实现了一套requestIdleCallback 空间调度的函数

schedul阶段 主要做了什么?

判断虚拟DOM转化为Fiber的时间是否充足,不充足就停止转换。时间充足则一直转化,直到全部转换结束

reconcile阶段

将Vdom 转化为Fiber 并通过一定的结构,将每个Fiber工作单元相连接,形成一个链表

function performNextWork(fiber: any) {
        console.log(fiber,'fibelfibelfibel');
        reconcile(fiber);
        if (fiber.child) {
            return fiber.child;
        }
        let nextFiber = fiber;
        while (nextFiber) {
            if (nextFiber.sibling) {
                return nextFiber.sibling;
            }
            nextFiber = nextFiber.return;
        }
    }

在上述代码中,可以看到转化的流程 ,我将其分为3个流程

  • 先找fiber的child(子元素),子元素存在,将子元素作为一个fiber工作单元return出去,重新执行schedule

  • 再找fibel的sibling(兄弟元素),兄弟元素存在,将兄弟元素作为一个fiber工作单元return出去,重新执行schedule

  • 最后return fibel的父节点,

在这个过程中,每转化一个Vdom 变成一个Fiber工作单元,都会重新去执行一下schedule,判断时间是否充足,如果不充足,停止转化,先渲染页面。等待下帧在继续转化,直到全部转化完成,执行commit阶段,在下帧重新渲染页面,如果充足,继续转化,全部转化完成,执行commit,重新渲染页面。

reconcile过程中,也会根据Fiber去创建DOM节点。最后会把所有Fiber工作单元连接成一个链表,通俗点来讲就是一个Fiber树。

beginWork阶段,通过diff 算法 对比两棵树。

单节点diff比对

  • 如果同级节点不相同,直接放弃旧的Fiber节点,创建新的Fiber节点
  • 如果同级节点相同,对比key 如果key相同,则替换Fiber节点的props属性
  • 按照相同的比对方式,一直校验,直到全部Fiber节点全部对比结束

多节点diff比对

  • 判断同级节点是否相同,如果不相同,直接放弃oldFiber,创建新的Fiber节点
  • 如果节点相同,需要判断是否Fiber节点移动了位置,如果移动了位置,则打上标记
  • 如果节点相同,也没有移动位置,则只替换Fiber节点的props属性

beginWork 阶段涉及到两个问题,为什么React的元素都要有一个key,为什么不建议key使用索引(index),

在我的理解中,React 进行diff的比对阶段,如果两个元素的key相同,说明是相同的元素,可以直接替换Dom的props属性,将其更新,不需要重新去生成DOM,节省了性能,如果是index索引的话,index会随着数组的长度而改变。我们使用index作为唯一的标识,如果数组的长度加1或者减1,或者是某个元素移动了位置,相对应的index 也会发生改变,React比对的情况下,会删除掉之前的元素,重新创建一个相同的元素

这里有个问题:如果不设置key值,React diff阶段会先比对 标签,也就是babel中的第一个参数,设置key为index 就跳过比对标签这一步了吗?这里继续下,后续重新学习React,要注重关注这个问题

completeWork 阶段

这个是diff的附属阶段,会进行一次向上比对,尽可能的复用oldFiber 提高性能,同时也会将已经对比结束的Fiber树加入更新队列,在commit阶段完成渲染

commit 更新DOM

// commitRoot
    function commitRoot() {
        commitWork(wipRoot?.child);
        wipRoot = null
    }

    // commitWork
    function commitWork(fiber: any) {
        if (!fiber) {
            return
        }
    
        let domParentFiber = fiber.return
        while (!domParentFiber.dom) {
            domParentFiber = domParentFiber.return
        }
        const domParent = domParentFiber.dom
    
        if (
            fiber.effectTag === "PLACEMENT" &&
            fiber.dom != null
        ) {
            domParent.appendChild(fiber.dom)
        } 
        commitWork(fiber.child)
        commitWork(fiber.sibling)
    }

commit 阶段会根据Fiber树的结构child,sibling, return进行渲染,并且插入到DOM中,因为在reconcile阶段DOM已经创建好了,并且通过diff阶段也知道了是删除,新增,移动的哪一项操作,所以commit 阶段会很快

渲染视图,React 更新结束

博主也是一个初级的React的小白,这个只是博主自己对React 更新机制的理解,各位仅供参考,如果错误,或者更好的见解,可以在评论区指导下博主。