# React 设计模式读书笔记

# 第1章

UI= f(state)。框架内部运行机制根据当前状态渲染视图

  1. 根据自变量state变化计算出UI变化
  2. 根据UI变化执行具体的宿主环境API

副作用包括,调用DOM API,I/O操作,控制台打印等“函数调用过程中产生的,外部可观察的变化”都属于副作用。

前端框架可以分为三类

  1. 应用级框架 React
  2. 组件级框架 Vue
  3. 元素级框架 Svelte

useRef,用于在组件多次render之间缓存一个“引用类型的值”。

细粒度更新 在Vue和Mobx中使用的“能自动追踪依赖的技术”被称为“细粒度更新”。

useState

function useState(value) {
    // 保存了依赖了改state的effect
    const subs = new Set()
    const getter = () => {
        // 获取当前上下文的effect
        const effect = effectStack[effectStack.length - 1]
        if(effect) {
            // 建立订阅关系
            subscribe(effect,subs)
        }
        return value
    }
    const setter  = (nextValue) => {
        value = nextValue
        //通知所有订阅改state的effect执行
        for(const effect of [...subs]) {
            effect.execute()
        }
    }
    return [getter,setter]
}

function subscribe(effect, subs) {
    subs.add(effect)
    effect.deps.add(subs)
}
const [age] = useState(1)
const [sex] = useState(2)

useEffect(() => {

})


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

useEffect

  1. useEffect调用后,回掉函数立即执行
  2. 依赖的自变量变化后,回调函数立即执行
  3. 不需要显示指定依赖
function useEffect(callback) {
    const execute = () => {
        // 重置依赖
        cleanup(effect)
        // 将当前effect推入栈顶, 在useState的getter内部获取effectStack的栈顶effect及为“当前effect上下文”
        effectStack.push(effect)
    }
    try{
        // 执行回调
        callback()
    }finally{
        // effect 出栈
        effectStack.pop()
    }

    const effect = {
        execute,
        deps: new Set() // 该effect 依赖的state.subs
    }
    // 立即执行一次,建立订阅发布关系
    execute()
}

function cleanup(effect) {
    // 从该effect订阅的所有state对应subs中移除该effect
    for(const subs of effect.deps) {
        subs.delete(effect)
    }
    effect.deps.clear()
}



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
function useMemo(callback){
    const [s,set] = useState(callback)
    useEffect(() => set(callback()))
    return s
}
1
2
3
4
5

举例

const [name1,setName1] = useState('hello')
const [name2,setName2] = useState('wolrd')

const [showAll, triggerShowAll] = useState(true)

const whoIsHere = useMemo(() => {
    if(!showAll()){
        return name1()
    }
    return `${name1()}${name2()}`
})

useEffect(() => console.log('谁',whoIsHere()))

useEffect(() => {
    console.log('我是',name1())
})

setName1('小明')


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 1.2.2 AOT

  • AOT提前编译或预编译,宿主环境获取的是编译后的代码。
    • 编译时可以检测错误
    • 首次运行笔JIT快
    • 代码体积小于JIT
  • JIT即时编译,代码在宿主环境中编译并执行,运行时检测错误

Vue3的编译优化,在编译时可以标记模版语法为静态部分与动态部分。

❗️❗️React Forget❗️❗️

# Virtual DOM

vdom是实现“根据自变量变化计算出UI变化”的一种主流技术,工作原理如下

  • 将“元素描述的UI”转为“VDOM描述的UI” 。比如vue中的render,react中的createElement
  • 对比变化前后“VDOM描述的UI”。计算出UI中发生变化的部分。比如vue中的patch,react中的fiberNode

优点如下

  1. 相比较DOM的体积优势,dom包含大量冗余的属性。使用“包含较少冗余属性的vdom”,能减少内存开销
  2. 相较于AOT更强大的描述能力。
  3. 多平台渲染多抽象能力

# 第2章 React理念

React是一款“重运行时”的应用级框架。

CPU瓶颈,I/O瓶颈

