H5新Api | requestIdleCallback - requestAnimationFram

浏览器渲染机制

  • 每一轮 Event Loop 都会伴随着渲染吗?
  • requestAnimationFrame 在哪个阶段执行,在渲染前还是后?在 microTask 的前还是后?
    requestAnimationFrame在重新渲染屏幕之前执行
  • requestIdleCallback 在哪个阶段执行?如何去执行?在渲染前还是后?
    requestIdleCallback在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度

事件循环机制

作用:事件循环机制的作用是协调事件、用户交互、脚本、渲染及网络任务等。

宏队列与微队列

一个事件循环有一个或多个宏队列,有一个微队列

一个宏队列在数据结构上是一个集合(叫做任务队列),事件循环处理模型会从选定的任务队列中获取一个可运行任务。微队列是FIFO先进先出队列。

  • 宏任务
    • setTimeout、setInterval
    • setImmediate(node 独有)
    • DOM事件、Ajax事件
    • 用户交互、用户操作事件
    • script(整体代码)
    • indexDB操作
  • 微任务
    • process.nextTick
    • Promise一些方法,如.then
    • Async/Await(实际就是promise)
    • MutationObserver(html5新特性)

浏览器中事件循环流程

在这里插入图片描述

  • 同步任务和异步任务进入不同的执行环境,同步任务放入执行栈中,异步任务放入任务队列中。
  1. 先执行同步代码
  2. 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
  3. 进入更新渲染阶段,判断是否需要渲染(根据屏幕刷新率、页面性能等)。并不是每轮事件循环都会执行浏览器渲染
  4. 没有就开启下一轮循环,取出一个宏任务执行。一个宏任务执行完毕后就清空微队列,然后见检查需不需要。循环这个过程

DOM的修改不会立刻导致渲染,渲染线程和Javascript线程是互斥的,必须等待Javascript的这次调度执行完或线程挂起了,才能执行渲染。
这次调度可以看成是一轮事件循环完,一次事件循环=宏任务(第一次是同步代码)+微任务
在这里插入图片描述

requestAnimationFrame(rAF)

是什么

requestAnimationFrame是H5新增的API类似于setTimeout ,告诉浏览器在重新渲染屏幕之前执行。主要用途是按帧对网页进行重绘。

rAF是官方推荐的用来做一些流畅动画所应该使用的 API,做动画不可避免的会去更改 DOM,而如果在渲染之后再去更改 DOM,那就只能等到下一轮渲染机会的时候才能去绘制出来了

优势
调用时机:在重新渲染前调用。
requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。保证回调函数在屏幕每一次刷新间隔中只执行一次,避免丢帧

如果浏览器不渲染,是不是就不会调用requestAnimationFrame? 如果requestAnimationFrame做太多事情,会导致降频,比如1s刷新60次变成1s刷新30次。

requestAnimationFrame API

基本语法:requestAnimationFrame (callback)
返回值:回调函数列表中的唯一值,可以使用cancelAnimationFrame传入请求ID取消回调函数。

说明

  1. requestAnimationFrame不管理回调函数,意思是多次调用带有同一回调函数的 requestAnimationFrame,会导致回调在同一帧中执行多次。
    由rAF的返回值是回调函数列表中的唯一值,可以理解为即使是同一回调函数,但是在回调函数列表中的值都是不一样的。
    所以配合cancelAnimationFrame使用。
  2. requestAnimationFrame() 运行在后台标签页或者隐藏的<iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

requestIdleCallback

对于人眼来说,当每秒切换60张图片时,就会认为是连贯的。所以主流的显示器是60hz的(1s刷新60次),那么每16.7ms需要刷新一次,浏览器会自动适配这个频率,这时对应前端页面就是每16.7ms需要渲染一次。
在这里插入图片描述

页面每隔16.7ms才会渲染一次,那么在两次渲染的中间时间,就是浏览器的空闲时间,在这段空闲时间执行的任务,是不会阻塞到页面渲染的流畅性的。

如果在某一帧区间内执行过多的任务会导致下一帧一直没办法渲染,页面看起来就被卡住。

对大量任务的计算首先考虑Web Worker 使其不占用主线程,如果需要操作DOM,可以考虑任务拆分。

在这里插入图片描述
图中一帧包括了用户的交互, JavaScript 脚本执行; 以及requestAnimationFrame(rAF)的调用, 布局计算以及页面重绘等。如果某一帧里执行的任务不多, 在不到 16.66ms内就完成了上述任务, 那么这一帧就会有一定空闲时间来执行requestIdleCallback的回调。会在layout/paint 之前调用。

回流也叫重排(layout),当 DOM 的变化影响了元素的几何信息(位置、尺寸大小等),浏览器需要重新计算元素的几何属性,将其安放在界面的正确位置,这个过程叫做回流。
当一个元素的外观发生变化,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘(paint)。

requestIdleCallback API

