React运行机制

fiber架构大体

在这里插入图片描述

fiberRoot对象:是整个Fiber树的入口(使用FiberRootNode构造函数实例化fiberRoot对象,使用FiberNode构造函数实例化Fiber树的第一个结点对象)
stateNode属性:存储了当前结点对应的DOM实例(用来更新到屏幕上)

总体:
React通过时间分片的方式来处理一个或多个Fiber 结点的更新任务,每次更新Fiber结点时会向调度器请求任务执行权

渲染流程三个阶段:

prerender阶段(构建fiberRoot对象,只在首次渲染)

实例化fiberRoot 对象,创建(整个应用程序的)更新队列(updateQueue)并添加到 fiberRoot 对象上
(使用FiberRootNode构造函数实例化fiberRoot对象,使用FiberNode构造函数实例化Fiber树的第一个结点对象)

render阶段(构建workInprogres树、收集副作用)

  • 每次更新Fiber结点时会向调度器请求任务执行权(任务调度器为其分配任务执行优先级),才能走到render阶段。
  • 得到任务执行权后,React 将每个 Fiber 结点作为最小工作单位,通过自顶向下逐个遍历Fiber结点,构建workInProgress树(一颗新的Fiber树,更新的计算、调用部分生命周期函数等会在这个过程中完成)。这一过程总是从顶层的HostRoot结点开始遍历,直到找到未完成工作或者需要处理的结点。
  • render阶段执行完成后,FiberRoot对象上面的current属性指向了一颗Fiber树(current 树),current 树上面的lternate属性指向了另一颗Fiber树(workInProgress树),这两颗Fiber树通过alternate属性形成了一个闭环。
  • render阶段执行完成后也会得到副作用列表Effect List(就是那些需要更新的Fiber结点集合,一般是workInProgress树的子集)

commit阶段(把effect list变成真正的dom)

将副作用链表映射到屏幕上后,将current树替换为workInProgress树(执行current=workInProgress)

  • 判断的是当前副作用是否需要更新
  • React将这个过程又分为三个步,分别是beforecommit、commit 和aftercommit。在这三个步分别调用commitBeforeMutationEffects, commitMutationEffects和commitLayoutEffects三个函数来执行具体的工作
    • commit副作用之前会先在commitBeforeMutationEffects函数中处理相应的生命周期函数
    • 正式commit副作用前,React会调用结点的getSnapshotBeforeUpdate生命周期函数
    • 在commitMutationEffects函数中正式commit副作用,将副作用更新到屏幕(会根据EffectTag的类型执行对应的操作:插入,删除,更新等)
    • commitPlacement 函数中的 node.stateNode 指向的就是当前结点对应的 DOM 元素实例。执行完 appendChildToContainer(parent, stateNode) 后应用程序中的副作用就会渲染到屏幕上
    • workInProgress树赋值给current树
    • commitLayoutEffects函数在更新内容commit到屏幕之后执行,主要负责commit阶段的一些收尾工作(对于ClassComponent 类型的结点,会调用该结点的生命周期函数,如componentDidMount和componentDidUpdate)
      在这里插入图片描述

如何构建workInProgress树

React完善workInProgress对象的过程就是将元素转换为Fiber结点的过程

  • 循环解析工作单元(执行workLoop 函数,称为工作循环)
    在工作循环的过程中,React总是先从外部根节点开始解析(按照child路线从上向下层层分析的),当解析到叶子结点时就要开始完成工作单元。完成工作单元时如果有兄弟结点时则需要继续解析兄弟结点,然后完成兄弟结点后会逐层向上完成工作单元(检查当前结点是否有兄弟结点,如果没有兄弟结点就检查父结点有没有兄弟结点)

  • 解析工作单元的逻辑

    • 匹配fiber结点的tag值调用对应的函数分别解析HostRoot(根组件元素),ClassComponent(组件元素),HostComponent(对应的真实DOM为div或span)和HostText等类型的fiber结点
    • 为结点标记effectTag属性,标识结点的更新类型(如插入、修改或者删除)
  • 完成工作单元(主要工作是收集副作用,同时处理HostComponent类型的结点,创建对应的DOM元素并将他们append到父结点上)

    • 创建DOM元素或更新DOM元素中的属性(分别是首次渲染和更新渲染。应用程序首次渲染时React为HostComponent和HostText类型的Fiber结点创建对应的DOM实例并将其赋值到结点上的stateNode属性上,而在应用程序更新渲染时React不需要为它们重新创建DOM实例,而是把新的值更新到DOM元素中)
    • 收集副作用,将副作用列表中的所有DOM实例(存在fiber结点的stateNode属性中)更新到屏幕中

在这里插入图片描述

任务调度器怎么更新任务

React中的任务都是由任务调度器scheduler统一管理(是独立于react和react-dom的模块)
任务调度器为其分配的任务执行优先级

setState() —> 为任务创建过期时间 --> 根据过期时间创建任务类型 --> 向任务调度申请执行 --> 根据过期时间将任务加入任务队列 --> 执行任务(使用requestAnimationFrame在浏览器下一次绘制之前执行函数,即用postMessage去通知执行更新任务队列,和执行任务)
在这里插入图片描述
在这里插入图片描述