如果js执行时间过长,导致渲染流水线绘制图片的速度跟不上屏幕刷新频率,就会造成页面掉帧。表现为页面卡顿,这就是造成CPU瓶颈的原因

react使用时间切片技术来解决这个问题,将vdom的执行过程拆分为一个个独立的宏任务,将每个宏任务的执行时间限制在一定范围内(5ms)。

I/O瓶颈对于前端来说主要是网络延迟。

  1. 优先响应人机交互,为不同的操作赋予不同的优先级
  2. 所有优先级统一调度,优先处理“最高优先级的更新”
  3. 如果当前更新,有“更高优先级的更新”产生,则中断当前更新,优先处理高优先级更新

要实现上述3个要点,需要React底层实现:

  1. 用于调度优先级的调度器
  2. 用于调度器的调度算法
  3. 支持可中断的VDOM实现

时间切片, V16及以上都是使用新架构,旧架构不能无法实现Time slice

新架构

  1. Reconciler(协调器)- VDOM的实现,负责根据自变量变化计算出UI的变化
  2. Renderer(渲染器)- 负责将变化的UI渲染到宿主环境
  3. Scheduler(调度器) - 调度任务的优先级,高优先级任务优先进入Reconciler

新架构中Reconciler中的更新流程从递归变成了“可中断的循环过程”。每次循环都会调用shouldYield判断当前Time Slice是否有剩余时间,没有剩余时间则暂停更新流程,将主线程交给渲染流水线,等待下一个宏任务再继续执行,这就是Time Slice的实现原理。

function workLoopConcurrent() {
    // 一直执行任务,直到任务执行完成中断
    while(workInProgress !== null && !shouldYield()) {
        performUnitOfWork(workInProgress)
    }
}
function shouldYield() {
    // 当前时间是否大于过期时间
    // 其中deadline = getCurrentTime() + yieldInterval
    // yieldInterval为调度器的预设时间间隔,默认是5ms
    return getCurrentTime() >= deadline
}

1
2
3
4
5
6
7
8
9
10
11
12
13

当Scheduler将调度后的任务交给Reconciler后,Reconciler最终会为VDOM元素标记各种副作用flags。 比如移动,插入,删除。

Scheduler和Reconciler的工作都是在内存中进行。只有Reconciler完成工作后,才会进入Renderer

Renderer根据“Reconciler标记各种副作用flags”执行对应的操作。

# 2.2.2 主打特性的迭代

React大体经历了4个发展时期

  1. Sync(同步) - 旧架构
  2. Async Mode(异步模式)- 新架构
  3. Concurrent Mode(并发模式)- 新架构
  4. Concurrent Feature(并发特性)- 新架构

CPU瓶颈和I/O瓶颈,并不是同时解决的。解决CPU瓶颈的方式是“架构重构”。 重构后Reconciler的工作流由“同步”变为“异步,可中断”。因此这一个时间的React被称为Async Mode。

单一更新的工作流程变为“同步、可中断”并不能解决I/O瓶颈,解决问题的关键在于“使多个更新的工作流程并发执行”。所以继续迭代为Concurrent Mode(并发模式)。

React渐进升级

  1. Legacy模式,通过ReactDOM.render(<APP/>, rootNode)创建的应用遵循该模式。默认关闭StrictMode。表现未开启并发更新
  2. Blocking模式。通过ReactDOM.creatBlockingRoot(rootNode).render(<App/>)创建的应用遵循该模式,作为从Legacy向Concurrent过度的中间模式,默认开启StrictMode,表现未开启并发更新,但是启用了一些新功能,(比如AutomaticBatching)
  3. Concurrent模式,通过ReactDOM.createRoot(rootNode).render()创建的应用遵循该模式,默认开启StrictMod,表现为开启并发特性

三种模式特性对比图

Snipaste_2023-11-04_21-59-31.png

React在18中不再提供三种开发模式,而是以“是否使用并发特性”作为“是否开启并发更新”的依据。具体说就是,开发者在v18中统一使用ReactDOM.createRoot创建应用。当不使用并发特性时,表现为未开启并发更新,但是启用了一些新功能,(比如AutomaticBatching)。当使用并发特性后表现为开启并发特性

