一帧的生命周期
- 浏览器动画的执行频率一般为每秒60次。相当于每秒60帧。
-
一帧大概为16ms(1000ms/60帧),不同的电脑配置可能会有所差别
requestIdleCallback
- requestIdleCallback 为fiber的核心,它是浏览器自带的方法。可用于在浏览器每一帧的空闲时段处理回调任务(空闲时间即为上图的最后一个阶段)。单个回调任务最好能在此帧的剩余时间内完成,否则会引起卡顿。
- 这样做的好处是避免占用主线程长时间执行一个大的任务,导致浏览器卡顿。
function sleep(time) {
let startTime = Date.now();
while (Date.now() - startTime < time) {
continue;
}
}
const fnArr = [
function () {
sleep(20);
console.log('任务1')
},
function () {
sleep(20);
console.log('任务2')
},
function () {
console.log('任务3')
}
];
function rIdCb (deadline) {
console.log('此帧剩余时间' + deadline.timeRemaining());
// timeRemaining 代表剩余时间; didTimeout代表是否已超时;
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && fnArr.length > 0) {
fnArr.shift()();
}
if(fnArr.length > 0){
requestIdleCallback(rIdCb, {timeout: 1000}); // timeout代表执行rIdCb的超时时间,如果超过该时间仍未执行,didTimeout则为true;
}
}
requestIdleCallback(rIdCb, { timeout: 1000 });
requestAnimationFrame
- 每次执行完优先级高的input click事件、timeout、浏览器窗口等事件,会在绘制页面前执行requestAnimationFrame的回调方法。
- 由于每一帧的时间周期为16ms左右,因此以下demo打印的时间差为16ms左右,同时可实现动画平滑的效果。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>test</title>
<style>
.box {
width: 0px;
height: 20px;
background: lightgray;
}
</style>
</head>
<body>
<div class="box"></div>
<button id="btn">change width</button>
<script type="text/javascript">
const obtn = document.getElementById('btn');
const odiv = document.getElementsByClassName('box')[0];
let start = 0;
obtn.addEventListener('click', () => {
odiv.style.width = 0;
start = Date.now();
requestAnimationFrame(rafCb);
})
const rafCb = () => {
let currTime = Date.now();
console.log(currTime - start); // 此时间差为每一帧的执行周期,为16ms左右
start = currTime;
odiv.style.width = odiv.offsetWidth + 1 + 'px';
odiv.innerHTML = odiv.offsetWidth + '%';
if (odiv.offsetWidth < 100) {
requestAnimationFrame(rafCb); // 利用raf代替常规的while循环或递归,可实现16ms执行一次rafCb,实现了既不占用主线程,动画又平滑的效果
}
}
</script>
</body>
</html>
单链表
// 定义第一份数据
let data1 = { x: 1 };
// 将第一份数据data1的内存地址指向火车头和最后一节车厢
let huochetou = lastone = data1;
// 定义第二份数据
let data2 = { y: 2 };
// 将第二份数据data2的内存地址指向最后一节车厢的next属性。(与此同时,火车头也拥有的next属性,其值为data2);
lastone.next = data2;
// 将第二份数据data2的内存地址指向最后一节车厢lastone。(此时,火车头的next属性和lastone指向的都是data2的内存地址);
lastone = data2;
// 定义第三份数据
let data3 = { z: 3 };
// 将第三份数据data3的内存地址指向lastone的next属性。(此时火车头的next将新增next属性,并指向data3)
lastone.next = data3;
// 将lastone指向data3
lastone = data3;
最终在火车头上通过next实现了单链表效果
- 利用单链表实现react的forceUpdate
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>test</title>
</head>
<body>
<div class="box"></div>
<button id="btn">change width</button>
<script type="text/javascript">
class Update {
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.lastUpdate = update;
this.firstUpdate = this.lastUpdate;
} else {
this.lastUpdate.nextUpdate = update;
this.lastUpdate = update;
}
}
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;
}
}
let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({ name: 'skyler' }));
queue.enqueueUpdate(new Update({ number: 0 }));
queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));
console.log(queue.firstUpdate, queue.lastUpdate);
queue.forceUpdate();
console.log(queue.baseState);
</script>
</body>
</html>
react的链表结构及遍历规则
- 数据结构
const A1 = { type: 'div', key: 'A1' };
const B1 = { type: 'div', key: 'B1', return: A1 };
const B2 = { type: 'div', key: 'B2', return: A1 };
const C1 = { type: 'div', key: 'C1', return: B1 };
const C2 = { type: 'div', key: 'C2', return: B1 };
A1.child = B1;
B1.child = C1;
B1.sibling = B2;
C1.sibling = C2;
-
遍历规则 绿色会遍历顺序,蓝色为完成顺序
image.png - 代码实现
// fiber 数据结构
const A1 = { type: 'div', key: 'A1' };
const B1 = { type: 'div', key: 'B1', return: A1 };
const B2 = { type: 'div', key: 'B2', return: A1 };
const C1 = { type: 'div', key: 'C1', return: B1 };
const C2 = { type: 'div', key: 'C2', return: B1 };
// 每个节点只有自己的父亲、大儿子、弟弟
A1.child = B1; // 每个父亲只携带一个大儿子,让大儿子携带大儿子自己的弟弟,以此建立链条关系
B1.child = C1;
B1.sibling = B2;
C1.sibling = C2;
/**
* 1. 开始从顶点遍历。
* 2. 如果有大儿子,先遍历大儿子。(同时要等自己所有的儿子任务完成自己的任务才算完成。)
* 3. 如果没有大儿子,说明自己的任务已完成。开始找自己的弟弟。
* 4. 如果找到弟弟,开始遍历弟弟。
* 5. 没有弟弟,则通过自己的父亲找父亲的弟弟,同时代表自己的父亲已完成全部工作
* 6. 父亲的弟弟开始循环上面的步骤,直到根节点完成任务
*/
let rootFiber = A1; // A1为根节点 通知携带了自己的所有的children
// 1. 开始从顶点遍历
workLoop(rootFilber);
// nextUnitOfWork为下一个执行单元(链表的单个节点,也叫做一个fiber)
function workLoop(nextUnitOfWork) {
while (nextUnitOfWork) { // 如果有执行单元就执行,然后返回下一个执行执行单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
console.log('render阶段结束')
}
// 处理一个fiber
function performUnitOfWork(fiber) {
beginWork(fiber); // 此fiber开始工作
if (fiber.child) { // 2. 如果有大儿子先遍历大儿子
return fiber.child;
} // 3. 如果没有大儿子,说明自己的任务已完成。开始找自己的弟弟。
while (fiber) {
completeUnitOfWork(fiber);
if (fiber.sibling) { // 4. 如果找到弟弟,开始遍历弟弟。
return fiber.sibling;
} // 5. 没有弟弟,则通过自己的父亲找父亲的弟弟,同时代表自己的父亲已完成全部工作
fiber = fiber.return;
}
return null; // 6. 代表任务执行到了rootFiber的父亲,它的父亲为null,所以本次任务结束运行
}
function completeUnitOfWork(fiber) {
console.log(fiber.key, '执行完成');
}
function beginWork(fiber) {
console.log(fiber.key, '开始执行'); // A1 B1 C1
}
requestIdleCallback和链表遍历相结合
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// fiber 数据结构
const A1 = { type: 'div', key: 'A1' };
const B1 = { type: 'div', key: 'B1', return: A1 };
const B2 = { type: 'div', key: 'B2', return: A1 };
const C1 = { type: 'div', key: 'C1', return: B1 };
const C2 = { type: 'div', key: 'C2', return: B1 };
// 每个节点只有自己的父亲、大儿子、弟弟
A1.child = B1; // 每个父亲只携带一个大儿子,让大儿子携带大儿子自己的弟弟,以此建立链条关系
B1.child = C1;
B1.sibling = B2;
C1.sibling = C2;
/**
* 1. 开始从顶点遍历。
* 2. 如果有大儿子,先遍历大儿子。(同时要等自己所有的儿子任务完成自己的任务才算完成。)
* 3. 如果没有大儿子,说明自己的任务已完成。开始找自己的弟弟。
* 4. 如果找到弟弟,开始遍历弟弟。
* 5. 没有弟弟,则通过自己的父亲找父亲的弟弟,同时代表自己的父亲已完成全部工作
* 6. 父亲的弟弟开始循环上面的步骤,直到根节点完成任务
*/
let nextUnitOfWork = A1;
let startTime = Date.now();
// 1. 开始从顶点遍历
requestIdleCallback(workLoop, { timeout: 1000 });
// nextUnitOfWork为下一个执行单元(链表的单个节点,也叫做一个fiber)
function workLoop(deadline) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) { // 如果有执行单元并且本帧还有剩余时间就执行,然后返回下一个执行执行单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork) {
console.log('render阶段结束。共耗时:', Date.now() - startTime);
} else {
requestIdleCallback(workLoop, { timeout: 1000 })
}
}
// 处理一个fiber
function performUnitOfWork(fiber) {
beginWork(fiber); // 此fiber开始工作
if (fiber.child) { // 2. 如果有大儿子先遍历大儿子
return fiber.child;
} // 3. 如果没有大儿子,说明自己的任务已完成。开始找自己的弟弟。
while (fiber) {
completeUnitOfWork(fiber);
if (fiber.sibling) { // 4. 如果找到弟弟,开始遍历弟弟。
return fiber.sibling;
} // 5. 没有弟弟,则通过自己的父亲找父亲的弟弟,同时代表自己的父亲已完成全部工作
fiber = fiber.return;
}
return null; // 6. 代表任务执行到了rootFiber的父亲,它的父亲为null,所以本次任务结束运行
}
function completeUnitOfWork(fiber) {
sleep(20);
console.log(fiber.key, '执行完成');
}
function beginWork(fiber) {
console.log(fiber.key, '开始执行'); // A1 B1 C1
}
function sleep(time) {
let start = Date.now();
while (Date.now() - start < time) { }
}
</script>
</body>
</html>
网友评论