基本语法:requestIdleCallback(callback,options)

  • callback被调用时,回接受一个参数 deadlinedeadline是一个对象,对象上有两个属性
    • timeRemaining属性是一个函数,函数的返回值表示当前空闲时间还剩下多少时间
    • didTimeoutdidTimeout属性是一个布尔值,如果didTimeouttrue,那么表示本次callback的执行是因为超时的原因
  • options是一个对象,可以用来配置超时时间。如果指定了timeout,但是浏览器没有在timeout指定的时间内,执行callback。在下次空闲时间时,callback会强制执行。并且callback的参数,deadline.didTimeout=true, deadline.timeRemaining()返回0。

空闲时间
在空闲期间,callback的执行顺序是以FIFO(先进先出)的顺序。但是如果在空闲时间内依次执行callback时,有一个callback的执行时间,已经将空闲时间用完了,剩下的callback将会在下一次的空闲时间执行。
在这里插入图片描述

const startTask = (deadline) {
    // 如果 `task` 花费的时间是20ms
    // 超过了当前空闲时间的剩余毫秒数,我们等到下一次空闲时间执行task
    if (deadline.timeRemaining() <= 20) {
        // 将任务带到下一个空闲时间周期内
        // 添加到下一个空闲时间周期callback列表的末尾
        requestIdleCallback(startTask)
    } else {
        // 执行任务
        task()
    }
}

当网页处于不可见的状态时(比如切换到其他的tag),空闲时间将会每10s, 触发一次空闲期。

任务拆分

将批量的任务进行拆分,保证这些任务只在空闲时间执行。每次执行下一个任务时,先检查当前页面是否该渲染下一帧了,如果是则让出线程,进行页面渲染。
requestIdleCallback是浏览器提供给我们用来判断这个时间的api,它会在浏览器空闲的时候来执行其回调函数,如果指定了超时时间,会在超时后的下一帧强制执行。

const id = window.requestIdleCallback((deadline) => {
  // 当前帧剩余时间大于0,或任务已超时
  if(deadline.timeRemaining() > 0 || deadline.didTimeout) {
      // do something
      console.log(1)
  }
}, { timeout: 2000 }) // 指定超时时间

// window.cancelIdleCallback(id) 与定时器类似,支持取消

requestIdleCallback在Event Loop的执行时机如下图所示,蓝色区域代表一帧内的渲染任务,当这些任务执行完后,剩余的时间被认为是空闲时间。
在这里插入图片描述
使用案例

// class中的一个方法
idleDownload(){
	// 先取消之前的
	cancelIdleCallback(this.ridId); // ridId是class中的属性,存放一个requestIdleCallback的id
	const { tasks } = this // tasks为总任务数
	let index = 0; // 任务索引
	const ridOption = {timeout:2000}; // 指定超时时间,会在超时后的下一帧强制执行。
	// 当前帧空闲时执行的回调函数
	const handler = (idleDeadline) => {
		const {timeRemaining} = idleDeadline; // 获取空闲时间
		while(timeRemaining()>0 && index<tasks.length ){
			// 在空闲时间执行任务
			index ++;
		}
		// 判断任务是否下载完成
		if(index< tasks.length){ // 不空闲了,但是任务还没有执行完毕
			this.ridId = requestIdleCallback(handler, ridOption); // 继续等待下次空闲时下载
		}else{ 
		// 已经下载完毕
		}
	}
	this.ridId =  requestIdleCallback(handler, ridOption); 
}
requestIdleCallback的使用场景

适用场景

  1. 预加载
  2. 检测卡顿
    如果requestIdleCallback长时间内没能得到执行,说明一直没有空闲时间,很有可能就是发生了卡顿,从而可以打点上报。它比较适用于行为卡顿,举个例子:点击某个按钮并同时添加requestIdleCallback 回调,如果点击后的一段时间内这个回调没有得到执行,很大概率是这个点击操作造成了卡顿。
  3. 拆分耗时任务

不适用场景

  • 更新DOM操作
    requestIdleCallback回调执行之前, 样式变更以及布局计算等都已经完成。如果在callback中修改DOM, 之前所作的布局计算都会失效。 并且如果下一帧里有获取布局相关的操作, 浏览器就需要强制进行重排, 极大的影响性能。 另外由于修改 DOM 的时间是不可预测的, 因此容易超过当前帧空闲的阈值.
  • promise 的回调(resolve/reject)属于优先级较高任务,在一帧的过程中如果产生了微任务会执行微任务。所以会在 requestIdleCallback 回调结束后立即执行,可能会给这一帧带来超时的风险。
// console
// 空闲时间1
// 等待了1000ms
// 空闲时间2
// Promise 会在空闲时间1接受后立即执行,即使没有空闲时间了也是如此。拖延了进入下一帧的时间

requestIdleCallback(() => {
    console.log('空闲时间1')
    Promise.resolve().then(() => {
        sleep(1000)
        console.log('等待了1000ms')
    })
})

requestIdleCallback(() => {
    console.log('空闲时间2')
})

参考文章:
批量任务导致页面卡死?试试requestIdleCallback对任务进行拆分
详解 requestIdleCallback
requestAnimationFrame 执行机制探索

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值