# 2.3 Fiber架构

Fiber架构是新架构的基础。React中有三种节点类型

  1. React Element(React元素),即createElement方法的返回值
  2. React Component(React 组件),开发者可以在React中定义函数,类两种类型的Component
  3. FiberNode,组成Fiber架构的节点类型。
// React Component
const App = () => {
    return <h3>1</h3>
}
// React Element
const ele = <App/>

// 在React运行时内部,包含App对应FiberNode

ReactDOM.createRoot(rootNode).render(ele)
1
2
3
4
5
6
7
8
9
10

# 2.3.1 FiberNode的含义

FiberNode包含以下三次含义

  1. 作为架构,v15的Reconciler采用递归的方式执行,被称为Stack Reconciler。v16及以后版本的Reconciler基于FiberNode实现,被称为Fiber Reconciler
  2. 作为“静态的数据结构”,每个FiberNode对应一个React元素,用于保存React元素的类型,对应的DOM元素等信息
  3. 作为“动态的工作单元”,每个FiberNode用于保存“本次更新中该React元素变化的数据,要执行的工作(增删改查,更新ref,副作用等)”

双缓存,Fiber架构中同时存在两颗Fiber Tree,一颗是“真实UI对应的Fiber Tree”,可以理解为前缓冲区,另一颗是“正在内存中构建的Fiber Tree”,可以理解为后缓冲区。

current指“前缓冲区中的FiberNode”,workInProgress指“后缓冲区的FiberNode”,alternate指向“另一个缓冲区中对应的FiberNode” current.alternate === workInProgress workInProgress.alternate === current

# 2.3.3 mount时Fiber Tree的构建

moute时有2个情况

  1. 整个应用的首次渲染,这种情况发生在首次进入页面时
  2. 某个组件的首次渲染,当isShow为true时,Btn组件进入mount流程,此时Btn组件所在应用可能处于update流程 {isShow ? <Btn/> :null}

整个应用的首次渲染

  1. 创建fiberRootNode(情况2没有)
  2. 创建tag为3的FiberNode,代表HostRoot,后文称该FiberNode为HostRootFiber(情况2没有)
  3. 从HostRootFiber开始,以DFS深度优先搜索的顺序生成FiberNode
  4. 在遍历过程中,为FiberNode标记“代表不同副作用的flags”,以便后续在Renderer中使用

HostRoot代表“应用在宿主环境挂载的根节点”,也就是“rootElement” HostRootFiber 代表“HostRoot对应的FiberNode” FiberRootNode负责管理该应用的全局事宜,比如

  1. Current Fiber Tree与 Wip Fiber Tree之间的切换
  2. 应用中任务的过期时间
  3. 应用的任务调度信息

fiberRootNode.current === HostRootFiber

function App() {
    const [num, add] = useState(0)

    return <p onClick={() => add(num + 1)}>{num}</p>
}

const rootElement = document.getElementById('root')

ReactDOM.createRoot(rootElement).render('<App/>')

1
2
3
4
5
6
7
8
9
10
  1. fixtures: 包含一些为开发者准备的小型React测试项目

  2. packages: 所有与React相关的包存放于此

    • react-art,对应Canvas,SVG,VML
    • react-dom,对应浏览器和ssr
    • react-native-renderer,用于Native环境
    • react-noop-renderer,用户debug Fiber
    • react-test-renderer,渲染成js对象,用于测试
    • react-server,创建自定义ssr流
    • react-client,自定义的流数据模型
    • react-interactions,用于测试交互相关的内部特性,比如react的事件模型
    • react-reconciler,即Fiber Reconciler
    • react-fetch用于数据请求
    • react-is 用于测试“组件是否时某中类型”
    • react-refresh,用于在react中使用fast refresh(类似热重载,在运行时调试 React component不会丢失状态)
    • create-subscription,用于在组件内订阅React外部数据源
  3. scripts: 各种工具链脚本,比如git,jest ,eslint

