likes
comments
collection
share

你应该知道的React源码

作者站长头像
站长
· 阅读数 7

前端研发模式的发展

石器时代 ===> JSP/JQuery ===> React/Vue

这背后的本质是一个对“DOM操作”的演化过程

虚拟DOM

本质是JS对象和DOM之间的映射,通过JS描述DOM的结构

React中的虚拟DOM

挂载阶段

React通过JSX的描述,构建出虚拟DOM树(React.createElement),然后通过ReactDOM.render实现虚拟DOM到DOM的映射(触发渲染)

更新阶段

页面的变化会先作用于虚拟DOM,虚拟DOM将在JS借助Diff算法对比出需要更新的部分,然后将这些更新作用于DOM

为什么需要虚拟DOM?

标准回答:“DOM操作慢,性能低下,导致大量的回流与重绘,JS性能更好”

虚拟 DOM 真正解决的:

  • 数据驱动视图,研发效能的提升,同时带来不错的性能
  • 增量更新伴随的批量更新
  • 跨平台,编码与视图层解耦,一次编写多端运行

你应该知道的React源码

Reconciler

关于Reconciliation

React的官方定义:

即将虚拟 DOM 映射为DOM的过程

你应该知道的React源码

Reconciler ≠ Diff,Diff仅作为Reconciler的代表性环节,其中还包括挂载、卸载、更新(包含Diff)等过程

Diff的优化策略

前面也提到了,谈到reconciler,Diff是绕不开的话题

计算机科学中对比两棵树的常见方法是递归的对节点做一一对比,但带来的时间复杂度是爆炸的O(n^3)💥

O(n^3)即两棵树嵌套循环寻找不同的节点:O(n^2),寻找到不同的节点后,需要再遍历得到最小的转换消耗O(n)得来

React对Diff的优化三要素:

  1. 分层求异

减少时间复杂度的关键

不建议做跨层级比较

  1. 类型一致才继续进行 Diff

一刀切手段,减少冗余的递归

  1. 通过key区分节点

优化同层级节点位置交换时,第二点优化带来的弊端

你应该知道的React源码

关于batchUpdate

解决多次setState,导致的生命周期钩子多次执行带来的低下性能

setState的工作流

你应该知道的React源码 enqueueSetState

  1. 将state放入组件状态队列中
  1. 调用enqueueUpdate

enqueueUpdate

决定是否进行批量更新

这里不得不先谈谈Transaction机制,这是React中的一种黑盒机制,可用来封装任意方法,并绑定函数执行前后需要运行的方法。

方法:

const handleClick = ()=>{
    this.setState({
        count:this.state.count+1
    })
}

经过transaction机制:

const handleClick = ()=>{
    isBatchingUpdates = true
    this.setState({
        count:this.state.count+1
    })
    isBatchingUpdate = false
}

所以,这也是为什么当setTimeOut中执行setState会呈现出同步的效果

StackReconciler的局限性到底在哪

背景: JavaScript 线程 与浏览器渲染线程互斥(串行) (JS可以操作DOM,若二者同时工作,渲染结果难以预测)

核心问题:stackReconciler对主线程的超时占用(同步递归过程,无法打断)

根本原因:stackReconciler机制的Diff算法,是树的深度优先遍历过程

Fiber如何破局

Fiber 的“增量渲染”,分解渲染任务,分散到多个帧内

⚠️注:Fiber ≠ 异步,而是一种兼容同步、异步渲染的数据结构

核心

可中断、可恢复、优先级

核心架构:

stack

你应该知道的React源码

fiber

你应该知道的React源码

泛生命周期:

stack

你应该知道的React源码

fiber 你应该知道的React源码

ReactDOM.render首次渲染链路

首先看看首次渲染的入口ReactDOM.render渲染链路

调用栈:

你应该知道的React源码

关键信息:

performSyncWorkOnRoot开启了render阶段

commitRoot开启了真实DOM的渲染过程即commit阶段

初始化阶段

基本实体创建(如下图)

你应该知道的React源码