var update = { 
	// 过期时间与任务优先级相关联 
	expirationTime: expirationTime, 
	suspenseConfig: suspenseConfig, 
	// tag用于标识更新的类型如UpdateState,ReplaceState,ForceUpdate等 
	tag: UpdateState, 
	// 更新内容 
	payload: null, 
	// 更新完成后的回调 
	callback: null, 
	// 下一个更新(任务) 
	next: null, 
	// 下一个副作用 
	nextEffect: null 
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
// React给任务定义了数据结构,使其具有过期时间、下一个任务和上一个任务等属性,多个任务对象链接成一个双向循环链表
// 源码位置:packages/scheduler/src/Scheduler.js 
var task = { 
	callback, // 任务的回调函数,主要用于和其他框架的链接,比如React fiber 
	priorityLevel, // 任务优先级,数值越小优先级别越高
	startTime, // 任务开始执行时间 
	expirationTime, // 任务过期时间
	next: null, // 下一个任务 
	previous: null // 上一个任务 
};

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Q:过期时间依据什么来计算?
通过任务优先级计算更新的过期时间。
React 在计算当前 Fiber 结点更新对象的过期时间时,会向其任务调度器查询该更新对象应该什么样的优先级。任务调度器会根据当前计算机资源(如 CPU)的使用情况返回合适的优先级

// computeExpirationForFiber函数的作用是为Fiber结点计算过期时间,根据当前任务优先级为Fiber结点匹配对应的过期时间
// 源码位置:packages/react-reconciler/src/ReactFiberWorkLoop.js 
function computeExpirationForFiber(currentTime, fiber, suspenseConfig) { 
	var mode = fiber.mode; // 如果处于非批量更新模式直接按照同步任务执行 
	if ((mode & BatchedMode) === NoMode) { return Sync; }
	// ... 
	var priorityLevel = getCurrentPriorityLevel() // 可获取当前更新任务的优先级
	// 根据调度器的任务优先级计算过期时间 
	switch (priorityLevel) { // ImmediatePriority对应的数值为99 
		case ImmediatePriority: expirationTime = Sync;   // 同步执行 break; // UserBlockingPriority对应的数值为98 
		case UserBlockingPriority: // 计算交互事件的过期时间  expirationTime = computeInteractiveExpiration(currentTime); break; // NormalPriority对应的数值为97,为初始值 
		case NormalPriority: // LowPriority对应的数值为96 
		case LowPriority: // 计算异步更新的过期时间  expirationTime = computeAsyncExpiration(currentTime); break; // IdlePriority对应的数值为95 
		case IdlePriority: expirationTime = Never; break; default: throw ReactError(Error('Expected a valid priority level')); 
	}
	// ... 
}


/* 任务调度器监听到浏览器当前的任务执行状态,
如果有交互事件,则将当前任务优先级(currentPriorityLevel)设置为UserBlockingPriority
也就是说接下来 Fiber 结点是要根据用户 交互行为阻塞情况选择适当时机执行更新的。
如果任务调度器返回的是 ImmediatePriority,一般情况是由于 Fiber 结点的更新任务一直得不到执行,
任务调度器将其改变为饥饿任务并为其提高优先执行权
*/
// computeInteractiveExpiration函数:计算交互事件的过期时间
// 下面是React定义的一些常量,单位均为ms 
var HIGH_PRIORITY_EXPIRATION = 500; 
var HIGH_PRIORITY_BATCH_SIZE = 100; 
var LOW_PRIORITY_EXPIRATION = 5000; 
var LOW_PRIORITY_BATCH_SIZE = 250; 
var MAX_SIGNED_31_BIT_INT = 1073741823; 
var Sync = MAX_SIGNED_31_BIT_INT; 
var Batched = Sync - 1; 
var MAGIC_NUMBER_OFFSET = Batched - 1; 
var UNIT_SIZE = 10; 
function ceiling(num, precision) { 
// | 0 为取整操作 
return ((num / precision | 0) + 1) * precision; 
}// 计算过期时间bucket 
function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs) { 
	return MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE); }
// 计算交互事件过期时间的入口函数 
function computeInteractiveExpiration(currentTime) { 
	return computeExpirationBucket(currentTime, HIGH_PRIORITY_EXPIRATION, HIGH_PRIORITY_BATCH_SIZE); 
}

  
 
  • 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
// currentTime 一般是通过window.performance.now()获取,然后转化成React中的时间 
function computeInteractiveExpiration(currentTime) { return computeExpirationBucket(currentTime, 500, 100); 
}
function computeExpirationBucket(currentTime, 500, 100) { // return 1073741821 - ceiling(1073741821 - currentTime + 500 / 10, 100 / 10);  // return 1073741821 - ceiling(1073741821 - currentTime + 50, 10);  return 1073741821 - ((1073741871 - currentTime) / 10 | 0) * 10 + 10 // 如果currentTime的值为 1073738202 过期时间为 1073738171  // 如果currentTime的值为 1073738211 过期时间依然为 1073738171  // 如果currentTime的值为 1073738212 过期时间依然为 1073738181 
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
// 源码位置:packages/scheduler/src/Scheduler.js 
// 优先级-立即执行 
// 32位系统在V8中的最大整数大小 Math.pow(2, 30) - 1 = 0b111111111111111111111111111111 
var maxSigned31BitInt = 1073741823; 
var IMMEDIATE_PRIORITY_TIMEOUT = -1; // 立即过期/饥饿的任务 
var USER_BLOCKING_PRIORITY = 250;  // 用户阻塞任务的过期时间250ms 
var NORMAL_PRIORITY_TIMEOUT = 5000;  // 正常任务的过期时间5000ms 
var LOW_PRIORITY_TIMEOUT = 10000; // 低优先级任务的过期时间10000ms 
var IDLE_PRIORITY = maxSigned31BitInt; // 最低优先级任务的过期时间(大约12天后过期) 

任务调度器会通过 performance.now() + timeout 来计算出任务的过期时间,随着时间的推移,当前时间会越来越接近这个过期时间,所以过期时间越小的代表优先级越高。如果过期时间已经比当前时间小了,说明这个任务已经过 期了还没执行,需要立马去执行

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

参考:《剖析React内部运行机制》

文章来源: blog.csdn.net,作者:sayid760,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/qq_14993375/article/details/115024741

(完)