帧
- 大多数设备屏幕的刷新频率为60次/秒
- 页面帧数(FPS)小于60时,会感觉到页面卡顿
- 每帧的时间大约是16.66毫秒
- 每帧的包括:样式计算、布局、绘制
- JavaScript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和JavaScript执行两者是互斥的
- 任务执行时间过长,浏览器会推迟渲染
一个帧:
Fiber
- 把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行。
- 通过Fiber架构,让自己的协调过程变成可被中断的。适时让出CPU执行权,让浏览器及时响应
什么是fiber
- Fiber是一个执行单元
每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去 - Fiber是一种数据结构
使用链表,每个虚拟dom节点,内部表示为一个Fiber,可以用一个JS对象来表示
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
举个🌰
let element= <div id="A1">
<div id="B1">
<div id="C1"></div>
<div id="C2"></div>
</div>
<div id="B2"></div>
</div>
requestAnimationFrame(每帧执行)
requestAnimationFrame(callback)会在浏览器每次重绘前执行callback回调
🌰
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div style="background-color: yellow;width:0;height: 30px;"></div>
<button>开始</button>
<script>
let div = document.querySelector('div')
let button = document.querySelector('button')
let startTime
button.onclick = function () {
div.style.width = 0
startTime = Date.now()
requestAnimationFrame(progress)
}
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)
}
}
</script>
</body>
</html>
requestIdleCallback(请求空闲调用)
- requestIdleCallback在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
- 正常帧任务完成后没超过16ms,说明时间有富余,此时会执行requestIdleCallback里注册的任务
- requestAnimationFrame 的回调会在每一帧确定执行,属于高优先级任务
requestIdleCallback的回调不一定执行,属于低优先级任务
MessageChannel
- requestIdleCallback目前只有Chrome支持
- React利用MessageChannel模拟requestIdleCallback将回调延迟到操作之后执行
- MessageChannel通过两个MessagePort属性发送数据
- MessageChannel是一个宏任务
🌰
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function progress () {
console.log('每次帧执行')
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)=>{
// 每一帧开始的时间加上16.6 就是一帧截止的时间
frameDeadline = rafTime + activeFrameTime
pendingCallback = callback
channel.port1.postMessage('hello')
})
}
function sleep(duration) { // 睡眠时间
let start = Date.now()
while (start + duration > Date.now()) {
}
}
const works = [
() => {
console.log('A1开始')
sleep(20)
console.log('A1结束')
},
() => {
console.log('A2开始')
sleep(20)
console.log('A2结束')
},
() => {
console.log('A3开始')
sleep(20)
console.log('A3结束')
},
]
// 让浏览器在空闲的时间执行任务,但是如果已经过期了(超过1秒还没执行),就立刻执行
requestIdleCallback(workLoop, {timeout: 1000})
// 循环执行队列
function workLoop(deadline) {
console.log('本帧的剩余时间', deadline.timeRemaining())
// 如果说还有剩余时间,或者此任务已过期了,并且还有没完成的任务
while((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0 ) {
performUnitOfWork()
}
// 时间片已经到期了,等待下次调度
if (works.length > 0) {
requestIdleCallback(workLoop)
}
}
function performUnitOfWork () {
let work = works.shift(); // 执行任务数组的第一个
work()
}
</script>
</body>
</html>
Fiber执行
每次渲染有两个阶段:Reconciliation(协调render阶段)和Commit(提交阶段)
- 协调阶段,可以认为是diff阶段,这个阶段可以被中断,这个阶段会找出所有节点变更,例如:节点新增、删除、属性变更等,这些变更被react称为副作用(effects)
- 提交阶段:将上一个阶段计算出来需要处理的副作用(effects)一次性执行,必须同步执行,不能被打断
render阶段
- 从顶点开始遍历
- 如果有第一个子节点,先遍历第一个子节点如果没有子节点,标志着此节点遍历完成
- 如果有兄弟节点,遍历兄弟节点,如果没有下一个兄弟节点,返回父节点,此父节点遍历完成
- 遵循深度优先遍历规则
commit阶段
firstEffect指向第一个有副作用的子节点,lastEffect指向最后一个有副作用的子节点
🌰 fiber渲染实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='root'></div>
<script>
let root = {
type: 'div',
props: {
id: 'A1',
children: [
{
type: 'div',
props: {
id: 'B1',
children: [
{
type: 'div',
props: {
id: 'C1',
children: []
},
},
{
type: 'div',
props: {
id: 'C2',
children: []
},
}
]
},
},
{
type: 'div',
props: {
id: 'B2',
children: []
},
}
]
},
}
let container = document.getElementById('root')
const PLACEMENT = 'PLACEMENT' // 插入
//应用的根,fiber其实也是一个普通的js对象
let workInProgressRoot = {
stateNode: container,// 此fiber对应的DOM节点
props:{children:[root]}// fiber的属性
// child,
// sibling,
// return
}
//下一个工作单元
let nextUnitOfWork = workInProgressRoot
function workLoop(deadline) {
// 如果有当前的工作单元,就执行,并返回一个工作单元
while(nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (!nextUnitOfWork) {
commitRoot() // 挂载
}
}
function commitRoot () {
let currentFiber = workInProgressRoot.firstEffect
while(currentFiber) {
if (currentFiber.effectTag === 'PLACEMENT') { // 如果是插入,就挂载
currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
}
currentFiber = currentFiber.nextEffect
}
workInProgressRoot = null
}
/**
* beginWork 1.创建此fiber的真实dom,通过虚拟DOM创建fiber结构
* @param {*} workingInProgressFiber
*
* **/
function performUnitOfWork (workingInProgressFiber) {
// 1.创建真实DOM,并没有挂载,2.创建fiber子树
beginWork(workingInProgressFiber)
if (workingInProgressFiber.child) {
return workingInProgressFiber.child //如果有子节点,返回子节点
}
while(workingInProgressFiber) { // 没有子节点的时候
completeUnitWork(workingInProgressFiber) // 如果没有子节点,当前节点就结束完成了
if(workingInProgressFiber.sibling) { // 有兄弟节点,返回兄弟节点
return workingInProgressFiber.sibling
}
workingInProgressFiber = workingInProgressFiber.return // 没有兄弟节点,指向父亲节点
}
}
function beginWork(workingInProgressFiber) {
console.log('begin' ,workingInProgressFiber.props.id)
if (!workingInProgressFiber.stateNode) {
workingInProgressFiber.stateNode = document.createElement(workingInProgressFiber.type)
}
for (let key in workingInProgressFiber.props) {
workingInProgressFiber.stateNode[key] = workingInProgressFiber.props[key]
}
// 在beginWork中不会挂载
// 创建子fiber
let previousFiber
// children是一个虚拟DOM数组
workingInProgressFiber.props.children.forEach((child, index) => {
let childFiber = {
type: child.type,
props: child.props,
return: workingInProgressFiber,
effectTag: 'PLACEMENT', // 这个fiber对应的dom节点,需要被插入到页面中父DOM里去
}
if (index === 0) {
workingInProgressFiber.child = childFiber
} else {
previousFiber.sibling = childFiber
}
previousFiber = childFiber
});
}
function completeUnitWork (workingInprogressFiber) {
console.log('completeUnitWork', workingInprogressFiber.props.id)
// 构建副作用链,effectList 指那些有副作用的节点
let returnFiber = workingInprogressFiber.return
if (returnFiber) {
//把当前fiber的有副作用的子链表挂载到父节点上
if(!returnFiber.firstEffect) {
returnFiber.firstEffect = workingInprogressFiber.firstEffect
}
if (workingInprogressFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workingInprogressFiber.firstEffect
}
returnFiber.lastEffect = workingInprogressFiber.lastEffect
}
// 再把自己挂到后面
if(workingInprogressFiber.effectTag) {
if(returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workingInprogressFiber
} else {
returnFiber.firstEffect = workingInprogressFiber
}
returnFiber.lastEffect = workingInprogressFiber
}
}
}
// 浏览器空闲时执行
requestIdleCallback(workLoop)
</script>
</body>
</html>
执行效果: