【React源码】2.源码架构

195 阅读4分钟

在学习React源码之前,需要有一个大概的源码地图,知道React渲染的大致流程和架构,才能从上帝视角看React是怎么更新的。

架构

React的核心可以用ui=fn(state)来完成,也就是把state数据反映到ui上。更详细可以是

const state = reconcile(update)
const UI = commit(state);

上面的fn分为以下几个部分:

  • Scheduler(调度器):排序优先级,让优先级高的任务进入reconcile
  • Reconciler(协调器):找出哪些节点发生了变化,并打上不同的Flags(就版本叫Tag)
  • Renderer(渲染器):将Reconciler中打好标签的节点渲染到视图上

React渲染流程图(极简)

流程图.jpg

perfercomce.png

概念

这的很多概念只是做初步了解,后面源码学习中会有更深入的理解

jsx

  • jsx是js的语法扩展,React通过babel词法解析,将jsx转换成React.createElement,React-createElement函数返回vdom对象(内存中用来描述dom阶段的对象),所有jsx本质就是React.createElement的语法糖,用声明式的编写我们想要组件呈现什么样子的UI效果

jsx --(babel)--> React.createElement --(执行)--> vdom

Fiber双缓存

  • Fiber对象上保存了包括节点的属性类型dom等,Fiber通过childsiblingreturn来形成Fiber树
  • 同时还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,还有和副作用相关的信息
  • 双缓存是指存在两个Fiber,current Fiber树描述了当前呈现的dom树,workInProgress Fiber是正在更新的Fiber树,两棵树都是在内存中运行的,在workInProgress Fiber构建完成后会将它作为current Fiber应用到dom上 Fiber双缓存
function App() {
    return <h1>
        <p>111</p>
        max
    </h1>
}

alternate.jpg

Scheduler调度任务

需要实现一个异步可中断的更新

// ReactFiberWorkLoop.old.js
function workLoopConcurrent() {
    while(workInProgress !== null && !shouldYield()) { // shouldYield 判读是否暂停任务
        workInProgress = performUnitOfWork(workInProgress)
    }
}

在Scheduler中的每个任务的优先级是通过过期时间表示的。过期的在taskQueue中,没有过期的在timerQueue中,都是小顶堆

111.jpg

reconciler

发生在render阶段,render阶段分别为节点执行 beginWorkcompleteWork,或者计算state,对比节点的差异,为节点赋值相应的EffectFlags(标记dom节点的增删改)

简单概括就是:会创建更新Fiber节点

  • 在mount的时间根据jsx生成Fiber对象
  • 在update的时候会根据最新的state形成的jsx对象和current Fiber对象对比构建workInProgress Fiber树,这个对比的过程就是diff算法
  • 会给这些Fiber打上Flags标签
//ReactFiberFlags.js
export const Placement = /*             */ 0b0000000000010; // 替换
export const Update = /*                */ 0b0000000000100; // 更新
export const PlacementAndUpdate = /*    */ 0b0000000000110; // 替换和更新
export const Deletion = /*              */ 0b0000000001000; // 删除

遍历Fiber树类型dfs过程,“捕获”阶段发生在beginWork函数中,主要工作是创建Fiber节点,计算state和diff算法,“冒泡”阶段发生在completeWork中,该函数主要是做一些收尾工作,例如处理节点的props和形成一条EffectList链表,该链表是被标记了更新的节点形成的链表

  • beginWork
    • 捕获阶段
    • 创建Fiber节点
    • 计算state
    • diff算法
  • completeWork
    • 处理props
    • 生成EffectList链表 如下代码
function App() {
  const [count, setCount] = useState(0);
  return (
   	 <>
      <h1
        onClick={() => {
          setCount(() => count + 1);
        }}
      >
        <p title={count}>{count}</p>
        max
      </h1>
    </>
  )
}

如果p和h1节点更新了则effectList如下,从rootFiber->h1->p,顺便说下

  • fiberRoot:是整个项目的根节点,只存在一个
  • rootFiber:是应用的根节点,可能存在多个,例如多个ReactDOM.render(<App />, document.getElementById("root"));创建多个应用节点

effectList

effectlist.jpg

  • 这里的h1为什么也在effectList中呢?

因为h1标签接收的onClick是一个匿名函数,每次render的时候,接收的都是一个新的地址,所以有变化

  • 为什么firstEffect指向的是p,而不是h1 因为行程effectList是在completeWork阶段做的,是从下往上走

Lane模型

处理优先级

  • 2022.3.17:现在也不理解

renderer(commit阶段)

遍历EffectList,执行对应的dom操作或部分生命周期。不同平台Renderer不同,浏览器对应的就是react-dom

分别用三个函数处理EffectList上的几点:commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects

commit.jpg