React: fiber

requestAnimationFrame 和 requestIdleCallback

通常情况下,显示设备屏幕刷新率为 60Hz,即 1 秒 60 帧,折合每帧平均时间约 16.6ms。
浏览器无法严格控制每16.6ms执行一帧,而是通过vSync标识符动态控制每帧执行时间,(非阻塞情况下)大约为16.6ms左右。
在高刷新率显示设备上每帧时间会缩短。
浏览器对每一帧的执行和渲染的流程分为7个阶段,如下图:
在这里插入图片描述

  • 第1-3阶段:js执行阶段,分别为用户事件回调、定时器回调、窗口变更事件回调
  • 第4阶段:rAF阶段,即window.requestAnimationFrame回调执行阶段
  • 第5-6阶段:页面渲染阶段,前3阶段js执行时间过长将阻塞渲染,导致页面卡顿
  • 第7阶段:帧空闲阶段,即window.requestIdleCallback回调执行阶段。若前6阶段运行时间超过16.6ms,则该回调不会执行

requestIdleCallback调用示例:

/* 
为requestIdleCallback绑定回调函数
第一个参数:回调函数,必选
第二个参数:配置选项,可选
配置选项中timeout为强制执行超时时间毫秒数
如果超过指定时间浏览器仍没有空闲则强制执行
*/
const handle = window.requestIdleCallback(callback, {timeout: 1000});
// callback方法接收一个deadline对象参数
function callback(deadline){
	// deadline.timeRemaining() 返回当前帧剩余空闲时间毫秒数
	console.log(deadline.timeRemaining())
	// deadline.didTimeout 返回当前回调是否已经触发超时
	console.log(deadline.didTimeout)
}
// 取消requestIdleCallback回调
window.cancelIdleCallback(handle) 

注意:
目前只有chrome浏览器支持requestIdleCallback,React并不使用requestIdleCallback进行调度,而是使用MessageChannel实现了schedule package(调度包),概念上模拟requestIdleCallback。

fiber 和 Virtual DOM

fiber是React最小工作单元,每一个Virtual DOM对应一个fiber工作单元(但不是每一个fiber工作单元都有对应的Virtual DOM)。
在React应用中,通过createElement方法或JSX语法创建Virtual DOM,通过ReactDOM的render方法将Virtual DOM渲染到页面。
在这个过程中,render方法会创建根fiber,将传入的Virtual DOM作为根fiber的children,将传入的页面容器元素作为对应真实DOM,将根fiber作为下一工作单元,开启fiber执行工作循环。

workLoop

workLoop即负责运行fiber工作单元调度的fiber执行工作循环。
React通过fiber架构 将一次更新任务划分为以一个fiber作为一个工作单元的多个子任务,并对子任务设置执行优先级,使用schedule package对子任务执行进行调度,优先执行重要任务,异步执行低优先级任务,减少主任务进程运行(阻塞)时间。
在每一次帧空闲阶段,workLoop会运行一个或多个fiber工作单元,然后让出调度权给浏览器来处理用户事件等高优先级工作,在下一帧空现阶段,workLoop会从上一次帧空闲阶段结束时停止的fiber继续运行,直到fiber tree全部fiber运行完成,或者(如触发重绘时)丢弃之前没运行完的fiber tree,重新从根fiber开始运行并生成新的fiber tree,如下图:
在这里插入图片描述
每一个fiber工作单元运行时会执行三项核心工作:

  1. 为 当前 fiber 创建 真实DOM(如果没有创建)
  2. 执行 reconcileChildren,遍历 children Virtual DOM,创建 对应的 fiber,生成 fiber tree
  3. 根据 fiber tree 返回下一个工作单元

随着工作循环执行,会生成完整Virtual DOM树和对应的完整fiber tree。
在组件首次渲染到页面时,React 把对应容器元素创建的根fiber和根据Virtual DOM生成的子fiber为组成一个完整fiber tree。
在之后每次组件更新时,React 把当前Virtual DOM树和上一次渲染创建的fiber tree通过diff算法生成一个新的fiber tree。

fiber tree

fiber代码示例:

