从React源码学习React的工作原理之渲染更新(四)

302 阅读8分钟

一、 React组件的渲染

1、React组件渲染到页面中的步骤

  • 触发更新;
  • 处理更新;
  • 提交更新;

2、触发更新

        当React页面首次渲染,或者当组件的state和props发生变化时,React就会触发更新过程,这通常发生在用户交互、数据获取 或 应用程序状态改变等场景中。

3、处理更新

        React处理更新主要包含两个阶段:render阶段和commit阶段,主要是调用一个叫做workLoop的函数去循环构建workInProgress树,构建过程是从root节点开始向下创建的。

        那么,React是怎样开启workLoop循环,构建workInProgress树的?有以下几种方式:

(1)root.render方法开启workLoop循环

render.png

        在页面首次渲染调用render方法时,会发起一次调度,即调用 scheduleUpdateOnFiber 函数,随后调用 scheduleCallback 将 performConcurrentWorkOnRoot 加入到任务队列中,在执行 performConcurrentWorkOnRoot 这个回调函数时,就开始进入workLoop循环,开始构建fiber树。

(2)类组件之调用this.setState方法如何进入workLoop循环?

class.png

        onClick函数调用this.setState函数,该函数的执行实际上是调用实例对象的enqueueSetState函数,发起调度,调用 scheduleUpdateOnFiber ;随后,调用 scheduleCallback 将 performSyncWorkOnRoot 加入到任务队列中,执行 performSyncWorkOnRoot ,进入workLoop循环,开始构建fiber树。

(3)函数组件之调用useState方法如何进入workLoop循环?

func.png

        useState hook在函数组件中调用时,会根据current是否存在来判断组件是首次调用还是后续渲染,首次渲染调用mountState,后续渲染调用updateState;这两个函数会调用 dispatchSetState ,依照上述图片中的函数调用顺序,就进入workLoop循环,开始构建fiber树。

4、提交更新

        当current树与workInProgress树一致时,提交更新,将虚拟DOM转为真实DOM节点插入到页面中;

5、如何区分首次渲染与更新?

        【依据】current是否存在,在首次渲染时,页面中还未显示任何内容,current也就不存在,即为null;

        首次渲染时,current树是一个null值,React会先构建一棵workInProgress树,首次渲染结束后,会将其赋值给 current树;
        在后续更新时,会将当前渲染在屏幕上的current树赋值给workInProgress树,然后使用diff算法标记哪些节点有更新,生成新的fiber树;
        React会根据节点是首次渲染还是更新来决定创建Fiber对象还是diff Fiber对象;

二、了解各个阶段的职责

1、render阶段

(1)职责
        React的render阶段是一个协调(Reconciliation)阶段,该阶段的主要工作是构建Fiber树、找出需要更新的组件、注册调度任务以及收集副作用;

【注意】
        render阶段会执行任务的调度,可以对任务进行挂起、恢复和中止等操作;但render阶段执行的工作不会导致任何用户可见的更改,因为此时的更改还没有被提交到真实的DOM节点上。

(2)组成
        render阶段由Scheduler和Reconciler两个模块组成,它们的主要任务是任务更新的调度以及Fiber Tree的构建。

(3)工作步骤

  • 根据更新数据生成新的虚拟DOM树;
    • React会为每一个React元素构建对应的Fiber对象,并根据该Fiber对象构建Fiber树;
    • Fiber对象存储了元素的上下文信息以及指针域构成的链表结构,使得React能够在执行到一半时,将工作保存在内存的链表中,以便在之后重新获取到可用的时间片继续工作;
  • 通过diff算法,快速找出需要更新的元素,并放入到更新队列中;
    • Fiber树构建完成后,Reconciler会通过diff算法找出需要更新的组件,并为这些组件打上effectTag标签,标注对应的DOM对象要进行的操作,如插入、删除或更新等;
  • 调用scheduleCallback函数注册调度任务;
    • 该阶段的工作由Scheduler来完成;
    • Scheduler会调度任务并可能中断任务;
  • 收集副作用,在commit阶段执行;

2、commit阶段

(1)职责
        React的commit阶段主要负责将在render阶段计算得到的更新应用到页面上,并同步到真实DOM中;该阶段是React更新流程中的最后一个阶段,也是最重要的一个阶段,因为它直接决定了用户最终看到的页面内容;

