一 Fiber之前的React
下面代码实现了一个简单的react手写
let element = (
<div id = "A1">
<div id = "B1">
<div id="C1"></div>
<div id="C2"></div>
</div>
<div id="B2"></div>
</div>
)
console.log(JSON.stringify(element, null, 2))
// 如果节点多,层级特别深
// 因为js是单线程,而且ui渲染和js执行是互斥的
function render(element, parentDom) {
// 创建DOM元素
let dom = document.createElement(element.type);
// 处理属性
Object.keys(element.props)
.filter(key => key !== 'children')
.forEach(key => {
dom[key] = element.props[key];
});
if(Array.isArray(element.props.children)) {
// 把每个子虚拟DOM变成真实DOM插入到DOM节点里
element.props.children.forEach(child => render(child, dom));
}
parentDom.appendChild(dom);
}
render(
element,
document.getElementById('root')
);
打印出的element如下:
{
"type": "div",
"key": null,
"ref": null,
"props": {
"id": "A1",
"children": [
{
"type": "div",
"key": null,
"ref": null,
"props": {
"id": "B1",
"children": [
{
"type": "div",
"key": null,
"ref": null,
"props": {
"id": "C1"
},
"_owner": null,
"_store": {}
},
{
"type": "div",
"key": null,
"ref": null,
"props": {
"id": "C2"
},
"_owner": null,
"_store": {}
}
]
},
"_owner": null,
"_store": {}
},
{
"type": "div",
"key": null,
"ref": null,
"props": {
"id": "B2"
},
"_owner": null,
"_store": {}
}
]
},
"_owner": null,
"_store": {}
}
效果如下:
jsx标签化是嵌套的结构,如代码所示,最终会编译成递归执行的代码,要想中断递归是很困难的。即react16之前的调度器为栈调度器,栈浅显易懂,代码量少,但不能随意break掉,continue掉,要维护一系列的栈上下文;
二 帧
- 目前大多数设备的屏幕刷新频率为60次/秒
- 当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感觉到卡顿;
- 每个帧的预算时间是16.66毫秒(1秒/60)
- 每个帧的开头包括样式计算,布局和绘制
- JavaScript执行JavaScript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和JavaScript执行两者是互斥的
- 如果某个任务执行时间过长,浏览器就会推迟渲染;
三 什么是Fiber
我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
通过Fiber架构,让自己的协调过程变成可被中断,适时地让出CPU执行权,可以让浏览器即使地响应用户的交互;
fiber:就是一个数据结构,它有很多属性,虚拟dom是对真实dom的一种简化;一些真实的dom都做不到的事情,那虚拟dom更做不到,于是就有了fiber,有很多属性,希望借由fiber上的这堆属性来做一些比较厉害的事情;
fiber架构
为了弥补一些不足,就设计了一些新的算法,而为了能让这些算法跑起来,所以出现了fiber这种数据结构; fiber数据结构+算法 = fiber架构;
react应用从始至终管理着基本的三样东西:
-
Root(整个应用的根儿,一个对象,不是fiber,有个属性指向current树,同时也有个属性指向workInProgress树)
-
current树(树上的每一个节点都是fiber,保存的是上一次的状态 并且每个fiber节点,都对应着一个jsx节点)
-
workInProgress树(树上的每一个节点都是fiber,保存的是本次新的状态,并且每个fiber节点都对应一个jsx节点)
初次渲染的时候,没有current树 react在一开始创建Root,就会同时创建一个unintialFiber的东西(未初始化的fiber) 让react的current指向了uninitialFiber 之后再去创建一个本次要用到的workInProgress
react 中主要分两个阶段
render阶段(指的是创建fiber的过程)
-
为每个节点创建新的fiber(workInProgress)(可能是复用) 生成一颗有新状态的workInProgress树
-
初次渲染的时候(或新创建了某个节点的时候) 会将这个fiber创建真实的dom实例 并且对当前节点的子节点进行插入appendChildren,
-
如果不是初次渲染的话,就对比新旧的fiber的状态,将产生了更新的fiber节点,最终通过链表的形式,挂载到RootFiber
commit阶段****才是真正的要操作页面的阶段
-
执行生命周期
-
会从RootFiber上获取到那条链表,根据链表上的标识来操作页面;
3.1 Fiber是一个执行单元
Fiber是一个执行单元,每次执行完一个执行单元,React就会检查还剩下多少时间,如果没有时间就把控制权让出去
3.2 Fiber是一种数据结构
react目前的做法是使用链表,每个虚拟节点内部表示为一个Fiber;
代码如下所示:
class FiberNode {
constructor(tag, key, pendingProps) {
this.tag = tag; // 表示当前fiber的类型
this.key = key;
this.type = null // 'div' || 'h1' || Ding
this.stateNode = null; // 表示当前fiber的实例
this.child = null; // 表示当前fiber的子节点 每一个fiber有且只有一个指向它的firstChild
this.sibling = null; // 表示当前节点的兄弟节点 每个fiber有且只有一个属性指向隔壁的兄弟节点
this.return = null; // 表示当前fiber的父节点
this.index = 0;
this.memoizedState = null; // 表示当前fiber的state
this.memoizedProps = null; // 表示当前fiber的props
this.pendingProps = pendingProps; // 表示新进来的props
this.effecTag = null; // 表示当前节点要进行何种更新
this.firstEffect = null; // 表示当前节点的有更新的第一个子节点
this.lastEffect = null; // 表示当前节点的有更新的最后一个子节点
this.nextEffect = null; // 表示下一个要更新的子节点
this.alternate = null; // 用来连接current和workInProgress的
this.updateQueue = null; // 一条链表上面挂载的是当前fiber的新状态
// 其实还有很多其他的属性
// expirationTime: 0
}
}
四 rAF
requestAnimationFrame回调函数会在绘制之前执行
-
requestAnimationFrame(callback)会在浏览器每次重绘前执行callback回调,每次callback执行的时机都是浏览器刷新下一帧渲染周期的起点上
-
requestAnimationFrame(callback)的回调callback回调参数timestamp是回调被调用的时间,也就是当前帧的起始时间
-
rAfTime performance.timing.navigationStart + performance.now()约等于Date.now();
下面代码实现了一个绘制进度条的功能;
<script>
let div = document.querySelector('div');
let button = document.querySelector('button');
let startTime;
function progress() {
div.style.width = div.offsetWidth + 1 +'px';
div.innerHTML = div.offsetWidth + '%';
if(div.offsetWidth < 100) {
console.log(Date.now() - startTime + 'ms');
startTime = Date.now();
requestAnimationFrame(progress);
}
}
button.onclick = function() {
div.style.width = 0;
startTime = Date.now();
// 浏览器会在每一帧渲染前执行progress
requestAnimationFrame(progress);
}
</script>
五 requestIdleCallbac
-
我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互
-
requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
-
正常帧任务完成后没超过16ms,说明时间有富余,此时就会执行requestIdleCallback里注册的任务
-
requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务;
<script type="text/javascript">
function sleep(duration) {
let start = Date.now();
while(start + duration > Date.now()) {}
}
function progress() {
console.log('progress');
requestAnimationFrame(progress);
}
// requestAnimationFrame(progress);
let channel = new MessageChannel();
let activeFrameTime = 1000/60; // 16.6
let frameDeadline; // 这一帧的截止时间
let pendingCallback;
let timeRemaining = () => frameDeadline - performance.now();
channel.port2.onmessage = function() {
let currentTime = performance.now();
// 如果帧的截止时间已经小于当前时间,说明已经过期了
let didTimeout = frameDeadline <= currentTime;
if(didTimeout || timeRemaining() > 0) {
if(pendingCallback) {
pendingCallback({didTimeout, timeRemaining});
}
}
}
window.requestIdleCallback = (callback, options) => {
requestAnimationFrame((rafTime) => {
console.log('rafTime', rafTime);
// 每一帧开始的时间加上16.6 就是一帧的截止时间了
frameDeadline = rafTime + activeFrameTime;
pendingCallback = callback;
// 其实发消息只会,相当于添加一个宏任务
channel.port1.postMessage('hello');
});
}
const works = [
() => {
console.log('A1开始');
sleep(20);
console.log('A1结束');
},
() => {
console.log('B1开始');
sleep(20);
console.log('B1结束');
},
() => {
console.log('C1开始');
sleep(20);
console.log('C1结束');
},
() => {
console.log('C2开始');
sleep(20);
console.log('C2结束');
},
() => {
console.log('B2开始');
sleep(20);
console.log('B2结束');
},
]
// 告诉浏览器 你可以在空闲的时间执行任务,但是如果已经过期了 不管你有没有空 都要帮我执行
requestIdleCallback(workLoop, {timeout: 1000});
// 循环执行工具
function workLoop(deadline) {
console.log('本帧的剩余时间', parseInt(deadline.timeRemaining()));
// 如果说还有剩余时间 并且还有没有完成的任务
while((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0){
performUnitWork();
}
if(works.length > 0) {
console.log(`只剩下${deadline.timeRemaining()}, 时间片已经到期,等待下次调试`);
requestIdleCallback(workLoop);
}
}
function performUnitWork() {
let work = works.shift();
work();
}
六 MessageChannel
-
目前requestIdleCallback只要Chrome支持
-
所以目前React利用MessageChannel模拟了requestIdleCallback,将回调延迟到绘制操作只后执行
-
MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据;
-
MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了,就能收到另一个端口传过来的数据
-
MessageChannel是一个宏任务;
七 Fiber执行阶段
每次渲染有两个阶段:Reconciliation(协调render阶段)和Commit(提交阶段)
- 协调阶段:可以认为是diff阶段,这个阶段可以被中断,这个阶段会找出所有节点变更,例如节点新增,删除,属性变更等等,这些变更React称之为副作用;
- 提交阶段:将上一个阶段计算出来的需要处理的副作用(Effetcs)一次性执行了。这个阶段必须同步执行,不能被打断;
7.1 render阶段
- 从顶点开始遍历
- 如果有第一个儿子,先遍历第一个儿子
- 如果没有第一个儿子,标志着此节点遍历完成
- 如果有弟弟遍历弟弟
- 如果没有下一个弟弟,返回父节点标志完成父节点遍历,如果有叔叔遍历叔叔
- 没有父节点遍历结束
先儿子,后弟弟,再叔叔,辈分越小越优先
什么时候一个节点遍历完成,没有子节点,或者所有子节点都遍历完成了,没爹了就表示全部遍历完成了;
7.2 commit阶段
下面代码实现了一个简易的Fiber架构,只有初次render过程;
import element from './element.js';
let container = document.getElementById('root');
const PLACEMENT = 'PLACEMENT';
// 下一个工作单元
// fiber其实也是一个普通的JS对象
let workInProgressRoot = {
stateNode: container, // 此fiber对应的dom节点
props: {children: [element]} // fiber的属性
}
let nextUnitOfWork = workInProgressRoot;
function workLoop(deadline) {
// 如果有当前的工作单元,就执行它,并返回一个工作单元
while(nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if(!nextUnitOfWork) {
commitRoot();
}
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while(currentFiber) {
console.log('commitRoot', currentFiber.props.id);
if(currentFiber.effectTag === 'PLACEMENT') {
currentFiber.return.stateNode.appendChild(currentFiber.stateNode);
}
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function performUnitOfWork(workingProgressFiber) {
// 1 创建真实的dom, 并没有挂载 2,创建fiber子树
beginWork(workingProgressFiber);
if(workingProgressFiber.child) {
return workingProgressFiber.child; // 如果有儿子,返回儿子
}
while(workingProgressFiber) {
// 如果没有儿子当前节点其实就结束完成了
completeUnitOfWork(workingProgressFiber);
if(workingProgressFiber.sibling) { // 如果有弟弟,返回弟弟
return workingProgressFiber.sibling;
}
workingProgressFiber = workingProgressFiber.return; // 先指向父亲
}
}
function beginWork(workingProgressFiber) {
console.log('beginWork', workingProgressFiber.props.id);
if(!workingProgressFiber.stateNode) {
workingProgressFiber.stateNode = document.createElement(workingProgressFiber.type);
for (let key in workingProgressFiber.props) {
if(key !== 'children') {
workingProgressFiber.stateNode[key] = workingProgressFiber.props[key];
}
}
}
// 在beginwork里是不会挂载的
let previousFiber;
if(Array.isArray(workingProgressFiber.props.children)) {
workingProgressFiber.props.children.forEach((child, index) => {
let childFiber = {
type: child.type,
props: child.props,
return: workingProgressFiber,
effectTag: 'PLACEMENT', // 这个fiber对应的dom节点需要被插入到页面中
}
if(index === 0) {
workingProgressFiber.child = childFiber;
} else {
previousFiber.sibling = childFiber;
}
previousFiber = childFiber;
});
}
}
function completeUnitOfWork(workingProgressFiber) {
console.log('completeUnitOfWork', workingProgressFiber.props.id);
// 构建副作用链effectList 只有那些有副作用的节点
let returnFiber = workingProgressFiber.return;
if(returnFiber) {
// 把当前fiber的有副作用子链表挂到父亲身上
if(!returnFiber.firstEffect) {
returnFiber.firstEffect = workingProgressFiber.firstEffect;
}
if(workingProgressFiber.lastEffect) {
if(returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workingProgressFiber.firstEffect;
}
returnFiber.lastEffect = workingProgressFiber.lastEffect;
}
// 再把自己挂到后面
if(workingProgressFiber.effectTag) {
if(returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workingProgressFiber;
} else {
returnFiber.firstEffect = workingProgressFiber;
}
returnFiber.lastEffect = workingProgressFiber;
}
}
}
// 告诉浏览器在空闲的时间执行workLoop
requestIdleCallback(workLoop);
上面代码可以实现第一章的dom结构;
八 fiber架构本质
循环条件:利用requestIdleCallback空闲时间递减
遍历过程:利用链表,找孩子找兄弟找父亲;
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!