React 设计原理

coderljw 2024-10-13 大约 8 分钟

# 1. 前端框架原理概览

  • AOT:代码在构建时,被称为 AOT(Ahead Of Time,提前编译或预编译),宿主环境获得的是编译后的代码。

  • JIT:代码在宿主环境执行时,被称为 JIT(Just In Time,即时编译),代码在宿主环境中编译并执行。

  • 如果模板上有错误。使用 AOT 时,会在编译阶段报出;而 JIT 需要等到在浏览器中执行到错误代码时,才会报出。

  • 使用 JIT 的应用在首次加载时慢于使用 AOT 的应用,因为其需要先编译代码;而使用 AOT 的应用已经在构建时完成编译,可以直接执行代码。

  • 使用 JIT 的应用代码体积可能大于使用 AOT 的应用,因为其在运行时会增加编译器代码。

框架 重渲染层级分类 编译技术分类
Svelte 元素级框架 极致的编译时框架
Vue 组件级框架 拥有两者的特效(AOT 和 VDOM),比较均衡
React 应用级框架 极致的运行时框架

# 2. React 理念

  • Fiber Reconciler 采用双缓冲的更新机制。对于每个应用,同时存在两颗 Fiber Tree,Current Fiber Tree 对应真实 UI,Wip Fiber Tree 对应“正在内存中构建的 UI”。

# 3. render 阶段

  • Fiber Reconciler 是从 Stack Reconciler 重构而来,通过遍历的方式实现可中断的递归,因此 performUnitOfWork 的工作可以分为两部分:“递”和“归”。

    • “递”阶段会从 HostRootFiber 开始向下以 DFS 的方式遍历,为“遍历到的每个 fiberNode”执行 beginWork 方法。该方法会根据传入的 fiberNode 创建下一级 fiberNode。当遍历到叶子元素(不包含子 fiberNode)时,performUnitOfWork 就会进入“归”阶段。
    • “归”阶段会调用 completeWork 方法处理 fiberNode。当某个 fiberNode 执行完 completeWork 方法后,如果其存在兄弟 fiberNode(fiberNode.sibling !== null),会进入其兄弟 fiberNode 的“递”阶段。如果不存在兄弟 fiberNode,则进入父 fiberNode 的“归”阶段。“递”阶段和“归”阶段会交错执行直至 HostRootFiber 的“归”阶段。至此,render 阶段的工作结束。
    function App() {
      return (
        <div>
          Hello
          <span>World</span>
        </div>
      )
    }
    
    /**
     * 1. HostRootFiber beginWork(生成 App fiberNode)
     * 2. App fiberNode beginWork(生成 DIV fiberNode)
     * 3. DIV fiberNode beginWork(生成 'Hello'、SPAN fiberNode)
     * 4. 'Hello' fiberNode beginWork(叶子元素)
     * 5. 'Hello' fiberNode completeWork
     * 6. SPAN fiberNode beginWork(叶子元素)
     * 7. SPAN fiberNode completeWork
     * 8. DIV fiberNode completeWork
     * 9. APP fiberNode completeWork
     * 10. HostRootFiber completeWork
     */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
  • beginWork 会根据当前 fiberNode 创建下一级 fiberNode,在 update 时标记 Placement(新增、移动)、ChildDeletion(删除)。completeWork 在 mount 时会构建 DOM Tree,初始化属性,在 update 时标记 Update(属性更新),最终执行 flags 冒泡。