commit阶段的任务是不可中断的;

(2)工作步骤

  • 获取effectList链表
    • 在commit阶段开始时,会获取effectList链表,该链表包含了所有需要执行副作用的Fiber节点;
  • 执行before mutation阶段
    • 在该阶段,React会遍历effectList链表,并执行commitBeforeMutationEffects函数;该函数负责更新class组件实例上的state和props,以及执行getSnapshotBeforeUpdate生命周期函数;
  • 执行mutation阶段
    • 该阶段调用commitMutationEffects函数,完成副作用的执行,包括重置文本节点以及真实DOM节点的插入、删除和更新等操作;
    • 该阶段是实际执行DOM操作的阶段,是commit阶段中最耗时的部分;
  • 执行layout阶段
    • 该阶段主要执行commitLayoutEffects函数,触发componentDidMount、componentDidUpdate以及各种回调函数等;
    • 该阶段是commit的最后一步;

三、任务的处理

1、workLoop

(1)职责
        WorkLoop是React中一个关键的工作机制,它负责协调和管理组件的更新过程,通过调用performUnitOfWork去构建Fiber树。

        React通过Fiber架构将一次更新任务划分为多个子任务,每个子任务都由一个Fiber对象表示,WorkLoop的工作就是循环执行这些子任务,知道所有任务完成或遇到需要中断的情况。

        WorkLoop在React中是以异步方式运行的,它不会阻塞浏览器的主线程;使得React在更新界面时能够保持流畅的用户交互体验;同时,通过优先执行高优先级任务,有助于减少主任务进程的运行时间,提高应用的性能。

(2)Fiber树的构建

        Fiber树的构建过程分为向下遍历或向上回溯,向下遍历由beginWork函数执行,向上回溯由completeWork函数执行,performUnitOfWork函数会根据next是否存在来执行从beginWork到completeWork的过程。

        如果next一直存在,就会一直执行beginWork函数,直到next为null,表示已经没有子组件了,开始向上回溯,进入completeWork阶段。

(3)具体步骤

  • 循环执行Fiber:WorkLoop会不断从任务队列中取出Fiber对象,并执行对应的任务;
  • 检测是否需要中断循环:如果需要中断,WorkLoop会暂停当前任务,并将控制权交给其他任务;
  • 更新Fiber对象的状态:在任务执行过程中,WorkLoop会更新Fiber对象的状态,包括任务的完成情况、优先级等;
  • 循环直到所有任务完成:当所有任务完成后,WorkLoop会结束循环并退出;

2、beginWork

        beginWork是处理节点更新的入口,依据fiber节点的类型去调用不同的处理函数;

        React对current树的每个节点进行beginWork操作,进入beginWork后,首先判断当前节点及其子树是否有更新:

  • 若有更新,则会计算状态和diff后生成新的fiber,在新的fiber上标记EffectTag,最后return子节点,继续对子节点进行beginWork;
  • 如果没有子节点,则返回null;开始向上回溯,进入completeWork阶段。

3、completeWork

        completeWork主要负责处理Fiber节点的完成阶段的工作,包括更新DOM、执行副作用、标记节点状态等;effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段;

四、更新对象的数据结构

1、update对象(更新对象)

        用户交互产生更新,就会产生一个新的update对象去承载新的状态;存储更新信息;

// 函数组件:更新对象 
const update: Update\<S, A> = { 
    lane, // 更新优先级 
    action, // 更新操作 
    hasEagerState: false, // 是否有紧急更新的state 
    eagerState: null, // 紧急更新的state 
    next: (null: any), // 下一个要更新的对象 
};

2、UpdateQueue

        多个update对象会连接成一个环状链表:UpdateQueue,挂载在fiber上;

// Hook的更新队列 
const queue: UpdateQueue\<S, BasicStateAction<S>> = { 
    pending: null, 
    lanes: NoLanes, 
    dispatch: null, 
    lastRenderedReducer: basicStateReducer, 
    lastRenderedState: (initialState: any), // 最后一次渲染组件时的状态 
};

总结

React的渲染更新主要包含两个阶段:render阶段和commit阶段。

  • render阶段主要负责构建Fiber树、找出需要更新的组件、注册调度任务以及收集副作用;
  • commit阶段主要负责将在render阶段计算得到的更新应用到页面上,并同步到真实DOM中;