// 使用JSX语法<span key="1"></span>创建Virtual DOM,其对应fiber实例可能如下
{
    stateNode: new HTMLSpanElement, // 真实DOM
    type: "span", // 组件类型:原生组件为HTML标签名,类组件为构造函数,函数组件为函数自身
    tag: 5, // fiber类型:原生类组件为5,类组件为1
    alternate: null, // 上一次render生成的fiber
    key: "1", // key属性用于同级元素列表diff优化
    updateQueue: null, // 用于状态更新,回调函数,DOM更新的队列
    memoizedState: null,// 用于创建输出的fiber状态
    memoizedProps: {children: 0},// 上一次render创建的props
    pendingProps: {children: 0}, // 当前已更新且待应用到子组件或DOM元素的props
    effectTag: 0, // effect类型
    nextEffect: null // 下一个effct指针,用于effect串联为effect list
    ...
}

所有fiber工作单元以链表形式组成fiber tree,其中每一个fiber工作单元通过child、sibling、return三个指针进行关联。
在这里插入图片描述
fiber tree 结构示例:

const A1 = {key:'A1', tag:'div'}
const B1 = {key:'B1', tag:'div'}
const C1 = {key:'C1', tag:'div'}
const C2 = {key:'C2', tag:'div'}
const B2 = {key:'B2', tag:'div'}

A1.child = B1;
B1.return = A1

B1.child = C1;
C1.return = B1

C1.sibling = C2;
C2.return = B1;

B1.sibling = B2
B2.return = A1

const fiberTree = A1;

reconcile 阶段

在运行fiber工作单元过程中,会执行 reconcileChildren方法,React会进行diffing计算,遍历当前虚拟DOM树和上一次更新生成的fiber tree并进行比对,找出需要调用getDerivedStateFromProps和shouldComponentUpdate生命周期函数的组件并执行,找出需要调用其他生命周期函数的组件,找出需要更新的真实DOM,创建出新的fiber tree,在对应fiber上创建effect,将全部effect构建成effect list链表。
该阶段不会导致任何用户可见的更改(如真实DOM更新),允许中断执行或通过调度实现异步执行。

effect list

在构造 fiber tree过程中,React将需要执行的生命周期函数、真实DOM更新操作等effect保存到对应fiber中,所有的effect以链表形式保存为effect list。
在commit 阶段,通过effect list获取全部effect进行批量更新。
在这里插入图片描述

// 生成 effect list
// effect list 基于根节点生成,每个fiber的firstEffect指向其第一个子fiber,lastEffect指向其最后一个子effect,nextEffect指向下一个effect,组成单链表结构
function collectFiberEffect(fiber){
	let returnFiber = fiber.return;
	if(returnFiber){ // 有父级
		if(!returnFiber.firstEffect){ // 父级未定义firstEffect 
			returnFiber.firstEffect = fiber.firstEffect // 定义父级fiber的firstEffect为当前fiber的firstEffect 
		}
		if(fiber.lastEffect){ // 当前fiber已经定义lastEffect
			if(returnFiber.lastEffect){ // 父级已经定义lastEffect
				// 父级lastEffect已经不是最后一个,
				returnFiber.lastEffect.nextEffect = fiber.lastEffect //定义父级fiber的lastEffect的nextEffect为当前fiber的lastEffect
			}else{ // 父级未定义lastEffect 即 当前为第一个effect
				returnFiber.firstEffect = fiber // 定义firstEffect 
			}
		}
		const effectTag = fiber.effectTag
		if(effectTag){ // 有effect
			if(returnFiber.lastEffect){ // 已经定义lastEffect
				// returnFiber.lastEffect已经不是最后一个,定义其nextEffect为当前effect
				returnFiber.lastEffect.nextEffect = fiber // 定义nextEffect 
			}else{ // 未定义lastEffect 即 当前为第一个effect
				returnFiber.firstEffect = fiber // 定义firstEffect 
			}
			returnFiber.lastEffect = fiber // 更新lastEffect 到当前 effect
		}
	}
}

commit 阶段