调度updateContainer

  • 获取当前Fiber节点的lane
  • 结合lane创建当前Fiber节点的update对象并入队
  • 调度当前节点

render阶段

调度performSyncWorkOnRoot开启同步任务

beginWork + completeWork是一个模拟递归的过程,同步模式下与stack Reconciler相似

workInProgress Node

即rootFiber节点的副本

workLoopSync

通过while循环判断workInProgress是否为空,继而执行performUnitOfWork(workInProgress)

performUnitOfWork

  • 触发beginWork的调用,创建新Fiber节点,放入workInProgress tree
  • 若新Fiber节点不为空,则更新workInProgress

workInProgress tree

render 你应该知道的React源码

commit

你应该知道的React源码

beginWork(递)

beginWork 创建Fiber节点

调用reconcileChildren,首次渲染时生成当前节点的子节点

reconcileChildren

effectTag为记录副作用类型的标记

首次渲染直接生成子Fiber节点,更新阶段为子Fibe节点打上effectTag

completeWork(归)

completeWork 负责将Fiber映射为DOM节点

performUnitOfWork函数中,若next为空(即当前为叶子节点),则调用completeWork

关键动作:

  • 创建DOM节点
  • 将DOM节点插入DOM树中
  • 为DOM节点设置属性
  • 创建好的DOM节点赋值给workInProgress.stateNode

更新阶段则会将当前节点的副作用链(effectList)插入到父节点对于的副作用链中

effectList

带effectTag属性的Fiber节点链表

维护firstEffect和lastEffect属性,effectList最终仅挂载在App的父节点(rootFiber)上

commit阶段

绝对同步的过程

在performSyncWorkOnRoot中被调用

  • before mutation
  • mutation 该阶段渲染DOM
  • layout 将fiberRoot.current指向workInProgress tree

双缓存机制

current树workInProgress树之间的转换

下面个例子:

首次渲染

render阶段完成后,commit执行前

你应该知道的React源码

commit执行后

你应该知道的React源码

更新

commit执行前

你应该知道的React源码

commit执行后

你应该知道的React源码

update对象

大致流程如下:

你应该知道的React源码

首次渲染

updateContainer

  • 创建update
  • enqueueUpdate将update入队
  • 开始调度(scheduleUpdateOnFiber)

更新

dispatchAciton

  • 创建update对象
  • 将update入队fiber.updateQueue
  • 开始调度当前fiber

小结

总结一下几个关键函数的作用,以便于对于react有一个宏观上的认知

workLoopSync

作用:循环调用performUnitOfWork,并赋值给workInProgress,一直到workInProgress为空

performUnitOfWork

作用:

递阶段,调用beginWork

符合条件时进入归阶段,调用competeWork。

递归交替,直到回到root节点

beginWork

作用:主要调用reconcileChildren方法。

渲染阶段生成子Fiber节点。

更新阶段为子Fiber节点添加effectTag属性

completeWork

作用:

渲染阶段将FIber映射为真实的DOM节点。即创建DOM节点并为其添加属性后赋值给workInProgress.stateNode。

更新阶段则是不断生成effectList链表,最终挂载在rootFiber上。

Scheduler

时间切片是如何实现的?

function workLoopConcurrent(){
    while(workInProgress !== null && !shouldYield()){
        performUnitOfWork(workInProgress)
    }
}

shouldYield

由Scheduler提供,判断当前时间切片是否到期

时间切片的长度:根据浏览器性能计算得出

优先级调度如何实现

  • startTime:任务开始时间
  • expirationTime:越小优先级越高
  • timerQueue:一个以startTime为值的小顶堆(startTime>now,即待执行任务)
  • taskQueue:一个以expirationTime为值的小顶堆(startTime<now,即已过期任务)

取出timerQueue的堆顶,当任务到期后加入taskQueue中,触发对flushWork的调用

flushWork中将调用workLoop,workLoop将会逐一执行taskQueue中的任务,直到调度过程被暂停(时间切片到期)或任务清空。