用过react框架的开发者都知道react 16 后应用到fiber 架构,今天来跟大家唠叨一下fiber及react源码,虽然react17也已经发布,但官方表示了并没有其他多大的变化,所以还是基于16版本。
fiber
首先一点是官方为什么要用fiber及什么时fiber,这个就要从16之前的版本来说了,在没有应用之前,官方的一句话解释是“React Fiber是对核心算法的一次重新实现”,说白了引用了fiber是未来提高性能
同步更新过程的局限
在现有React中,更新过程是同步的,这可能会导致性能问题。
当React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,那React就以不破楼兰终不还的气概,一鼓作气运行到底,中途绝不停歇。
表面上看,这样的设计也是挺合理的,因为更新过程不会有任何I/O操作嘛,完全是CPU计算,所以无需异步操作,的确只要一路狂奔就行了,但是,当组件树比较庞大的时候,问题就来了。 假如更新一个组件需要1毫秒,如果有200个组件要更新,那就需要200毫秒,在这200毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这200毫秒内,用户往一个input元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被React占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等React更新过程结束之后,咔咔咔那些按键一下子出现在input元素里了。 这就是所谓的界面卡顿,很不好的用户体验。
简单一点来说就是,react更新过程,如果更新几百个组件,这种遍历是递归调用,执行栈会越来越深。而且 不能中断,因为中断后再想恢复 就非常难了,遍历很深的话,因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。
而fiber实现原理是:把整个任务分解成很多小任务,每次执行一个任务后看一下有没有剩余时间,如果有,继续下一个任务,如果没有时间,则放弃执行,交给浏览器进行调度
所以fiber 是一个可执行单元,是一种数据结构
requestIdleCallback
一般屏幕的刷新频率24Hz,一帧16.6 如果每次执行一个fiber因为此任务的执行时间已经超过了16.6毫秒, 那么需要把控制 权交给浏览器。
其中window.requestIdleCallback(callback, { timeout: 1000 });
react内部调用了window里面的这个函数,
-
callback:回调,即空闲时需要执行的任务,该回调函数接收一个
IdleDeadline
对象作为入参。其中IdleDeadline
对象包含: -
didTimeout
,布尔值,表示任务是否超时,结合timeRemaining
使用。 -
timeRemaining()
,表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。 我们写一个案例来模拟一下,感受一下requestIdleCallback的用法:
//这是一个全局属性
//我作为用户,告诉浏览器,我现在执行callback函数,但是它的优先级比较低。告诉 浏览器,可以空闲的时候执行callback
//但是如何这个到了超时时间了,就必须马上执行
//window.requestIdleCallback(callback, { timeout: 1000 });
function sleep(delay) { //d=50//
//在JS里如何实现睡眠的功能 t=当前时间
for (var start = Date.now(); Date.now() - start <= delay;) {
console.log(222);
}
}
sleep(2)
console.log(444);
let allStart = Date.now();
//fiber是把整个任务分成很多个小任务,每次执行一个任务
//执行完成后会看看有没剩余时间,如果有继续下一个任务,如果没有放弃执行,交给浏览器进行调度
const works = [
() => {
console.log('第1个任务开始');
while (true) { }
sleep(3);//一帧16.6 因为此任务的执行时间已经超过了16.6毫秒,所需要把控制 权交给浏览器
console.log('第1个任务结束 ');
},
() => {
console.log('第2个任务开始');
sleep(3);
console.log('第2个任务结束 ');
},
() => {
console.log('第3个任务开始');
sleep(3);
console.log('第3个任务结束 ');
console.log(Date.now() - allStart);
}
]
//告诉 浏览器在1000毫秒后,即使你没有空闲时间也得帮我执行,因为我等不及你
// requestIdleCallback(workLoop, { timeout: 1000 });
//deadline是一个对象 有两个属性
// timeRemaining()可以返回此帧还剩下多少时间供用户使用
// didTimeout 此callback任务是否超时
function workLoop(deadline) {
console.log(`本帧的剩余时间为${parseInt(deadline.timeRemaining())}`);
//如果此帧的剩余时间超过0,或者此已经超时了
while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork();
}//如果说没有剩余时间了,就需要放弃执行任务控制权,执行权交还给浏览器
if (works.length > 0) {//说明还有未完成的任务
window.requestIdleCallback(workLoop, { timeout: 1000 });
}
}
function performUnitOfWork() {
//shift取出数组中的第1个元素
works.shift()();
}
现在我把更新遍历加到requestIdleCallback中:
/**
* 1.从顶点开始遍历
* 2.如果有大儿子,先遍历大儿子
*
*/
function sleep(delay) { //d=50//
//在JS里如何实现睡眠的功能 t=当前时间
for (var start = Date.now(); Date.now() - start <= delay;) { }
}
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 }
let B2 = { type: 'div', key: 'B2', return: A1 }
let C1 = { type: 'div', key: 'C1', return: B1 }
let C2 = { type: 'div', key: 'C2', return: B1 }
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
let nextUnitOfWork = null;//下一个执行单元
let startTime = Date.now();
function workLoop(deadline) {
//while (nextUnitOfWork) {//如果有待执行的执行单元,就执行,然后会返回下一个执行单元
while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork) {
console.log('render阶段结束了');
console.log(Date.now() - startTime);
} else {//请求下次浏览器空闲的时候帮我调
requestIdleCallback(workLoop, { timeout: 1000 });
}
}
function performUnitOfWork(fiber) {//A1 B1 C1 C2
beginWork(fiber);//处理此fiber
if (fiber.child) {//如果有儿子,返回大儿子
return fiber.child;
}//如果没有儿子,说明此fiber已经完成了
while (fiber) {
completeUnitOfWork(fiber);
if (fiber.sibling) {
return fiber.sibling;//如果说有弟弟返回弟弟
}
fiber = fiber.return;
}
}
function completeUnitOfWork(fiber) {
console.log('结束', fiber.key);//A1 B1 C1 C2
}
function beginWork(fiber) {
sleep(20);
console.log('开始', fiber.key);//A1 B1 C1 C2 B2
}
nextUnitOfWork = A1;
requestIdleCallback(workLoop, { timeout: 1000 });
element.js
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 }
let B2 = { type: 'div', key: 'B2', return: A1 }
let C1 = { type: 'div', key: 'C1', return: B1 }
let C2 = { type: 'div', key: 'C2', return: B1 }
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
module.exports = A1;
我们再看看更新队列:
/**
* 在Fiber中 很多地方都用到链表
*/
class Update {//payload数据或者 说元素
constructor(payload, nextUpdate) {
this.payload = payload;
this.nextUpdate = nextUpdate;//指向下一个节点的指针
}
}
class UpdateQueue {
constructor() {
this.baseState = null;//原状态
this.firstUpdate = null;//第一个更新
this.lastUpdate = null;//最后一个更新
}
enqueueUpdate(update) {
if (this.firstUpdate == null) {
this.firstUpdate = this.lastUpdate = update;
} else {
this.lastUpdate.nextUpdate = update;//上一个最后一个节点的nextUpdate指向自己
this.lastUpdate = update;//让最后一个节指向自己
}
}
//1.获取老状态。然后遍历这个链表,进行更新 得到新状态
forceUpdate() {
let currentState = this.baseState || {};//初始状态
let currentUpdate = this.firstUpdate;
while (currentUpdate) {
let nextState = typeof currentUpdate.payload == 'function' ?
currentUpdate.payload(currentState) : currentUpdate.payload;
currentState = { ...currentState, ...nextState };//使用当前更新得到新的状态
currentUpdate = currentUpdate.nextUpdate;// 找下一个节点
}
this.firstUpdate = this.lastUpdate = null;//更新完成后要把链表清空
this.baseState = currentState;
return currentState;
}
}
//计数器 {number:0} setState({number:1}) setState((state)=>({number:state.number+1}))
let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({ name: 'zhufeng' }));
queue.enqueueUpdate(new Update({ number: 0 }));
queue.enqueueUpdate(new Update((state) => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update((state) => ({ number: state.number + 1 })));
queue.forceUpdate();
console.log(queue.baseState);
queue.enqueueUpdate像不像你在外部更新状态的写法,没错,react内部事用链表的方式实现的。
github源码 gitee.com/yokeney/rea…