当所有fiber工作单元运行完成后,新的fiber tree、收集了全部effect的effect list链表、更新后的Virtual DOM树 也随之构建完成,workLoop会将新的fiber tree从根fiber进行commit。
在commit阶段,React将遍历执行effect list中全部effect,通过effectTag执行相应的真实DOM更新或生命周期函数运行操作:

  • Snapshot:执行getSnapshotBeforeUpdate生命周期函数
  • Placement:执行componentDidMount生命周期函数,创建真实DOM
  • Update:执行componentDidUpdate生命周期函数,更新真实DOM
  • Deletion:执行componentWillUnmount生命周期函数,删除真实DOM

由于使用effect list实现了仅对需要更新的fiber的遍历,避免了遍历整个fiber tree,减少了实际更新的真实DOM,从而实现性能提升。
该阶段会将发生改变的真实DOM渲染或更新到页面,必须同步执行且一次性执行完成不可中断。

实现原理示例:

// React.createElement方法
function createElement(type, props, ...children) {
  // 返回创建的Virtual DOM
  return {
    type,
    props: {
      ...props,
       children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child) // 文本节点类型
      ),
    },
  }
}
// 创建文本节点类型Virtual DOM
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

const React = { createElement }

// 接收key,返回key是否以"on"开头,用来简单判断是否是事件方法属性
const isEvent = key => key.startsWith("on")

// 接收key,返回key是否不等于"children"且不是事件方法属性
const isProperty = key => key !== "children" && !isEvent(key)

// 从fiber创建真实DOM
function createDom(fiber) {
  	const dom = // 创建真实DOM
    fiber.type === "TEXT_ELEMENT" // 判断虚拟DOM类型是否为文本节点
      ? document.createTextNode("")
      : document.createElement(fiber.type)
  	// 更新真实DOM
  	updateDom(dom, {}, fiber.props)
	// 返回真实DOM
  	return dom
}

// 接收prev和next,返回一个函数 :接收key,返回next的key是否不等于prev的key
const isNew = (prev, next) => key => prev[key] !== next[key]

// 接收prev和next,返回一个函数:接收key,返回next中是否不存在key
const isGone = (prev, next) => key => !(key in next)

// 更新真实DOM
function updateDom(dom, prevProps, nextProps){
  // 移除新props中没有或变更的事件方法属性
  Object.keys(prevProps)
    .filter(isEvent) // 选择事件方法属性
    .filter(
      key =>
        !(key in nextProps) || // 新props中不存在
        isNew(prevProps, nextProps)(key) // 新props和老props不同
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener( // 取消事件绑定
        eventType,
        prevProps[name]
      )
    })

  // 移除老props中存在,新props中不存在的属性
  Object.keys(prevProps) // 遍历 老props
    .filter(isProperty) // 过滤children属性和事件方法属性
    .filter(isGone(prevProps, nextProps)) // 选择新props中不存在的属性
    .forEach(name => {
      dom[name] = "" // 置空
  })

  // 设置新props中新增或变更的属性
  Object.keys(nextProps) // 遍历新props
    .filter(isProperty) // 过滤children属性和事件方法属性
    .filter(isNew(prevProps, nextProps))// 选择新props和老props值不同的属性
    .forEach(name => {
      dom[name] = nextProps[name] // 重新赋值
  })

  // 重新设置 事件方法属性
  Object.keys(nextProps)
    .filter(isEvent) // 选择事件方法属性
    .filter(isNew(prevProps, nextProps))// 选择新props和老props值不同的属性
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener( // 添加事件绑定
        eventType,
        nextProps[name]
      )
    })
}

// 下一个fiber工作单元
let nextUnitOfWork = null
// 当前工作中的根fiber
let wipRoot = null
// 上一次提交的根fiber
let currentRoot = null
// 需要删除的fiber的数组
let deletions = null

// 提交根fiber
function commitRoot() {
	// 处理需要删除的fiber
	deletions.forEach(commitWork)
	// 提交当前工作中的根fiber的第一个子fiber
  	commitWork(wipRoot.child)
  	// 将当前工作中的根fiber更新到上一次提交的根fiber
  	currentRoot = wipRoot
  	// 置空当前工作中的根fiber
  	wipRoot = null
}

