异步是前端开发中的一个重点内容, 也是难点之一。为了更优雅地实现异步,JavaScript语言在各个历史阶段进行过多种尝试,但是由于异步天生具有一定的“复杂度”,使得开发者并不能够轻松地吃透相关的理论知识并上手实践。在理论方面,我们知道JavaScript 是单线程的,那它又是如何实现异步的呢?在这个环节中,浏览器或Node.js又起到了什么样的作用?什么是宏任务?什么是微任务?在实践方面,从callback到Pormise,从Gemeator到aypc/aia,到底应该如何更优雅地实现异步操作?下面让我们来一-探究竟。
一、红绿灯任务控制
红灯亮三秒,黄灯亮俩秒,绿灯亮一秒,如何让三个灯交替重复地亮呢
先声明亮灯函数
function green(){
console.log('绿灯亮');
}
function yellow(){
console.log('黄灯亮');
}
function red(){
console.log('红灯亮');
}
1.回调实现
function callback(){
green()
setTimeout(()=>{
yellow()
setTimeout(()=>{
red()
setTimeout(()=>callback(),3000)
},2000)
},1000)
}
2.Promise实现
const task = (timer, light) => new Promise((res,rej) => {
if(light == 'red'){
red()
}else if(light == 'yellow'){
yellow()
}else{
green()
}
setTimeout(()=>{
res()
},timer)
})
function promiseFun(){
task(1000,'green')
.then(()=> task(2000,'yellow') )
.then(()=> task(2000,'red'))
.then(promiseFun)
}
3.async/await实现
const task = (timer, light) => new Promise((res,rej) => {
if(light == 'red'){
red()
}else if(light == 'yellow'){
yellow()
}else{
green()
}
setTimeout(()=>{
res()
},timer)
})
async function awaitFn(){
await task(1000,'green')
await task(2000,'yellow')
await task(2000,'red')
awaitFn()
}
awaitFn()
二、实现图片预加载
先编写图片预加载函数,返回一个Promise,在Image的load事件中对Promise进行决议,先绑定load事件再赋值src是为了防止有浏览器缓存时,加载太快导致load事件不被触发
const loadImg = (url) => {
const baseUrl = `http://www.image.com/` //接口头
return new Promise((res,rej)=>{
var img = new Image()
img.onerror = () =>{
console.log('图片加载失败');
}
img.url = url
img.onload = () => {
res(img.url)
}
img.src = baseUrl + url
})
}
1.Promise依次预加载图片
urlArr.reduce((pre,cur) => pre.then(() => loadImg(cur)), Promise.resolve())
当前图片的url传参给loadImg,返回一个Promise,Promise决议后在then中将下一张图片的url传参给loadImg,又返回个Promise,这样重复下去
2.await依次预加载图片
var awaitImg = async () => {
for( i of urlArr){
await loadImg(i)
}
}
3.Promise.all进行图片并发预加载
const promiseArr = urlArr.map(item => loadImg(item))
Promise.all(promiseArr)
.then(()=> console.log('全部加载完成'))
.catch(() => console.log('出错了'))
Promise.all参数为Promise的数组,只有所有Promise都resolve了,才会触发then,否则触发catch
4.Promise.race和Promise.all进行图片并发预加载限制
const imgLoadLimit = (urlArr, limit) => {
const urlArrCy = [...urlArr]
if(urlArrCy.length <= limit){
const promiseArr = urlArrCy.map( url => {
var pro = loadImg(url)
pro.id = url
return pro
})
return Promise.all(promiseArr)
}
const promiseArr = urlArrCy.splice(0, limit).map( (url) => {
var pro = loadImg(url)
pro.id = url
return pro
})
urlArrCy.reduce( (prePromise,curUrl) =>
prePromise.then(() => Promise.race(promiseArr))
.catch( err => console.log(err))
.then((resUrl) => {
let promiseIndex = promiseArr.findIndex( item => item.id == resUrl)
promiseArr.splice(promiseIndex,1)
var pro = loadImg(curUrl)
pro.id = curUrl
promiseArr.push(pro)
})
,Promise.resolve())
.then(() => Promise.all(promiseArr))
}
imgLoadLimit(urlArr,3)
数组小于限制数,直接.Promise.all,否则裁剪数组前几位,数量为限制数,进行Promise.race。
Promise.race参数也是Promise数组,它的then的返回值是数组里第一个决议的Promose的值。
每次Promise决议后,通过标识找到它在数组里的位置,把它删去,再把未预加载的图片的Promise放进去,继续Promise.race。等最后一张图片的Promise放进数组后,进行Promise.all。
三、setTimeOut相关考查
1.JS分为同步任务和异步任务,同步任务形成执行栈,异步任务只会等同步任务执行完,也就是执行栈空闲才会执行
例
setTimeout(()=> console.log('setTimeOut'),1000)
console.log(2222);
while(1){
}
console.log(1111);
这里执行栈不会空闲,所以setTimeOut不会执行
setTimeout(() => {
while(1){
}
},1000)
console.log(1111);
这里console.log(1111);会执行,但在setTimeOut里卡线程了,setTimeOut之后的代码不会执行
例
const t1 = new Date()
setTimeout(() => {
const t3 = new Date()
console.log('t3 - t1:',t3-t1); //t3 - t1:200
},100)
let t2 = new Date()
while(t2 - t1 < 200){
t2 = new Date()
}
setTimeOut定时100ms后不会执行,等200ms执行栈空闲才执行
2.setTimeOut的细节
例
setTimeout(() => console.log('setTimeOut1'),200)
setTimeout(() => console.log('setTimeOut2'),100)
//setTimeOut2
//setTimeOut1
虽然第一个setTimeOut先进队列,但第二个setTimeOut定时比较早结束,所以先触发
例
setTimeout(() => console.log('setTimeOut1'),1)
setTimeout(() => console.log('setTimeOut2'),0)
//setTimeOut1
//setTimeOut2
因为1ms和0ms可以视为等价,所以第一个定时器先执行
3.宏任务和微任务
任务队列的异步任务又分宏任务和微任务,它们虽都是异步任务,都在任务队列中,又处于不同的队列中,宏任务队列和微任务队列
代码执行中,将同步任务放主线程形成执行栈,遇到的微任务放微任务队列,遇到的宏任务放宏任务队列,同步任务执行完,执行栈空闲,开始执行微任务,微任务都执行完,开始执行宏任务
宏任务包括:setTimeOut、setInterval、I/O、事件、postMessage、setImmediate(Node.js的特性,浏览器已废弃该api)、requestAnimationFrame、Ui渲染
微任务包括:Promise.then、MutationObserver、process.nextTick(Node.js)
例:
console.log('start');
setTimeout(() => console.log('setTimeOut1'),200)
let foo = () => new Promise((res, rej) => {
console.log('first promise');
new Promise((res,rej) => {
console.log('second promise');
setTimeout(() => console.log('setTimeOut2'),100)
res('second promise then')
}).then(res => console.log(res) )
res('first promise then')
})
foo().then(res => console.log(res))
console.log('end');
//输出结果
start
first promise
second promise
end
second promise then
first promise then
setTimeOut2
setTimeOut1
总结
异步任务的处理,因其重要性,在前端开发中始终是一个 不可忽视的考查点;又因其复杂性
点活多变,开发者需要熟悉各种异步方案。同时,每一种异步方案都是相辅相成的。如果你
没有完全理解callback,那也许就很难理解Promise;如果没有熟练掌握Promise, 那就更无从谈起会使用Generator和async/await.很多异步场景都涉及网络、高风险计算,但本篇还没有涉及异步中错误处理这个重要内容,这方面的信息会在后续内容中进行讲解。异步的整个学习过程需要我们从最基础的知识开始,步步为营。如果学习一次理解不了, 那就学习两次、三次。相信我,这一定是一个吃经验和重复次数的“水滴石穿”的过程。通过次次“死记硬背”,我们会慢慢理解其中的道理。