# 4. commit 阶段

  • render 阶段流程可能会被打断,而 commit 阶段一旦开始就会同步执行直到完成。

    • 开始前准备的工作,比如判断 “是否有副作用需要执行”
    • 处理副作用(BeforeMutation 阶段、Mutation 阶段、Layout 阶段)
    • 结束后的收尾工作,比如调度新的更新
  • Fiber Tree 的切换会在 Mutation 阶段完成后,Layout 阶段还未开始时执行。

  • Fiber 架构的早期版本并没有使用 subtreeFlags,而是使用一种被称为 Effects list 的链表结构保存 “被标记副作用的 fiberNode”。虽然 subtreeFlags 遍历子树的操作需要比 Effect list 遍历更多节点,但是 v18 中 Suspense 的行为恰恰需要遍历子树。React Effects List 大重构,是为了他? (opens new window)

    function App() {
      return (
        <Suspense fallback={<h3>loading...</h3>}>
          <LazyCpn />
          <Sibling />
        </Suspense>
      )
    }
    
    function Sibling() {
      useEffect(() => {
        console.log('Sibling effect')
      }, [])
    
      return <h1>Sibling</h1>
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!-- 旧Suspense,并且会打印 Sibling effect -->
    <div id="root">
      <h1 style="display: none !important;">Sibling</h1>
      <h3>loading</h3>
    </div>
    
    <!-- 新Suspense -->
    <div id="root">
      <h3>loading</h3>
    </div>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# 5. schedule 阶段

  • 为了更灵活地控制宏任务的执行时机,React 实现了一套基于 lane 模型的优先级算法,并基于这套算法实现了 Batched Update(批量更新)、任务打断/恢复机制等低级特性。

  • Scheduler 预置了五种优先级,优先级依次降低:

    • ImmediatePriority(最高优先级,同步执行)
    • UserBlockingPriority
    • NormalPriority
    • LowPriority
    • IdlePriority(最低优先级)
  • 为了解决饥饿问题,当一个 work 长时间未执行完,随着时间推移,当前时间离 work.expirationTime 越近,代表 work 优先级越高。当 work.expirationTime 小于当前时间,代表该 work 过期,表现为 didTimeout === true,过期 work 会被同步执行。

  • 使用 “小顶堆” 实现优先级队列。因为在 Scheduler 中经常需要获取 timerQueue、taskQueue “排序依据最小的值” 对应 task,所以选用 “小顶堆” 这一数据结构。

  • 宏任务的选择。

    • requestIdleCallback 未采用,rIC 的设计初衷是 “能够在事件循环中执行低优先级工作,减少对动画、用户输入等高优先级事件的影响”。这意味着 rIC 的应用场景被局限在 “低优先级工作” 中。这与 Scheduler 中 “多种优先级的调度策略” 不符。

      • 浏览器兼容性。
      • 执行频率不稳定,受很多因素影响。(比如:当切换浏览器 Tab 后,之前 Tab 注册的 rIC 执行的频率会大幅降低)
      • 应用场景局限。
    • requestAnimationFrame 未采用,由于 rAF 的执行取决于 “每一帧 Paint 前的时机”,即 “它的执行与帧相关”,执行频率并不高,因此 Scheduler 并没有选择它。满足条件的备选项应该在一帧时间内可以执行多次,并且执行时机越早越好。

    • setImmediate,在支持 setImmediate 的环境(Node.js、旧版本 IE)中,Scheduler 使用 setImmediate 调度宏任务。

      • 不同于 MessageChannel,他不会阻止 Node.js 进程退出。
      • 相比 MessageChannel,执行时机更早。
    • MessageChannel,在支持 MessageChannel 的环境(浏览器、Worker)中,使用 MessageChannel 调度宏任务。

    • setTimeout,其余情况则使用 setTimeout 调度宏任务,如果使用 setTimeout 调度新的宏任务,那么 Time Slice 之间会用 “被浪费的时间”。考虑 setTimeout 存在最小延迟时间,且在一帧中其执行时机晚于上诉两个 API,所以 “被浪费的时间” 应该略大于最小延迟时间。

  • v18 的 Batched Updates(批量更新)被称为 Automatic Bathing(自动批量更新),是因为在 v18 中,Batched Updates 是由 “基于 lane 模型的调度策略” 自动完成的。之前版本的 React 中则是半自动批量更新与手动批量更新。

  • 不管是同步还是异步,所有更新都会经历 schedule 阶段,v18 将自动批量更新交由 schedule 阶段的调度策略完成,实现了自动化。具体来讲,v18 将 “优先级” 作为自动批量更新的依据。对于 SyncLane,更新会在微任务队列中被调度执行。对于非 SyncLane,当有 work 正在调度时产生了 “同优先级” 的新 work,新 work 会命中该逻辑,不会产生新的调度。这意味着在上诉 “setTimeout 回调中触发多次更新” 的场景中,第一次更新会产生调度,后续更新都会命中上诉逻辑,不会产生新的调度。这就是 v18 中 “自动批量更新” 的实现原理。

  • React 同时存在 “使用微任务调度的同步调度策略” 与 “使用宏任务调度的并发调度策略”,这使得其自动批量更新也有两种可能。

    let count = 0
    let dom
    const onClick = () => {
      // 三次更新合并为一次
      count++
      count++
      count++
      console.log('同步结果:', dom.innerText)
      Promise.resolve().then(() => {
        console.log('微任务结果:', dom.innerText)
      })
      setTimeout(() => {
        console.log('宏任务结果:', dom.innerText)
      })
    }
    
    /**
     * Vue3
     * 同步结果:0;微任务结果:3;宏任务结果:3
     *
     * Svelte
     * 同步结果:0;微任务结果:3;宏任务结果:3
     *
     * Legacy Mode React
     * 同步结果:0;微任务结果:3;宏任务结果:3
     *
     * Concurrent Mode React
     * 同步结果:0;微任务结果:0;宏任务结果:3
     */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

# 6. 状态更新流程

  • 在触发更新时存在一种性能优化策略 —— eagerState。进入 render 阶段后存在一种性能优化策略 —— bailout。React 内部让人迷惑的性能优化策略 (opens new window)

    function App {
      const [num, updateNum] = useState(0);
      console.log('App render', num);
    
      useEffect(() => {
        setInterval(() => {
          updateNum(1);
        }, 1000)
      }, [])
    
      return <Child/>;
    }
    
    function Child() {
      console.log('child render');
      return <span>child</span>;
    }
    
    /**
     * App render 0(未命中策略)
     * child render
     * App render 1(未命中策略)
     * child render
     * App render 1(命中bailout)
     * (命中eagerState)
     * (命中eagerState)
    */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

# 7. FC 与 Hooks 实现

  • 代数效应 (opens new window)是函数式编程中的一个概念,用于 “将副作用从函数调用中分离”。

    let workInProgressHook // 当前工作的 Hook
    let isMount = true // true 代表首次挂载
    
    // App 组件对应的 fiber
    const fiber = {
      memoizedState: null, // Hooks 链表
      stateNode: App, // render
    }
    
    // 调度更新
    function schedule() {
      // 更新前将 workInProgressHook 重置为第一个 Hook
      workInProgressHook = fiber.memoizedState
      // render
      const app = fiber.stateNode()
      isMount = false
      return app
    }
    
    // 更新 Hook(updateNum)
    function dispatchAction(queue, action) {
      // 更新队列:环状单向链表(方便取第一个元素和添加元素)
      const update = {
        action, // updateNum 回调
        next: null, // 下个 update
      }
      // 创建 update 单向环状链表
      // queue.pending 为最后一个添加的元素,queue.pending.next 为第一个添加的元素
      if (queue.pending === null) {
        update.next = update
      } else {
        update.next = queue.pending.next
        queue.pending.next = update
      }
      queue.pending = update
    
      schedule()
    }
    
    function useState(initialState) {
      let hook
    
      if (isMount) {
        // 首次挂载
        hook = {
          // 更新队列
          queue: {
            pending: null,
          },
          memoizedState: initialState, // Hook 当前状态(num)
          next: null, // 下个 Hook
        }
        // 创建 Hooks 单向链表
        if (!fiber.memoizedState) {
          fiber.memoizedState = hook
        } else {
          workInProgressHook.next = hook
        }
        workInProgressHook = hook
      } else {
        // 更新
        hook = workInProgressHook
        workInProgressHook = workInProgressHook.next // 移动到下一个 Hook
      }
    
      let baseState = hook.memoizedState // 保存更新前状态
      // 判断当前 Hook 更新队列是否为空
      if (hook.queue.pending) {
        let firstUpdate = hook.queue.pending.next
    
        // 执行更新队列函数,获取最后的 Hook 状态
        do {
          const action = firstUpdate.action
          baseState = action(baseState)
          firstUpdate = firstUpdate.next
        } while (firstUpdate !== hook.queue.pending)
    
        // 清空更新队列
        hook.queue.pending = null
      }
      // 更新 Hook 当前状态
      hook.memoizedState = baseState
    
      return [baseState, dispatchAction.bind(null, hook.queue)]
    }
    
    function App() {
      const [num, updateNum] = useState(0)
    
      console.log(`${isMount ? 'mount' : 'update'} num: `, num)
    
      return {
        click() {
          updateNum(num => num + 1)
        },
      }
    }
    
    window.app = schedule()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
以父之名
周杰伦.mp3