// 提交fiber
function commitWork(fiber) {
  	if (!fiber) { // fiber tree 遍历完成
    	return
  	}

	let domParentFiber = fiber.parent // 父fiber
 	// 函数组件和类组件自身没有对应的真实DOM
  	while (!domParentFiber.dom) { // 判断是否存在真实DOM
  		// 查找最近的具有真实DOM的父fiber,作为当前fiber的真实DOM的容器
    	domParentFiber = domParentFiber.parent
  	}
  	const domParent = domParentFiber.dom // 父fiber真实DOM
	if (
    	fiber.effectTag === "PLACEMENT" &&
	    fiber.dom !== null
	) {
	    domParent.appendChild(fiber.dom) // 添加当前fiber真实DOM到父级
	} else if (
	    fiber.effectTag === "UPDATE" &&
	    fiber.dom !== null
	  ) {
	    updateDom( // 根据当前fiber更新真实DOM
	      fiber.dom,
	      fiber.alternate.props,
	      fiber.props
	    )
	} else if (fiber.effectTag === "DELETION") {
		 commitDeletion(fiber, domParent) // 移除需要删除的真实DOM
	}

	/*
	通过两次递归调用commitWork,
	先后分别传入子fiber和兄弟fiber,
	实现深度优先遍历,
	即有子fiber则先遍历子fiber,直到fiber tree最深层级子fiber,
	此时没有子fiber,开始遍历兄弟fiber,直到同级fiber遍历完成,
	此时没有兄弟fiber,如此直到整个fiber tree遍历完成。

	React在fiber tree中实际发生改变的fiber上创建effect,
	将effect构建成一个收集了全部fiber变更的effect list链表,
	在commit阶段执行effect list的全部effect,
	实现了只对实际发生改变的fiber的对应DOM进行更新,
	避免了遍历整个fiber tree造成的性能浪费。
	*/
  	commitWork(fiber.child) // 提交第一个子fiber
  	commitWork(fiber.sibling) // 提交下一个兄弟fiber
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) { // 原生组件
    domParent.removeChild(fiber.dom)
  } else { // 函数组件和类组件,没有对应真实DOM,删除其child
    commitDeletion(fiber.child, domParent)
  }
}

// ReactDOM.render方法
function render(element, container) {
	// 创建根fiber,更新到当前工作中的根fiber
  	wipRoot = {
    	dom: container, // 根fiber对应真实DOM即container
    	props: {
      		children: [element],
    	},
    	alternate: currentRoot, // 记录上一次提交的fiber
  	}
  	// 置空需要删除的fiber的数组
  	deletions = []
  	// 将根fiber设置为下一个工作单元
  	nextUnitOfWork = wipRoot 
}

const ReactDOM = { render };

// 运行fiber的工作循环
function workLoop(deadline){
	let shouldYield = false; // 超时标记
	// 有帧空闲时间 且 有工作单元待运行
	while(nextUnitOfWork && !shouldYield){
		nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 运行fiber并返回下一个fiber
		shouldYield = deadline.timeRemaining() < 1; // 更新超时标记
	}
	if(!nextUnitOfWork && wipRoot){ // 下一个工作单元为空 且 当前工作中的根fiber不为空
		commitRoot() // 提交当前工作中的根fiber
	}
	if(nextUnitOfWork){ // 下一个工作单元不为空
		requestIdleCallback(workLoop, {timeout: 167}) // 注册下一次帧空闲时间回调
	}
}

requestIdleCallback(workLoop, {timeout: 167})

