我的编程空间,编程开发者的网络收藏夹
学习永远不晚

React渲染机制超详细讲解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

React渲染机制超详细讲解

准备工作

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

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语法转换后,会通过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后,我们还不知道接下来要做什么,因为它们和我们的<App />函数组件没有一点关联。这时React开始创建update,并将ReactDom.render()的第一个参数,也就是基于<App />创建的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的根节点生成<App />fiber节点并将这个节点作为根节点的child,然后基于<App />fiber节点生成<div />fiber节点并作为<App />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);

看下commitMutationEffects的主要工作:

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;
  }
}

按照我们的示例代码,这里会走PlacementAndUpdate,首先是commitPlacement(nextEffect)方法,在一串判断后,最后会把我们生成的dom树插入到rootDOM节点中。

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中
  }
}

到这里,代码终于真正的渲染到了页面上。下面的commitWork方法是执行和useLayoutEffect()有关的东西,这里不做重点,后面文章安排,我们只要知道这里是执行上一次更新的effect unmount

fiber树切换

在讲layout阶段之前,先来看下这行代码

root.current = finishedWork  // 将`workInProgress`fiber树变成`current`树

这行代码在mutation和layout阶段之间。在mutation阶段, 此时的currentfiber树还是指向更新前的fiber树, 这样在生命周期钩子内获取的DOM就是更新前的, 类似于componentDidMountcompentDidUpdate的钩子是在layout阶段执行的,这样就能获取到更新后的DOM进行操作。

layout

简单描述layout阶段的工作:

  • 调用生命周期或hooks相关操作
  • 赋值ref

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

do {   // 调用生命周期和hook相关操作, 赋值ref
   invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
} while (nextEffect !== null);

来看下commitLayoutEffects方法:

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;
  }
}

提一下,useLayoutEffect()的回调会在commitLifeCycles方法中执行,而useEffect()的回调会在commitLifeCycles中的schedulePassiveEffects方法进行调度。从这里就可以看出useLayoutEffect()useEffect()的区别:

  • useLayoutEffect的上次更新销毁函数在mutation阶段销毁,本次更新回调函数是在dom渲染后的layout阶段同步执行;
  • useEffectmutation之前阶段会创建调度任务,在layout阶段会将销毁函数和回调函数加入到pendingPassiveHookEffectsUnmountpendingPassiveHookEffectsMount队列中,最终它的上次更新销毁函数和本次更新回调函数都是在layout阶段后异步执行; 可以明确一点,他们的更新都不会阻塞dom渲染。

layout之后

还记得在mutation之前阶段的这几行代码吗?

// 创建任务并加入任务队列,会在layout阶段之后触发
scheduleCallback(NormalPriority$1, function () {
  flushPassiveEffects();
  return null;
});

这里就是在调度useEffect(),在layout阶段之后会执行这个回调函数,此时会处理useEffect的上次更新销毁函数和本次更新回调函数。

总结

看完这篇文章, 我们可以弄明白下面这几个问题:

  1. React的渲染流程是怎样的?
  2. React的beginWork都做了什么?
  3. React的completeWork都做了什么?
  4. React的commitWork都做了什么?
  5. useEffect和useLayoutEffect的区别是什么?
  6. useEffect和useLayoutEffect的销毁函数和更新回调的调用时机?

到此这篇关于React渲染机制超详细讲解的文章就介绍到这了,更多相关React渲染机制内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

React渲染机制超详细讲解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

React渲染机制超详细讲解

React整个的渲染机制就是React会调用render()函数构建一棵Dom树,在state/props发生改变的时候,render()函数会被再次调用渲染出另外一棵树,重新渲染所有的节点,构造出新的虚拟Domtree
2022-11-13

React运行机制超详细讲解

React作为当下最为流行的前端开发框架之一,使用它可以快速构建大型Web应用,加上其出色的性能表现,使得众多互联网公司对它格外地青睐,这篇文章主要介绍了React运行机制
2022-11-13

MyBatis插件机制超详细讲解

MyBatis在四大对象的创建过程中,都会有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截的效果
2022-11-13

React diff算法超详细讲解

渲染真实DOM的开销很大,有时候我们修改了某个数据,直接渲染到真实dom上会引起整个dom树的重绘和重排。我们希望只更新我们修改的那一小块dom,而不是整个dom,diff算法就帮我们实现了这点。diff算法的本质就是:找出两个对象之间的差异,目的是尽可能做到节点复用
2022-11-13

Vue渲染器设计实现流程详细讲解

在浏览器平台上,用它来渲染其中的真实DOM元素。渲染器不仅能够渲染真实的DOM元素,它还是框架跨平台能力的关键。所以在设计渲染器的时候一定要考虑好自定义的能力
2023-01-03

Vue超详细讲解重试机制示例

这篇文章主要介绍了Vue重试机制示例,重试指的是当加载出错时,有能力重新发起加载组件的请求。异步组件加载失败后的重试机制,与请求服务端接口失败后的重试机制一样
2023-01-05

JavaGUI事件监听机制超详细讲解

Java事件监听器是由事件类和监听接口组成,自定义一个事件前,必须提供一个事件的监听接口以及一个事件类。JAVA中监听接口是继承java.util.EventListener的类,事件类继承java.util.EventObject的类
2023-03-03

React diff算法面试考点超详细讲解

渲染真实DOM的开销很大,有时候我们修改了某个数据,直接渲染到真实dom上会引起整个dom树的重绘和重排。我们希望只更新我们修改的那一小块dom,而不是整个dom,diff算法就帮我们实现了这点。diff算法的本质就是:找出两个对象之间的差异,目的是尽可能做到节点复用
2022-12-19

C++ pimpl机制详细讲解

PIMPL 是 C++ 中的一个编程技巧,意思为指向实现的指针。具体操作是把类的实现细节放到一个单独的类中,并用一个指针进行访问
2022-11-13

Python多进程并发与同步机制超详细讲解

进程(Process),顾名思义,就是进行中的程序。有一句话说得好:程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体。进程是资源分配的最小单元,也就是说每个进程都有其单独的内存空间
2022-12-23

React实现控制减少useContext导致非必要的渲染详解

这篇文章主要介绍了React如何有效减少使用useContext导致的不必要渲染,使用useContext在改变一个数据时,是通过自己逐级查找对比改变的数据然后渲染,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
2022-11-13

C++BoostMetaStateMachine定义状态机超详细讲解

Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
2022-12-08

编程热搜

目录