react fiber架构学习
Posted 神奇大叔
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react fiber架构学习相关的知识,希望对你有一定的参考价值。
同步更新过程的局限
在v16版本以前,react的更新过程是通过递归从根组件树开始同步进行的,更新过程无法被打断,当组件树很大的时候就会出现卡顿的问题
react中的虚拟dom
import React, Component from 'react';
export default class ClickCounter extends Component
constructor(props)
super(props);
this.state = count: 0;
this.handleClick = this.handleClick.bind(this);
handleClick()
this.setState((state) =>
return count: state.count + 1;
);
render()
return [
<button key="1" onClick=this.handleClick>Update counter</button>,
<span key="2">this.state.count</span>
]
在经过jsx编译器编译后
class ClickCounter
...
render()
return [
React.createElement(
'button',
key: '1',
onClick: this.onClick
,
'Update counter'
),
React.createElement(
'span',
key: '2'
,
this.state.count
)
]
最终转换成的虚拟dom
[
$$typeof: Symbol(react.element),
type: 'button',
key: "1",
props:
children: 'Update counter',
onClick: () => ...
,
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props:
children: 0
]
fiber
- 在reconciliation期间,来自render方法返回的每个React元素的数据被合并到fiber node树中,每个React元素(虚拟dom)都有一个相应的fiber node,这些可变的数据包含组件state和DOM。 根据React元素的类型,框架需要执行不同的活动。在我们的示例应用程序中,对于class组件ClickCounter,它调用生命周期方法和render方法,而对于span Host 组件(DOM节点),它执行DOM更新。因此,每个React元素都会转换为相应类型的fiber节点,用于描述需要完成的工作
- 当React元素第一次转换为fiber节点时,React使用createElement返回的数据来创建fiber
time-slicing时间切片
浏览器一帧执行的任务
- 处理用户的交互
- JS 解析执行
- 帧开始。窗口尺寸变更,页面滚去等的处理
- rAF(requestAnimationFrame)
- 布局
- 绘制
- 剩余时间执行requestIdleCallback
requestIdleCallback
requestIdleCallback((deadline)=>
deadline.timeRemaining() 一帧中剩余的时间
deadline.didTimeout 是否已超时timeout
timeout 设置当n毫秒后,强制执行任务
,timeout);
和requestAnimation的区别
- requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。
- 在requsetIdleCallback中执行dom操作会使得之前的布局计算以及页面重绘等失效重新计算,推荐在requsetAnimation中进行dom操作,之后就会进行计算
实现空闲时间暂停、恢复任务的基本实现
function myNonEssentialWork (deadline)
// 当回调函数是由于超时才得以执行的话,deadline.didTimeout为true
while ((deadline.timeRemaining() > 0 || deadline.didTimeout)
if(tasks.length > 0)
doWorkIfNeeded();
//如果当前帧没有执行完任务,交给下一帧的空闲时间去执行
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
react并未使用原生requestIdleCallback,而是使用扩展实现的原因
- 回调并不会严格执行,比如在tab页切换后触发频率会降低
- 兼容性不行
react在不支持requestIdleCallback的浏览器中,通过requestAnimation+MessageChannel模拟实现
window.myRequestIdleCallback = function(callback,options)
requestAnimationFrame((DOMHighResTimeStamp)=>
//requestAnimationFrame第一个参数表示执行时的时间戳
//最迟执行时间,当前触发的时间+一帧的时间
myRequestIdleCallback.IdleDeadline = DOMHighResTimeStamp + myRequestIdleCallback.activeAnimationTime
myRequestIdleCallback.peedingCallback = callback;
myRequestIdleCallback.channel.port1.postMessage('start');
)
myRequestIdleCallback.activeAnimationTime = 1000/60; // 每一帧的时间 ms
myRequestIdleCallback.IdleDeadline; // 这一帧的截止时间
myRequestIdleCallback.tiemRemaing = ()=> myRequestIdleCallback.IdleDeadline - performance.now(); // 执行到此语句还有多少空余时间剩余
myRequestIdleCallback.channel = new MessageChannel();
myRequestIdleCallback.channel.port2.onmessage = function(event)
// 当收到消息的时候表示,浏览器已经空闲,处理该任务
const currentTime = performance.now();//运行到当前回调函数的时刻
// 如果deadline为true,意味着当前时间已经超出了每一帧的截止时间,也就等同于本帧没有任何时间可以处理回调函数,此帧过期
const isDeadLine = currentTime > myRequestIdleCallback.IdleDeadline
if( !isDeadLine || myRequestIdleCallback.tiemRemaing()>0)
if(myRequestIdleCallback.peedingCallback)
//执行回调并传入一帧的剩余时间
myRequestIdleCallback.peedingCallback(
timeRemaining:myRequestIdleCallback.tiemRemaing
);
引入Fiber架构
React Fiber把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行。
通过Fiber的架构,提供了一种跟踪,调度,暂停和恢复的工作
React Fiber一个更新过程被分为两个阶段(Phase)
第一个阶段Reconciliation(协调) Phase和第二阶段Commit Phase,在第一阶段Reconciliation Phase,包含React元素的作用,生命周期方法和渲染方法,以及应用于组件子元素的diff算法等相关内容。这个阶段是可以被打断的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断
对生命周期函数的影响
下面这些生命周期函数则会在第一阶段调用:
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
getDerivedStateFromProps
因为第一阶段的过程会被打断而且“重头再来,比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中断的部分开始,也就是说,componentWillUpdate函数会被再调用一次
下面这些生命周期函数则会在第二阶段调用:
getSnapshotBeforeUpdate
componentDidMount
componentDidUpdate
componentWillUnmount
Reconciliation(协调调和阶段)
在render阶段时,React通过setState或React.render来执行组件的更新,并确定需要在UI中更新的内容。如果是第一次渲染,React会为render方法返回的每个元素,创建一个新的fiber节点。在接下来的更新中,将重用和更新现有React元素的fiber 节点。render阶段的结果是生成一个部分节点标记了side effects的fiber节点树
render阶段可以异步执行。 React可以根据可用时间来处理一个或多个fiber节点,然后停止已完成的工作,并让出调度权来处理某些事件。然后它从它停止的地方继续。但有时候,它可能需要丢弃完成的工作并再次从头。由于在render阶段执行的工作不会导致任何用户可见的更改(如DOM更新),因此这些暂停是不会有问题的。
commit阶段
side effects描述了在下一个commit阶段需要完成的工作。在此阶段,React采用标有side effects的fiber树并将其应用于实例。它遍历side effects列表并执行DOM更新和用户可见的其他更改。在此阶段执行的工作,将会生成用户可见的变化,这就是React需要一次完成它们的原因。
fiber的数据结构
每一个fiber都有最重要的三个属性,return指向父fiber,sibling指向兄弟fiber,child指向第一个子fiber,为了能实现中断后恢复,即记住中断时的状态,将其设计成链表的结构,即能实现每一个fiber单元都能找到其他fiber
fiber的遍历顺序
整个fiber tree采用DFS深度遍历的方式,优先访问第一个child子节点,当child为null时,访问该节点的sibling,然后访问sibling的child子节点,当sibling为null时,通过return返回到父节点继续采用这种方式进行遍历
class App extends React.Component
render()
return (
<div className="app">
<header>header</header>
<Content />
<footer>footer</footer>
</div>
);
class Content extends React.Component
render()
return (
<React.Fragment>
<p>1</p>
<p>2</p>
<p>3</p>
</React.Fragment>
);
export default App;
上述代码首次创建fiber树的遍历顺序为:
首次构建出的fiber树:
遍历所有的 fiber 节点,通过 Diff 算法计算所有更新工作,产出 EffectList 给到 commit 阶段使用
Current tree和WorkInProgress tree
在第一次渲染之后,React最终得到一个fiber tree,它反映了用于渲染UI的应用程序的状态。这棵树通常被称为current tree。当React开始处理更新时,它会构建一个所谓的workInProgress tree,它反映了要刷新到屏幕的未来状态。
每次重新渲染都会重新创建 Element(虚拟dom树), 但是 Fiber 不会,Fiber 只会使用对应的 Element 中的数据来更新自己必要的属性
每个Fiber上都有个alternate属性,也指向一个 Fiber,创建 WorkInProgress 节点时优先取alternate,如果没有的话就新建一个
所有work都在workInProgress tree中的fiber上执行。当React遍历current tree时,对于每个现有fiber节点,它会使用render方法返回的React元素中的数据创建一个备用(alternate)fiber节点,这些节点用于构成workInProgress tree(备用tree)。处理完更新并完成所有相关工作后,React将备用tree刷新到屏幕。一旦这个workInProgress tree在屏幕上呈现,它就会变成current tree。
每个fiber节点都会通过alternate字段保持对另一个树的对应节点的引用。current tree中的节点指向workInProgress tree中的备用节点,反之亦然。
React代码在第一次执行时,因为页面还没有渲染出来,此时是没有current树的,只有一个正在构建DOM的workInProgress树。
当下次更新时,会将Current tree的hostRootFiber拷贝过去
构建workInProgress fiber
//ReactFiber.old.js
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber
let workInProgress = current.alternate;
if (workInProgress === null) //区分是在mount时还是在update时
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
else
workInProgress.pendingProps = pendingProps;//复用属性
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
//...
workInProgress.childLanes = current.childLanes;//复用属性
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
:
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
;
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
在首次mount时会创建fiberRoot和rootFiber
然后根据element对象创建workInProgress Fiber
创建完成后把workInProgress Fiber切换成current Fiber
在update时:会根据新的状态形成的element 和current Fiber对比形(diff算法)成一颗叫workInProgress的Fiber树
然后将fiberRoot的current指向workInProgress树,此时workInProgress就变成了current Fiber。
双缓冲技术
render 的时候有了这么一条单链表,当调用 setState 的时候Diff 得到 change 的方式,采用的是一种叫双缓冲技术(double buffering),这个时候就需要另外一颗树:WorkInProgress Tree,它反映了要刷新到屏幕的未来状态。
current树对应了屏幕上已经渲染好的内容,workInprogress树是根据current树深度优先遍历出来新的fiber树,所有需要更新的内容都会在workInprogress树上体现。当更新未完成时,屏幕上始终都只会显示current树对应的内容,当更新完成则会将current树切换为workInprogress树,此时workInprogress树则变成新的current树。
好处:
- 能够复用内部对象(fiber)
- 节省内存分配、GC的时间开销
- 就算运行中有错误,也不会影响 View 上的数据
Side-effects(effects)
我们可以将React中的一个组件视为一个使用state和props来计算UI的函数。每个其他活动,如改变DOM或调用生命周期方法,都应该被认为是side-effects,应用effects是一种work,fiber节点是一种方便的机制,可以跟踪除更新之外的effects。每个fiber节点都可以具有与之相关的effects, 通过fiber节点中的effectTag字段表示。
因此,Fiber中的effects基本上定义了处理更新后需要为实例完成的工作,对于Host组件(DOM元素),工作包括添加,更新或删除元素。对于class组件,React可能需要更新ref并调用componentDidMount和componentDidUpdate生命周期方法,还存在与其他类型的fiber相对应的其他effects。
Effects list
构建的具有side-effects的fiber节点的链表,没有side-effects的fiber节点并不会在链表中
此链表的目标是标记具有DOM更新或与其关联的其他effects的节点,此列表是finishedWork tree的子集,并使用nextEffect属性指向下一个节点,而不是current和workInProgress树中使用的child属性进行链接。
Dan Abramove为effecs list提供了一个类比: 他喜欢将它想象成一棵圣诞树,“圣诞灯”将所有带有effects的节点绑定在一起。
例如:橙色的节点都有一些effects需要处理,effects list将它们链接在一起,以便React可以在以后跳过其他节点
当遍历节点时,React使用firstEffect指针来确定effects list的开始位置。所以上图可以表示为这样的线性列表
fiberRoot和rootFiber
- fiberRoot:整个React应用的根节点,即会基于ReactDom.render的第二个参数DOM创建fiberRoot
- rootFiber: 某个组件树的根节点;(因为我们可能多次使用React.render()函数,这样就会有多个rootFiber)
const fiberRoot = query('#container')._reactRootContainer._internalRoot
const hostRootFiberNode = fiberRoot.current
可以从组件实例中获取单个fiber节点,如下所示:
compInstance._reactInternalFiber
fiber的结构
上述ClickCounter组件
stateNode: new ClickCounter,
type: ClickCounter,
alternate: null,
key: null,
updateQueue: null,
memoizedState: count: 0,
pendingProps: ,
memoizedProps: ,
tag: 1,
effectTag: 0,
nextEffect: null
ClickCounter内的span元素
stateNode: new htmlSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: children: 0,
memoizedProps: children: 0,
tag: 5,
effectTag: 0,
nextEffect: null
fiber节点大致结构
fiber.tag: 表示 fiber 类型, 根据ReactElement组件的 type 进行生成, 在 react 内部共定义了25 种 tag.
fiber.key: 和ReactElement组件的 key 一致.
fiber.elementType: 一般来讲和ReactElement组件的 type 一致
fiber.type: 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新(HotReloading), 会对function, class, ForwardRef类型的ReactElement做一定的处理, 这种情况会区别于fiber.elementType, 具体赋值关系可以查看源文件.
fiber.stateNode: 与fiber关联的局部状态节点(比如: HostComponent类型指向与fiber节点对应的 dom 节点; 根节点fiber.stateNode指向的是FiberRoot; class 类型节点其stateNode指向的是 class 实例).
fiber.return: 指向父节点.
fiber.child: 指向第一个子节点.
fiber.sibling: 指向下一个兄弟节点.
fiber.index: fiber 在兄弟节点中的索引, 如果是单节点默认为 0.
fiber.ref: 指向在ReactElement组件上设置的 ref(string类型的ref除外, 这种类型的ref已经不推荐使用, reconciler阶段会将string类型的ref转换成一个function类型).
fiber.pendingProps: 输入属性, 从ReactElement对象传入的 props. 用于和fiber.memoizedProps比较可以得出属性是否变动.
fiber.memoizedProps: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps, 生成子节点之后会把pendingProps赋值给memoizedProps用于下一次比较.pendingProps和memoizedProps比较可以得出属性是否变动.
fiber.updateQueue: 存储update更新对象的队列, 每一次发起更新, 都需要在该队列上创建一个update对象.
fiber.memoizedState: 上一次生成子节点之后保持在内存中的局部状态.
fiber.dependencies: 该 fiber 节点所依赖的(contexts, events)等, 在context机制章节详细说明.
fiber.mode: 二进制位 Bitfield,继承至父节点,影响本 fiber 节点及其子树中所有节点. 与 react 应用的运行模式有关(有 ConcurrentMode, BlockingMode, NoMode 等选项).
fiber.flags: 标志位, 副作用标记(在 16.x 版本中叫做effectTag, 相应pr), 在ReactFiberFlags.js中定义了所有的标志位. reconciler阶段会将所有拥有flags标记的节点添加到副作用链表中, 等待 commit 阶段的处理.
fiber.subtreeFlags: 替代 16.x 版本中的 firstEffect, nextEffect. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
fiber.deletions: 存储将要被删除的子节点. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
fiber.nextEffect: 单向链表, 指向下一个有副作用的 fiber 节点.
fiber.firstEffect: 指向副作用链表中的第一个 fiber 节点.
fiber.lastEffect: 指向副作用链表中的最后一个 fiber 节点.
fiber.lanes: 本 fiber 节点所属的优先级, 创建 fiber 的时候设置.
fiber.childLanes: 子节点所属的优先级.
fiber.alternate: 指向内存中的另一个 fiber, 每个被更新过 fiber 节点在内存中都是成对出现(current 和 workInProgress)
// 性能统计相关(开启enableProfilerTimer后才会统计)
// react-dev-tool会根据这些时间统计来评估性能
fiber.actualDuration?: number, // 本次更新过程, 本节点以及子树所消耗的总时间
fiber.actualStartTime?: number, // 标记本fiber节点开始构建的时间
fiber.selfBaseDuration?: number, // 用于最近一次生成本fiber节点所消耗的实现
fiber.treeBaseDuration?: number, // 生成子树所消耗的时间的总和
v16之前的递归遍历
a1.render = () => [b1, b2, b3];
b1.render = () => [];
b2.render = () => [c1];
b3.render = () => [c2];
c1.render = () => [d1, d2];
c2.render = () => [];
d1.render = () => [];
d2.render = () => [];
walk(a1);
function walk(instance)
doWork(instance);
const children = instance.render();
children.forEach(walk);
function doWorkReact 之 简易实现 Fiber架构
文章目录
此篇文章是在学习一步一步实现
fiber
架构的同时,从另外一个由总到分的角度来总结
fiber
架构的实现思路。文章末尾有一些学习参考文章可以借鉴。
fiber架构是什么?它解决了什么问题?
当我们项目过于复杂,渲染树过于庞大的时候,那么我们的递归渲染会耗时很长,而且很难被中断,fiber 的主要原理就是让我们在 diff 的过程中可以被中断,去处理更高优先级的事件如:用户事件或者动画,这样让浏览器的渲染更加流畅。
fiber 的核心思想,实现 fiber 我们需要做到什么?如何做?
React 16.0
之前,我们在渲染的过程中,通过去遍历一整棵 虚拟 dom
树来更新变化,我们很难中断,并且无法标记中断来持续工作。那么 fiber 架构将递归 diff 拆分成一个一个小任务,并且随时可中断,利用浏览器的空闲时间来执行,当处理完更高优先级任务后回到中断点继续执行;
要实现这样的机制,fiber做了什么,这里我总结了几点:
- 将原有的 vdom 树结构变成一个新的链表结构的树,每个节点都标记了它的(
child,sibling,return,
分别代表节点的第一个字节点、兄弟节点、父节点),这样可以随时中断,下次从中断处继续执行; - fiber的核心思想是将一个庞大的任务拆分成一个个小的任务块,利用浏览器的空闲时间来执行,那么如何拆分,如何协调?
React
以虚拟dom
节点为维度对任务进行拆分,即一个虚拟dom节点对应一个任务,采用深度优先遍历的规则进行协调; - 从根节点开始调度和渲染的过程可以分为两个阶段:
render()
,和 commit()
; render 阶段会根据v-dom找出所有节点的变更(增删更新),然后构建出一棵Fiber 树,这个阶段是可以中断的。commit阶段就是将构建的Fiber three渲染成真实的dom, 这个阶段是不可中断的。 - 这里我们通过window.requestIdleCallback 来实现浏览器空闲时执行低优先级任务。
所以说 React Fiber
其实就是通过遍历将 VDom 转换成了 Fiber three,其中每个 Fiber都具有 child、singling、return属性;
遍历遵循深度优先遍历,自上而下,自左向右;从根节点出发,找到他的第一个子元素,找到则返回,没有则找他的兄弟元素,如果无兄弟元素,则直接返回其父元素, 父 ——> 第一个子 ——> 兄弟 ——> 父亲;
在遍历生成Fiber three 的时候根据节点的变更收集 effect list
, 通过tag(UPDATE、DELETE、PLACEMENT)
,直到没有下一个任务,commit 到DOM树上。
在此之前,我们的 vdom
是一颗树,它在 diff 的过程中是没法中断的,于是将其改造成一个链表结构,之前是只有 children 进行递归遍历,现在是包含了父——>子, 子——> 父, 子——> 兄弟这几层关系的链表。
Fiber reconcile
至此,我们可以跟着思路来实现 fiber, 说到 fiber, 它其实就是一个具有各种标识的对象,如:
dom: null, // 真实dom,这里function 组件的dom是null
type,
props,
child,
return,
sibling,
alternate: null, // 旧值,用于比对更新
effectTag: 'PLACEMENT'
正题来了,首先我们还是来实现createElement(type, config, ...children)
最终返回 虚拟dom 树,这里不是重点,所以不过多介绍,详细可以查看createElement原理,直接贴代码:
/**
* jsx语法糖,接受三个参数,返回v-dom(js对象)
* @param * type 元素类型:native HTML | Function | Class
* @param * config 属性
* @param ...any children 子元素
* @returns v-dom 对象
*/
function createElement(type, config, ...children)
delete config.__self
delete config.__source
const key, ref, ...rest = config
const vdom =
$$typeof: Symbol('react.element'),
type,
props:
...rest,
children: children.map(c => typeof c === 'object' ? c : createTextNode(c))
return vdom
/**
* 创建文本节点对象
* @param * nodeValue 文本值
* @returns v-dom 对象
*/
function createTextNode(nodeValue)
return
type: 'TEXT',
props:
nodeValue,
children: []
我们的核心是 任务拆解和任务协调,任务协调(reconcile
)我们利用浏览器 API requestIdleCallback 来实现,React 实现了自己的任务调度函数,它接受一个callback(idleDeadline => ) ,
利用 idleDeadline
我们能判断浏览器是否处于空闲时间来调度我们的任务。
/**
* 任务循环
* @param * idleDeadline 参数参考:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline
*/
function workLoop(idleDeadline)
// 当前还有空闲时间,可以设置超时 && 有任务可执行
while (idleDeadline.timeRemaining() > 1 && nextUnitOfWork)
// 执行当前任务单元,并返回下一个待执行任务单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// render循环结束的条件:无可执行任务,当前 effect list 已经生成, 开始commit了
if (!nextUnitOfWork && wipRoot)
commitRoot(wipRoot)
// 否则(当前忙碌,但是还有任务待执行), 继续监听等待执行
requestIdleCallback(workLoop);
// 立即执行
requestIdleCallback(workLoop);
通过 idleDeadline.timeRemaining()
判断当前是否出于空闲时间,并且有下一个任务nextUnitOfWork
可以执行, 循环执行当前任务,并返回下一个待执行任务;直到所有任务都执行完毕,我们将生成的 effect list
渲染成真正的 dom 树。当然,在有更高优先级事件正在处理的时,而我们还有 nextUnitOfWork
未完成,那么继续调用 requestIdleCallback
。
由此可见,真正可中断的是 协调 (reconcile)阶段,在渲染阶段(commit)是不可中断的;
那么接下来我们要搞清楚的是如何对任务进行拆分?看下performUnitOfWork(nextUnitOfWork)
做了啥?
/**
* performUnitOfWork 实质是通过遍历当前 fiber 节点的 children 来构建一颗小的Fiber three,最后根据遍历规则,如果有child,将其当成下一个任务返回
* 否则,向上回溯到有sibling的父节点(return),作为 nextUnitOfWork。此时这里应该有一张图 TODO:
* 这里的逻辑需要考虑首次渲染和更新操作,对Fiber进行effect标记。
* @param * fiber
* @returns 下一个任务
*/
function performUnitOfWork(fiber)
// 这里需要区分 Native HTMl 和 函数组件, 类组件的实现后续会迭代更新TODO:,这里先用函数classTransferToFun进行转换
if (fiber.type instanceof Function)
// 构建fiber three
updateFunctionFiberThree(fiber)
else
updateNativeFiberThree(fiber)
// 至此,我们拿到了一棵小的Fiber three
if (fiber.child)
return fiber.child
let parentFiber = fiber;
while (parentFiber)
if (parentFiber.sibling)
return parentFiber.sibling
parentFiber = parentFiber.return // 属性 return 其实就是父节点
return null
performUnitOfWork
的任务是接受一个fiber 节点,遍历其 children,深度遍历构建一棵小的fiber 链表, 将树上的每个 fiber 进行标识(child、sibling、return),最后依据“如果有第一个字节点,则返回子节点,否则返回其兄弟节点”的规则,来返回下一个任务的指定。
/**
* 函数组件调度 children
* @param * fiber
*/
function updateFunctionFiberThree(fiber)
// 状态重置
// .... 后面补充
// 函数组件的type对应的就是函数fn,直接调用返回vdom
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
/**
* HTML 元素 调度 children
* @param * fiber
*/
function updateNativeFiberThree(fiber)
if (!fiber.dom)
fiber.dom = createDom(fiber);
reconcileChildren(fiber, fiber.props.children)
我们补齐了不同 type 的函数调度,其核心就是拿到其 children
进行遍历,构建fiber 节点,重点看 reconcileChildren
函数:
let deletions = []; // 存储需要标记删除的 fiber
/**
* 通过 fiber 的 children,构建Fiber three
* @param * fiber
* @param * children
*/
function reconcileChildren(fiber, children)
// 如果有旧的Fiber,找到其子节点
let oldFiber = fiber.alternate && fiber.alternate.child;
let index = 0
// 记录上一个 fiber, 用来构建 兄弟关系
let prevFiber = null
// 如果有子元素 或者 旧 Fiber 子元素存在
while (index < children.length || oldFiber !== null)
let newFiber = null
const node = children[index]
// 对比新旧节点类型
const sameType = oldFiber && node && oldFiber.type === node.type
if (sameType)
newFiber =
dom: oldFiber.dom,
type: node.type,
props: node.props,
return: fiber,
alternate: oldFiber,
effectTag: 'UPDATE' // 标记状态
// 类型不同,新节点直接替换
if (!sameType && node)
newFiber =
dom: null,
type: node.type,
props: node.props,
return: fiber,
alternate: null,
effectTag: 'PLACEMENT'
// 类型不同,旧节点存在, 将旧节点标记为删除
if (!sameType && oldFiber)
oldFiber.effectTag = 'DELETE'
deletions.push(oldFiber)
if (index === 0)
fiber.child = newFiber
else
// 将上一个节点的sibling指向当前节点
prevFiber.sibling = newFiber;
prevFiber = newFiber;
// 移动指针,将oldFiber指向他的兄弟节点
if (oldFiber)
oldFiber = oldFiber.sibling;
index++;
这里考虑了节点的替换、删除和更新,通过字段effectTag
进行标识,在更新的同时通过字段alternate
记录上一次的fiber 节点,声明全局变量 deletions
用于存储删除的 fiber 节点,这些个字段均是用于再 commit 阶段用于判断 dom 如何挂载;
OK,至此,我们的整个reconcile
流程还差一步,设定初始 unitWork, 我们定义了一些全局变量,wipRoot
用于装载 整个 fiber 链表树 ,currentWipRoot
用于记录上一次更新的 fiber 链表树,用于更新比对,nextUnitOfWork
为下一个待执行任务。
定义render(vnode, container)
函数,将容器元素作为第一个执行单元,重置 deletions
// 定义一些全局变量
let wipRoot = null; // 当前执行 Fiber three 的根节点,首次执行时为 document.getElementById('root') 对应的 Fiber
let currentWipRoot = null; // 记录上一次生成的 Fiber three,也可以说是触法更新之前的 Fiber three, 方便此次更新比对
let nextUnitOfWork = null; // 下一个待执行的任务块
/**
* render 函数
* @param * vdom
* @param * container 容器
*/
function render(vnode, container)
wipRoot =
dom: container,
props:
children: [vnode]
,
alternate: currentWipRoot // 用于记录上一次的状态,渲染时做比对
deletions = []
nextUnitOfWork = wipRoot
这里会留有一些疑问,currentWipRoot
什么时候赋值的?
循环执行直到最后一个nextUnitOfWork
执行完毕,此时 wipRoot
已经构建完毕,接下来进入 commit 阶段,正式构建真实 dom 树。
Commit 阶段
commit 阶段主要做的就是 dom 元素的 新增、删除和更新。
这里需要注意的事,在dom操作过程中,由于函数组件dom为 null, 通过return向上查找,直到找到存在dom的父节点为止,通过fiber 对象中的dom 字段可以拿到当前 fiber 的真实 dom。最后我们存储本次构建的 wipRoot, 用于下次更新进行比对。
/**
* 渲染DOM树
* @param * wipRoot 此次构建的 Fiber Three
*/
function commitRoot(fiberThree)
// 主要做了 件事:
// 1. 将需要删除的deletions中的元素删除
// 2. 更新变更,插入替换的新元素
// 3. 将此次构建的 Fiber three 缓存在 currentWipRoot 中,下次触发更新时可做对比, 重置 wipRoot
deletions.forEach(commitWorker)
commitWorker(fiberThree.child)
currentWipRoot = fiberThree
wipRoot = null
/**
* fiber 的变更操作
* @param * fiber
*/
function commitWorker(fiber)
if (!fiber)
return
let parent = fiber.return
while (!parent.dom)
parent = parent.return
const parentDom = parent.dom;
if (fiber.effectTag === 'DELETE' && fiber.dom)
deleteDom(fiber, parentDom)
if (fiber.effectTag === 'PLACEMENT' && fiber.dom)
parentDom.appendChild(fiber.dom)
if (fiber.effectTag === 'UPDATE' && fiber.dom)
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
commitWorker(fiber.child);
commitWorker(fiber.sibling);
补气 dom 的新增、删除和更新函数,这里只是做了一些代表型的处理,想更详细的了解可以看官方源码
/**
* 生成dom
* @param * fiber
*/
function createDom(fiber)
const dom = fiber.type === 'TEXT' ? document.createTextNode('') : document.createElement(fiber.type)
updateDom(dom, , fiber.props);
return dom;
/**
* 更新dom,遍历旧节点属性,将新节点中没有的属性删除; 遍历新节点,将旧节点属性更新,并新增旧节点没有的属性。
* @param * dom 元素对象
* @param * prevProps
* @param * nextProps
*/
function updateDom(dom, prevProps, nextProps)
// 过滤掉 children , 得到新节点中没有的属性
Object.keys(prevProps)
.filter(pName => pName !== 'children')
.filter(pName => !(pName in nextProps))
.forEach(pName =>
// 这里考虑事件处理函数的解绑,暂时只对click函数做处理
if (pName.slice(0, 2) === 'on')
dom.removeEventListener(pName.slice(2).toLocaleLowerCase(), prevProps[pName], false)
else
dom[pName] = ''
)
Object.keys(nextProps)
.filter(pName => pName !== 'children')
.forEach(pName =>
if (pName.slice(0, 2) === 'on')
dom.addEventListener(pName.slice(2).toLocaleLowerCase(), nextProps[pName], false)
else
dom[pName] = nextProps[pName]
)
/**
* 删除子元素,这里需要考虑 Function 组件Fiber无dom
* @param * fiber
* @param * parentDom
*/
function deleteDom(fiber, parentDom)
if (fiber.dom)
parentDom.removeChild(fiber.dom)
else
deleteDom(fiber.child, parentDom)
好了,到目前为止,我们来写一个简单的示例,跑起来试试
import React from './fiber'
const ReactDOM = React
function Counter(props)
return (
<div >
<p>简易fiber架构实现</p>
<span>props.count</span>
</div>
)
ReactDOM.render(<Counter count=5 />, document.getElementById('root'))
完美,页面正常显示
太简单了吧,但是我们还缺少对状态的更新处理,接下来我们就来实现hook 函数 useState, 来管理状态更新;
实现useState管理状态更新
首先我们声明两个全局变量
wipFiber
变量与函数组件一对一,将当前执行组件的 fiber 赋值给此变量,同时扩展 hooks 字段来对函数组件中多个状态进行存储,用 hookIndex
来标识顺序,保证每次访问到正确的 state。
let wipFiber = null; // 与函数组件一一对应,存储组件fiber 和 hooks
let hookIndex = null; // 记录当前执行hook的指针,如组件中有多个useState hook
/**
* 函数组件调度 children
* @param * fiber
*/
function updateFunctionFiberThree(fiber)
// 状态重置
wipFiber = fiber;
wipFiber.hooks = [];
hookIndex = 0;
// TODO: 这里我觉得可以直接 用fiber.props.children, 应该是旧的?
const children = [fiber.type(fiber.props)] // 函数组件的type对应的就是函数fn,直接调用返回vdom
reconcileChildren(fiber, children)
我们在 updateFunctionFiberThree
中追加对wipFiber 和 hookIndex
状态的重置,以此来隔离组件间的状态。当前时刻只有一个组件正在执行。
现在我们来实现 useState, 我们看看它在函数中如何使用
const [count, setCount] = useState(props.count)
可以看出,useState 接受一个初始值,在第一次执行时赋初始值,下次直接返回当前state的值;返回一个数组,分别是状态值和改变状态的函数
/**
* 状态管理
* 函数组件中可以通过声明多个 useState 来管理多个状态,我们通过顺序来管理多个状态
* @param * defaultValue 初始值
* @returns
*/
function useState(defaultValue)
// 是否存在旧 hook
const oldHook = wipFiber?.alternate?.hooks && wipFiber.alternate.hooks[hookIndex];
const hook =
// 读取状态的值,如果有,直接返回,如果是第一次,则初始化为 defaultValue
state: oldHook?.state || defaultValue,
queue: [] // 更新栈
const actions = oldHook?.queue || [];
actions.forEach(action =>
if (typeof action === 'function')
hook.state = action(hook.state)
else
hook.state = actions
)
const setState = (action) =>
// 订阅更新操作
hook.queue.push(action);
// 设置 nextUnitWork 触发重新渲染
wipRoot =
dom: currentWipRoot.dom,
props: currentWipRoot.props,
alternate: currentWipRoot
nextUnitOfWork = wipRoot
deletions = []
wipFiber.hooks.push(hook)
console.log(wipFiber)
hookIndex++
return [hook.state, setState]
我们通过 fiber.alternate 可以拿到上次render 时的 olderFiber, 同时即可拿到 hooks, 在setState 函数中只对action进行了push 操作,并设置 nextUnitWork 触发页面更新;
页面重新render, 函数再次执行,我们通过遍历执行oldHook?.queue
来更新state 并返回。
ok,我们对示例追加状态变更
import React from './fiber'
const ReactDOM = React
function Counter(props)
const [count, setCount] = React.useState(props.count)
const handleClick = () => setCount(c => c + 1)
return (
<div >
<p>简易fiber架构实现</p>
<span>count</span>
<br />
<button onClick=handleClick>➕1</button>
</div>
)
ReactDOM.render(<Counter count=5 />, document.getElementById('root'))
可以看到能正常更新;
完整代码后续会整理到github。
参考文章
以上是关于react fiber架构学习的主要内容,如果未能解决你的问题,请参考以下文章
Deep In React之浅谈 React Fiber 架构