# 第3章 render阶段

Reconciler工作的阶段在React内部被称为render阶段,classComponent的render函数,Function Component函数本身都在该阶段被调用

performSyncWorkOnRoot 同步更新流程 performConcurrentWordOnRoot 并发更新流程

function workLoopSync() {
    while(workInProgress !== null) {
        performUnitOfWork(workInProgress)
    }
}

function workLoopConcurrent() {
    while(workInProgress !== null && !shouldYield()) {
        perforUnitOfWork(workInProgress)
    }
}
1
2
3
4
5
6
7
8
9
10
11
function beginWork(current,workInProgress,renderLanes) {
    if (current !== null) {
        // 执行update操作
    } else {
        // 执行mount
    }

    // 根据tag不同,进入不同处理逻辑
    switch (workInProgress.tag) {
        case IndeterminateComponent // FC mount 时进入的分支,
        case LazyComponent
        case FunctionComponent // FC update时进入
        case ClassComponent
        case HostRoot
        case HostHoistable
        case HostComponent // 代表原声Element类型(比如DIV SPAN)
        ...
        ...

    }
}

Reconciler 核心代码
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
){
    if (current === null) {
        // mount时
        workInProgress.child = mountChildFibers(
            workInProgress,
            null,
            nextChildren,
            renderLanes,
        );
    } else {
    
        workInProgress.child = reconcileChildFibers(
            workInProgress,
            current.child,
            nextChildren,
            renderLanes,
        );
    }
}



export const reconcileChildFibers: ChildReconciler = createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);


function createChildReconciler(
  shouldTrackSideEffects: boolean, // 代表是否追踪副作用,即是否标记flags
): ChildReconciler {

}
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

# 3.3.3 位运算在“标记状态”中的应用


export const NoContext =     /* 未处于React上下文          */ 0b000;
const BatchedContext =       /* 处于batchedUpdates上下文   */ 0b001;
export const RenderContext = /* 处于render阶段             */ 0b010;
export const CommitContext = /* 处于commit阶段             */ 0b100;

当执行流程进入render阶段,会使用按位或标记进入对应的上下文

let executionContext = NoContext
executionContext |= RenderContext

此时可以结合按位与和NoContext来判断“是否处于某一上下文中”

(0b010 & 0b010) !== 0b000  
(executionContext & RenderContext) !== NoContext // true

(executionContext & CommitContext) !== NoContext // false


从当前上下文中移除 RenderContext 上下文,结合按位与,按位非移除标记

executionContext &= ~RenderContext

0b000 0000 0000 0000 0000 0000 0000 0010  RenderContext

对RenderContext按位非
0b111 1111 1111 1111 1111 1111 1111 1101  ~RenderContext

上述数据执行按位与 得到

0b000 0000 0000 0000 0000 0000 0000 0000



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

# 3.4 completeWork

completeWork也会根据wip.tag 区分对待,流程大概包括2个

  1. 创建或者标记元素更新
  2. flags冒泡

beginWork的reconcilerChildFibers方法用来“标记fiberNode的插入,删除,移动”。

completeWork方法的步骤1会完成 “beginWork做的标记”。当步骤1完成后,该fiberNode在本次更新中的增删改操作均已标记完成。至此,Reconciler中“标记flags”相关工作基本完成。但是距离在Renderer中间“解析flags”还有一项重要工作,就是 ”flags冒泡“

当流程经过Reconciler后,会得到一个Wip Fiber Tree,部分fiberNode被标记flags。 Renderer需要对“被标记的fiberNode对应的DOM元素”执行“flags对应的DOM操作”。那么如何高效的找到这些散落在 Wip Fiber Tree各处的“被标记的fiberNode”,这就是flags冒泡的作用。

我们知道,completeWork属于归的阶段。从叶子元素开始,流程自下而上,fiberNode.subtreeFlags记录了 该fiberNode的“所有子孙fiberNode上被标记的flags”,每个fiberNode经过如下操作,可以将子孙fiberNode中 “标记的flags”向上冒泡一层

