对于异步概念还不懂的同学,请先阅读前面写过的:事件循环机制、调用栈、堆、主线程、任务队列、微任务队列、缓存管理之间的关系JS &
定时器有两种分别是: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
也一样,这里就不演示了。
既然 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)
})
})
按照这种思路,明显很快形成一个俄罗斯套娃布局,也常被人称为“回调地狱”。
由于回调函数写法实在太难看了,于是 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()
,看起来也不美观呀 …
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
来处理&接受数据。
简直就是降维打击呀有木有?