// 运行fiber工作单元
function performUnitOfWork(fiber){
	/*
	原生组件的type为一个通过React.createElement创建的虚拟DOM对象,
	类组件的type为组件类自身(组件构造函数),
	函数组件的type为组件函数自身。
	类组件和函数组件无法通过props直接获取属性,
	类组件需要创建组件实例,函数组件需要执行组件方法,
	然后各自从返回的虚拟DOM对象来获取其props。
	*/
	const isFunctionComponent = fiber.type instanceof Function
	const isClassComponent = fiber.type instanceof Class
	if (isFunctionComponent) { // 判断组件类型
	    updateFunctionComponent(fiber) // 函数组件
	} 
	else if(isClassComponent){
		updateClassComponent(fiber) // 类组件
	}
	else {
	    updateHostComponent(fiber) // 原生组件
	}
	
	/*
	注意:事实上,React源码中并没有完整遍历fiber tree,
	而是通过一些提示和试探性判断方法跳过了没有改变的完整子树
	*/
	if (fiber.child) { // 如果有子fiber
	    return fiber.child   // 第一个子fiber即下一个工作单元
	}
	// 如果没有子fiber
	let nextFiber = fiber
	while (nextFiber) {
	    if (nextFiber.sibling) { // 如果有兄弟fiber
	      return nextFiber.sibling // 下一个兄弟fiber即下一个工作单元
	    }
	    // 如果没有兄弟fiber 返回父级fiber查找
	    nextFiber = nextFiber.parent 
	}
}

// 原生组件更新方法
function updateHostComponent(fiber) {
	if (!fiber.dom) {
		// 创建 fiber 对应 真实DOM
    	fiber.dom = createDom(fiber)
  	}
	const elements = fiber.props.children
	reconcileChildren(fiber, elements) // 调和 子 虚拟DOM
}

// 函数组件更新方法
function updateFunctionComponent(fiber) {
  // 执行组件方法返回Virtual DOM,创建数组作为children 
  const children = [fiber.type(fiber.props)]
  // children 执行 reconcile ,进行diffing计算,与原生组件逻辑相同
  reconcileChildren(fiber, children) 
}

// 类组件更新方法
function updateClassComponent(fiber) {
  ...
}

// 调和子 虚拟DOM
function reconcileChildren(wipFiber, elements){
	let index = 0
	// 获取上一次提交的fiber的第一个子fiber
	let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  	let prevSibling = null
	// 遍历 子 虚拟DOM 和 上一次提交的fiber
  	while ( 
  		index < elements.length || 
  		oldFiber !== null
	) {
    	const element = elements[index]
    	let newFiber = null
		// diffing ... 
		const sameType = 
		oldFiber && // 有老fiber
      	element && // 有新元素
      	element.type === oldFiber.type // 相同type
      	// 为 子 虚拟DOM 创建 fiber
      	if (sameType) {
		    newFiber = {
		        type: oldFiber.type,
		        props: element.props, // 仅更新props
		        dom: oldFiber.dom, // 真实DOM保留
		        parent: wipFiber,
		        alternate: oldFiber, 
		        effectTag: "UPDATE", // effect类型 更新
		    }
	    }
	    if (element && !sameType) { // 有新元素 不同type
		    newFiber = {
		        type: element.type,
		        props: element.props,
		        dom: null, // 真实DOM 待创建
		        parent: wipFiber,
		        alternate: null, 
		        effectTag: "PLACEMENT", // effect类型 创建
		    }
	    }
		if (oldFiber && !sameType) { // 有老fiber 不同type
	      	oldFiber.effectTag = "DELETION" // effect类型 删除
	      	/* 
	      	对于需要移除的节点,
	      	不会在当前进行中的根fiber的fiber tree中创建新fiber,
	      	而老fiber则存在于上一次提交的根fiber的fiber tree中,
	      	即currentRoot中有老fiber,
	      	wipRoot中既无新fiber也无老fiber,
	      	故需要维护一个用来记录将要删除的节点的数组
	      	*/
	      	deletions.push(oldFiber)
	    }
		/* 
      	事实上,React在构建和更新fiber tree时,
      	非每次都创建新fiber删除老fiber,
      	而是尽量循环利用原有fiber
	    */

		/*
		注意:React中的diff算法还包含了通过key优化children比对等
		*/

    	// 将 子 虚拟DOM 的 fiber 添加到 fiber tree
    	if (index === 0) {
	      wipFiber.child = newFiber // 第一个子fiber
	    } else {
	      prevSibling.sibling = newFiber // 第一个以后的子fiber
	    }
	    prevSibling = newFiber
	    index++
  	}
}

调用示例:

/** @jsx React.createElement */
const container = document.getElementById("root")

function App (props){
  return (<div>
    <h2>Hello {props.name}</h2>
  </div>)
}

const element = <App name="Ronnie"/>

ReactDOM.render(element, container)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值