let subtreeFlags = NoFlags
// 收集子fiberNode的子孙fiberNode中标记的flags
subtreeFlags |=  child.subtreeFlags

// 收集子fiberNode标记的flags
subtreeFlags |=  child.flags

// 附加在当前fiberNode的subtreeFlags上

completeWork.subtreeFlags |= subtreeFlags



1
2
3
4
5
6
7
8
9
10
11
12
13

completeWork 在mount时的流程总结如下

  1. 根据wip.tag进入不同处理分支
  2. 根据current!=null区分是mount还是update
  3. 对于HostComponent,首先执行createInstance方法创建对应的DOM元素
  4. 执行appendAllChildren将下一级DOM元素挂载在步骤(3)创建的DOM元素
  5. 执行finalizeInitialChildren完成属性初始化
  6. 执行bubbleProerties完成flags冒泡

# 3.4.3 update概览

update流程将完成标记属性的更新。主要是diffProperties。包括

  1. 第一次遍历,标记删除“更新前有,更新后没有”的属性
  2. 第二次遍历,标记更新”update流程前后发生改变“的属性

diffProperties,所有变化属性的key,value会保存在fiberNode.updateQueue中,同时,该fiberNode会标记Update

workInProgress.flags |= Update

在updateQueue中,数据以key,value作为数组的相邻两项。如下

['title','1','style',{'color':"red"}]




1
2
3

# 第4章 commit 阶段

Renderer工作的阶段被称为commit阶段,在commit阶段,会将各种副作用(flags标记)commit(提交)到宿主环境的Ui中。

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

  1. BeforeMutaion阶段
  2. Mutation 阶段
  3. Layout 阶段

commit阶段的起点开始于commitRoot方法的调用

commitRoot(root);

  • root代表“本次更新所属FiberRootNode”
  • root.finishedWork代表Wip HostRootFiber,即“render阶段构建的 Wip FIber Tree”的HostRootFiber

在三个子阶段执行前,需要判断本次更新是否涉及“与三个子阶段相关的副作用”

