JS & setTimeout/setInterval、requestAnimationFrame 的关系以及回调函数、Promise、async/await 异步操作

文章探讨了JavaScript中的定时器setTimeout和setInterval在处理动画和频繁操作时可能带来的性能问题,推荐使用requestAnimationFrame以适应60帧/s的标准。同时,介绍了从回调函数到Promise再到async/await的异步处理进化,展示了如何通过async/await简化异步代码,提高代码可读性。
摘要由CSDN通过智能技术生成

对于异步概念还不懂的同学,请先阅读前面写过的:事件循环机制、调用栈、堆、主线程、任务队列、微任务队列、缓存管理之间的关系JS &

一、定时器 setTimeout/setInterval

定时器有两种分别是:setTimeout(只定时执行一次)和 setInterval(定时周期执行),例如:

const timeout = setTimeout(() => console.log('Hello, time'), 1000)
const interval = setInterval(() => console.log('Hello, more time'), 1000)

// 如果不想执行了,可以使用以下俩方法来解除定时器。
clearTimeout(timeout)
clearInterval(interval)

定时器在调用次数少的情况下,是无伤大雅的,可涉及动画 DOM 操作时,调用频率则变得非常高,极易出现性能和调用栈阻塞的问题,在展开问题前,有必要先了解帧数(FPS)的概念。

帧数,可以简单理解为“刷新频率”,比如在看电影或电视剧的时候,里面的人物动作实际上会通过不断地刷新来达到与现实一样的效果,问题是,为什么我们人眼看不出来像是刷新的样子?这就涉及到了人类的视觉感知。

据了解,在 刷新频率 >= 60 时,人类是感知不到快与慢的,只有在 < 60 时,才能逐渐感受到。于是 60帧/s 到了 Web 动画领域便成为一个标准,其目的就是让动画效果能够像影视剧那样丝滑地渲染;另外,像一般电脑配通常设置的也是 60帧/s,感兴趣的同学可以检查下。

言归正传,为什么定时器就不适用于动画呢?我们来做个实验:

var count = 0;
var start = Date.now();
function loop() {
  count += 1;
  if (Date.now() - start < 5000) {
    setTimeout(loop, 0);
  } else {
    console.log('5秒内循环了', count, '次');
  }
}
setTimeout(loop, 0);

将上面的代码放到控制台执行后将会呈现:“5秒内执行了 1009 次“(结果因电脑而异,性能越高,次数就越多)

于是问题就来了,1009 / 5 相当于每秒执行了 202 次,是帧数 60 的 3 倍之多,可人类的视觉感知并不需要这么高的刷新率;更何况,定时器的函数还会被放到事件循环 机制去执行,极易占用调用栈,明显这并不可取。
setInterval 也一样,这里就不演示了。

二、定时器的改进版 requestAnimationFrame

既然 setTimeout/setInterval 造成帧数浪费,有没有其它函数能控制在 60帧/s 呢?答案是 requestAnimationFrame,所有浏览器均支持,我们来看例子:

var count = 0;
var start = Date.now();
function loop() {
  count += 1;
  if (Date.now() - start < 5000) {
    requestAnimationFrame(loop);
  } else {
    console.log('5秒内循环了', count, '次');
  }
}
requestAnimationFrame(loop);

上面的代码将会打印:“5秒内循环了 302 次”,注意这是一个固定的值,所有电脑执行的结果均一致。
302 / 5 = 60.4 ,满足每秒 60 的帧数效果,而且 requestAnimationFrame 函数默认会放到’渲染循环‘机制中执行,与’事件循环‘机制解耦了。

因此,涉及到 DOM 动画操作时,应尽可能选择 requestAnimationFrame 作为实现方案,避免使用 setTimeout/setInterval。

三、回调函数

回调函数表示参数是一个函数,属于异步操作思想,下面举一个读取文件的例子:

const readFile = (callback) => {
	// 巴啦啦的一顿操作后,再将结果传给回调函数。
	const data = 'File'
	callback(data)
}

const getFile = readFile((file) => {
	console.log('Received a file', file)
})

假如回调再嵌套回调函数呢?于是写法就变成了这样:

const readFile = (callback) => {
	// 巴啦啦的一顿操作后,将结果给回调函数。
	const data = 'File'
	callback(data)
}
const fileToPDF = (file, callback) => {
	// 一顿神仙操作后,将结果给回调函数。
	const data = 'PDF'
	callback(data)
}
const getFile = readFile((file) => {
	fileToPDF(file, (pdf) => {
		console.log('Received a pdf', pdf)
	})
})

按照这种思路,明显很快形成一个俄罗斯套娃布局,也常被人称为“回调地狱”。

三、回调函数的改进版 Promise

由于回调函数写法实在太难看了,于是 JS 出了一个 API 叫 Promise
它是一种带有成功/失败的回调函数,其结果要么是失败要么是成功,不可能同时出现失败&&成功,
事实上,这种状态我们也可以在回调中实现。但是呢,Promise 真正有用的地方在于写法优雅,我们来将上面的例子重新改造下:

const readFile = () => {
	return new Promise((resolve, reject) => {
		if (1 === 1) {
			resolve('File') // 成功,将结果返回出去。
		} else {
			reject(0) // 失败
		}
	})
}
const fileToPDF = (file, callback) => {
	return new Promise((resolve, reject) => {
		if (file === 'File') {
			resolve('PDF') // 成功,将结果返回出去。
		} else {
			reject(0) // 失败
		}
	})
}
const getFile = readFile()
.then((file) => {
	fileToPDF(file).then((pdf) => {
		console.log('Received a pdf', pdf)
	})
})

咦,运行结果与上面的回调例子一样,但这里的代码咋看起来好像和回调没区别啊?嵌套风格还是存在,
别急,只是 Promise 的语法之一,我们还可以将它变成这样这种写法:

const getFile = readFile()
.then((file) => {
	return fileToPDF(file)
})
.then((pdf) => {
	console.log('Received a pdf', pdf)
})

原理是调用 fileToPDF 后,函数本身会返回一个 Promise,于是我们就可以通过链式操作,将. then 放到后面来接受前面的数据,这样看起来是不是更简洁了呢?

问题又来了,如果回调数量增多,会导致有许多个 .then() ,看起来也不美观呀 …

四、Promise 的改进版 Async/await

JS 是一门美丽的语言,不允许不美观的行为,便有了现在的 async/await 语法;
可以简单想成 Promise/then,好处是,它写起来更简洁,布局就跟写同步代码似的,我们将上面的例子再改造下:

const readFile = () => {
	return new Promise((resolve, reject) => {
		if (1 === 1) {
			resolve('File') // 成功,将结果返回出去。
		} else {
			reject(0) // 失败
		}
	})
}
const fileToPDF = (file, callback) => {
	return new Promise((resolve, reject) => {
		if (file === 'File') {
			resolve('PDF') // 成功,将结果返回出去。
		} else {
			reject(0) // 失败
		}
	})
}
// + 只需改造这一部分。
const getFile = async () => {
	const file = await readFile()
	const pdf = await fileToPDF(file)
	console.log('Received a pdf', pdf)
}
getFile();

原来的 readFile / fileToPDF 不变,只需给 getFile 函数加个 async 修饰符,函数里再通过 await 来处理&接受数据。
简直就是降维打击呀有木有?

五、总结

  1. 动画:setTimeout/setInterval——requestAnimationFrame (本身也属于异步操作)
  2. 异步操作:回调函数 ——Promise ——async/await
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cookcyq

请作者喝杯暖暖的奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值