// subtreeHasEffects 代表Wip HostRootFiber的子孙元素存在的副作用flags
  const subtreeHasEffects =
    (finishedWork.subtreeFlags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
// rootHasEffect 代表 Wip HostRootFiber 本身存在的副作用flags。
  const rootHasEffect =
    (finishedWork.flags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
  if (subtreeHasEffects || rootHasEffect) {
    // 进入三个阶段
  } else {
    // 省略本次更新没有三个子阶段的副作用
  }


export const BeforeMutationMask: number =
  // TODO: Remove Update flag from before mutation phase by re-landing Visibility
  // flag logic (see #20043)
  Update |
  Snapshot |
  (enableCreateEventHandleAPI
    ? // createEventHandle needs to visit deleted and hidden trees to
      // fire beforeblur
      // TODO: Only need to visit Deletions during BeforeMutation phase if an
      // element is focused.
      ChildDeletion | Visibility
    : 0);

export const MutationMask =
  Placement |
  Update |
  ChildDeletion |
  ContentReset |
  Ref |
  Hydrating |
  Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;

// TODO: Split into PassiveMountMask and PassiveUnmountMask
export const PassiveMask = Passive | Visibility | ChildDeletion;




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

They're used by React Dev Tools.

  • Placement:当前fiberNode或子孙fiberNode存在“需要插入或移动的HostComponent或HostText”

  • Hydrating:hydrate相关

  • Update classComponet存在更新,且定义了componentDidMount或者componentDidUpdate方法;HostComponent发生属性变化;HostText发生内容变化;Fc定义了useLayoutEffect

Skipped value

  • ChildDeletion有“需要被删除的子HostComponent或子HostText”
  • ContentReset 清空HostComponent的文本内容
  • Callback:当calssComponent中的this.setState执行时,或ReactDOM.render执行时传递了回调函数参数

Used by DidCapture

  • Ref:HostComponent ref属性的创建与更新
  • Snapshot;ClassComponent存在更新,且定义了getSnapshotBeforeUpdate方法
  • Passive :Fc中定义了useEffect且需要触发回调函数

Used by Hydrating

  • Visibility:控制SuspenseComponent的子树与fallback切换时子树的显隐

# 第5章 schedule阶段

Scheduler为“Time Slice分割出的一个个短宏任务”提供了执行的驱动力

为了更灵活的控制宏任务的执行时机,React实现了一套基于lane模型的优先级算法, 并基于这套算法实现了Batched Updates(批量更新),任务打断/恢复机制等低级特性。 基于低级特性,React实现了“面向开发者”的高级特性(也叫并发特性),比如Concurrent Suspense ,useTransition

Scheduler预设了5中优先级,优先级依次降低

  • ImmediatePriority(最高优先级,同步执行)
  • UserBlockingPriority
  • NormalPriority
  • LowPriority
  • IdleOriority(最低优先级)
scheduleCallback(LowPriority, fn)
执行上述函数后返回
task = {
    expirationTIme: startTime + timeout,
    callback:fn
}

expirationTIme 由scheduleCallback执行时的时间 + timeout(不同优先级对应的timeout)

Scheduler将expirationTIme字段作为“task之间排序的依据”,值越小优先级越高,高优先级的task.callback在新的宏任务重优先执行。这就是Scheduler调度的原理


1
2
3
4
5
6
7
8
9
10
11
12

Scheduler简易实现

// 保存所有work
const workList = []
// 上一次执行perform的work对应优先级
let pervPriority  = IdlePriority

// 当前调度的callback
let curCallback

function schedule(params) {
    // 获取“当前正在调度的callback”
    const cbNode = getFirstCallbackNode()
    // 取出最高优先级的工作
    const curWork = workList.sort((w1,w2) => {
        return w1.priority - w2.priority;
    })[0]

    // 以下直到“赋值curCallback”之前都是策略逻辑
    if(!curWork) {
        // 没有work需要调度,返回
        curCallback = null
        cbNode && cancelIdleCallback(cbNode)
        return 
    }

    // 获取当前最高优先级work的优先级
    const {priority:curPriority} = curWork
    if(curPriority === prevPriority) {
        // 如果优先级相同,则不需要调度,退出调度
        return
    }

    // 准备调度当前最高优先级的work
    // 调度之前,如果有工作在进行,则中断它

    cbNode && cancelIdleCallback(cbNode);
    // 调度当前最高优先级的work
    curCallback = scheduleCallback(curPriority,perform.bind(null,curWork))


}


function perform(work,didTimeout) {
    // didTimeout 解决饥饿问题不断的插入高优先级任务,导致低优先任务无法执行
    const needSync = work.priority === ImmdiatePriority || didTimeout;
    // 同步执行 ,如果
    while((needSync || !shouldYield()) && work.count) {
        work.count --
        inserItem()
    }

    // 跳出循环,prevPriority 代表上一次执行的优先级
    prevPriority = work.priority

    if(!work.count) {
        // 从workList 中删除完成的work
        const workIndex = workList.indexOf(work)
        workList.splice(workIndex, 1)

        // 重置优先级

        prevPriority = IdlePriority
    }
    const pervCallback = curCallback
    // 调度完成后,如果callback发生变化,代表这是新的work

    schedule()

    const newCallback = curCallback
    if(newCallback && pervCallback === newCallback) {
        // callback不变,代表同一个work,只不过Time Slice时间用尽
        // 返回的函数会被Scheduler继续调用
        return perform.bind(null,work)
    }
}

// 移除task
function cancelCallback(task) {
    task.callback = null
}

// 用于“以某一优先级调度callback”
function scheduleCallback(){

}
// 用于“提示Time Slice时间是否用尽”,作为中断循环的一个依据
function shouldYield() {

}


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

注意 delay 和 expirationTime

delay代表task需要延迟执行的时间,在scheduleCallback方法中配置,“配置delay后的task”会先进入 timerQueue中。当delay对应的时间后,task会从timerQueue中取出并移入taskQueue中。

expirationTime代表“task的过期时间”,不是所有的task都会有delay。没有配置delay的task 直接进入taskQueue

expirationTime用于解决饥饿问题,也会用这个对task进行排序

使用小顶堆堆数据结构实现优先级队列,push和pop的时间复杂度是O(log n),peek操作的时间复杂度是O(1)

小顶堆特点:

  • 是一个完全二叉树,(初最后一层外,其他层的节点个数都是满的,且最后一层节点靠左排列)
  • 堆中每一个节点的值都小于等于其子树的每一个值。

宏任务的选择 requestIdleCallback问题

  • 兼容问题
  • 执行频率不稳定,受很多因素影响,比如切换tab后,之前tab注册的rIC执行的频率会大幅下降
  • 应用场景局限

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

let schedulePerformWorkUntilDeadline
if(typeof localSetImmediate === 'function'){
    // 使用setImmediate(performWorkUntilDeadine)
    schedulePerformWorkUntilDeadline = function() {
        localSetImmediate(performWorkUntilDeadline)
    }
} else if(typeof MessageChannel !== 'undefined') {
    // 使用MessageChannel实现
    let channel = new MessageChannel()
    let port = channel.port2
    channel.port1.onmessage = performWorkUntilDeadline;
    schedulePerformWorkUntilDeadline = function() {
        port.postMessage(null)
    }
} else {
    // 使用setTimeout实现
    schedulePerformWorkUntilDeadline = function() {
        localsetTimeout(performWorkUntilDeadline, 0)
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5.3 lane模型

由于React与Scheduler的优先级并不通用,因此React选出优先级提交给Scheduler前会进行转换。 Scheduler的5种优先级

  • NoPriority = 0
  • ImmdiatePriority = 1
  • UserBlockingPriority = 2
  • NormalPriority = 3
  • LowPriority = 4
  • IdlePriority = 5

Scheduler并不与React共用一套优先级体系,React有4种优先级

  • DiscreteEventPriority = SyncLane 离散事件的优先级 ,比如click,input,focus,blur,touchstart等
  • ContinuousEventPriority = InputContinuousLane 连续事件的优先级 ,比如drag,mousemove,scroll, touchmove,wheel等
  • DefaultEventPriority = DefaultLane 对应默认的优先级,比如通过计时器周期性触发更新。这种情况产生的update不属于“交互产生的update”,所以优先级是默认的优先级
  • IdleEventPriority = IdleLane,对应空闲情况的优先级

从react到Scheduler,优先级需要经过如下2次转会

  1. 将lanes转会EventPriority,涉及到方法如下,经过转换,返回到是EventPriority
function lanesToEventPriority(lanes) {
    // 获取lanes中优先级最高的lane

    let lane = getHighestPriorityLane(lanes);
    // 如果优先级高于DiscreteEventPriority,返回DiscreteEventPriority

    if(!isHighrEventPriority(DiscreteEventPriority,lane)) {
        return DiscreteEventPriority
    }
    // 如果优先级高于ContinuousEventPriority,返回ContinuousEventPriority

    if(!isHighrEventPriority(ContinuousEventPriority,lane)) {
        return ContinuousEventPriority
    }
    // 如果包含 非Idle的任务,返回DefaultEventPriority
    if(includesNonIdleWork(lane)) {
        return DefaultEventPriority
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 将EventPriority转化为Scheduler优先级,具体逻辑如下
let schedulerPriorityLevel;

// lanes转化为EventPriority

switch(lanesToEventPriority(nextLanes)) {
    case DiscreteEventPriority:
        schedulerPriorityLevel = ImmdiatePriority
        break;
    case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingPriority
        break;
    case DefaultEventPriority:
        schedulerPriorityLevel = NormalPriority
        break;
    case IdleEventPriority:
        schedulerPriorityLevel = IdlePriority
        break;
    default:
        schedulerPriorityLevel = NormalPriority
        break;
                   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22