8-异步进阶

175 阅读6分钟

本文知识点:

  • event loop
  • promise 进阶
  • async/await
  • 微任务/宏任务
  • 场景题-promise then和catch的连接
  • 场景题-async/await语法
  • 场景题- promise和setTimeout的顺序
  • 场景题-外加async/await的顺序问题

问答题

1 请描述event loop(事件循环/事件轮询)的机制,可画图
2 什么是宏任务和微任务,两者有什么区别?
3 Promise有哪三种状态?如何变化?
  • 请描述event loop(事件循环/事件轮询)的机制,可画图
  • 什么是宏任务和微任务,两者有什么区别?
  • Promise有哪三种状态?如何变化?
  • 场景题-promise then和catch的连接
//第一题
Promise.resolve().then(()=>{
    console.log(1);
}).catch(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
})
// 打印 1  3

//第二题
Promise.resolve().then(()=>{
    console.log(1);
    throw new Error('error1')
}).catch(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
})
//打印 1 2 3

//第三题
Promise.resolve().then(()=>{
    console.log(1);
    throw new Error('error1')
}).catch(()=>{
    console.log(2);
}).catch(()=>{
    console.log(3);
})
//打印 1 2 
  • 场景题-async/await语法
async function fn(){
    return 100;
}
(async function(){
    const a = fn();//?? 返回一个promise 值是100
    const b = await fn(); //?? 返回值100(await 相当于promise的then)
})()

(async function(){
    console.log('start');
    const a = await 100;
    console.log('a',a);
    const b = await Promise.resolve(200);
    console.log('b',b);
    const c = await Promise.resolve(300);
    console.log('c',c);
    console.log('end');
    
})() //执行完毕,打印出哪些内容? 
//只打印 start  a:100 b:200
  • 场景题- promise和setTimeout的顺序
console.log(100)
setTimeout(()=>{
    console.log(200)
})
Promise.resolve().then(()=>{
    console.log(300)
})
console.log(400)

//100 400 300 200
  • 场景题-外加async/await的顺序问题
async function async1(){
    console.log('async1 start'); //2
    await async2();
    // await后面都作为回调内容 -- 微任务
    console.log('async1 end') //6   
}
async function async2(){
    console.log('async2') //3
}
console.log('script start') //1

setTimeout(function(){ //宏任务 
    console.log('setTimeout') //8
},0)

async1();

//**** 初始化promise时,传入的回调函数会立刻被执行
new Promise(function(resolve){
    console.log('promise1') //4
    resolve()
}).then(function(){ //微任务
    console.log('promise2') //7
})

console.log('script end')//5

//同步代码执行完毕(event loop - call stack 被清空)
//执行微任务
//(尝试触发DOM渲染)
//触发Event loop,执行宏任务

event loop(事件循环/事件轮询)

  • JS是单线程运行的

  • 异步要基于回调来实现

  • event loop就是异步回调的实现原理

  • 回顾:JS如何执行?

    • 从前到后,一行一行执行
    • 如果某一行执行报错,则停止下面代码的执行
    • 先把同步代码执行完,再执行异步

示例

console.log('Hi')
setTimeout(function cb(){
    console.log('cb1')
},5000)
console.log('Bye')

event loop过程

  • 同步代码,一行一行放在Call Stack执行
  • 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
  • 时机到了,就移动到Callback Queue
  • 如Call Stack为空(即同步代码执行完)Event Loop开始工作
  • 轮询查找Callback Queue,如有则移动到Call Stack执行
  • 然后继续轮询查找(永动机一样)

image.png

DOM事件和event loop

  • JS是单线程的
  • 异步(setTimeout,ajax等)使用回调,基于event loop
  • DOM事件也使用回调,基于event loop
console.log('Hi')
setTimeout(function cb(){
    console.log('cb1')
},5000)
console.log('Bye')
<button id="btn1">提交</button>
<script>
console.log('Hi')
$('#btn1').click(function(e){
    console.log('button clicked')
})
console.log('Bye')
</script>

宏任务macroTask和微任务micro Task

  • 什么是宏任务,什么是微任务
  • event loop和DOM渲染
  • 微任务和宏任务的区别

代码演示

console.log(100)
setTimeout(()=>{
    console.log(200)
})
Promise.resolve().then(()=>{
    console.log(300)
})
console.log(400)
//答案 100 400 300 200 

宏任务和微任务

  • 宏任务:setTimeout,setInterval,Ajax,DOM事件
  • 微任务:Promise async/await
  • 微任务执行时机比宏任务要早

event loop和DOM渲染

image.png

const $p1 = $('<p>一段文字</p>');
const $p2 = $('<p>一段文字</p>');
const $p3 = $('<p>一段文字</p>');
$('#container')
    .append($p1)
    .append($p2)
    .append($p3)
console.log('length',$('#container').children().length) //3
alert('本次 call stack结束,DOM结构已更新,但尚未触发渲染')
//(alert会阻断js执行,也会阻断DOM渲染,便于查看效果)
const $p1 = $('<p>一段文字</p>');
const $p2 = $('<p>一段文字</p>');
const $p3 = $('<p>一段文字</p>');
$('#container')
    .append($p1)
    .append($p2)
    .append($p3)
    
//微任务:DOM渲染前触发
Promise.resolve().then(()=>{
    console.log('length1',
        $('#container').children().length)//3
        alert('Promise then')//DOM 渲染了吗?---//NO
})

//宏任务:DOM渲染后触发
setTimeout(()=>{
    console.log('length2',
        $('#container').children().length)//3
        alert('setTimeout') //DOM渲染了吗? ---YES
})

从event loop解释,为何微任务比宏任务更早,会在渲染前触发

  • 宏任务是由浏览器规定的:setTimeout,setInterval,Ajax,DOM事件
  • 微任务是ES6语法规定的:Promise async/await

image.png

微任务和宏任务-总结

  • 宏任务有哪些?微任务有哪些?为什么微任务触发时机更早
  • 微任务、宏任务和DOM渲染的关系
  • 微任务、宏任务和DOM渲染,在event loop的过程
  • 描述event loop机制(可画图)
    • 自行回顾DOM渲染的关系

    • 微任务和宏任务在event loop过程中的不同处理

Promise

  • 三种状态
  • 状态的表现和变化
  • then 和 catch对状态的影响
//图片加载
function loadImg(src){
    return new Promise((resolve,reject)=>{
        const img = document.createElement('img');
        img.onload = () =>{
            resolve(img)
        }
        img.onerror = () =>{
            const err = new Error(`图片加载失败 ${src}`)
            reject(err)
        }
        img.src = src
    })
}

const url = 'https://img.xxx';
loadImg(url).then(img =>{
    console.log(img.width);
    return img;
}).then(img=>{
    console.log(img.height)
}).catch(err=>console.error(err))

//解决回调地狱的问题:
const url1 = 'https://img1.xxx';
const url2 = 'https://img2.xxx';

loadImg(url1).then(img1 =>{
    console.log(img.width);
    return img1;
}).then(img1=>{
    console.log(img1.height)
    return loadImg(url2);
}).then(img2=>{
    console.log(img2.width)
    return img2;
}).then(img2=>{
    console.log(img2.height)
}).catch(err=>console.error(err))

三种状态

  • pending(阻塞) resolved(成功) rejected(失败)

  • pending --> resolved 或 pending --> rejected

  • 变化不可逆

  • 状态的表现

    • pending状态,不会触发then和catch
    • resolved状态,会触发后续的then回调函数
    • rejected状态,会触发后续的catch回调函数
const p1 = new Promise((resolve,reject)=>{
    
})
console.log('p1',p1) //pending

const p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve()
    })
})
console.log('p2',p2)//pending
setTimeout(()=>console.log('p2-setTimeout',p2))//resolved


const p3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve()
    })
})
console.log('p3',p3)//pending
setTimeout(()=>console.log('p3-setTimeout',p3))//reject

then和catch改变状态

  • then正常返回resolve,里面有报错则返回rejected
  • catch正常返回resolve,里面有报错则返回rejected
const p1 = Promise.resolve().then(()=>{
    return 100
})
console.log('p1',p1) //resolved触发后续then回调
p1.then(()=>{
    console.log('124')
})

const p2 = Promise.resolve().then(()=>{
    throw new Error('then error')
})
console.log('p2',p2) //rejected触发后续catch回调
p2.then(()=>{
    console.log('456')
}).catch(err=>{
    console.log('err100',err)
})

image.png

const p3 = Promise.reject('my error').catch(err => {
    console.error(err)
})
console.log('p3',p3) //resolved  注意:触发then回调
p3.then(()=>{
    console.log(100)
})

const p4 = Promise.reject('my error').catch(err =>{
    throw new Error('catch err')
})
console.log('p4',p4)  //rejected  触发catch回调
p4.then(()=>{
    console.log(200)
}).catch(()=>{
    console.log('some err')
})

打印结果如下:

image.png

async/await

  • 异步回调callback hell
  • Promise then catch链式调用,但也是基于回调函数
  • async/await是同步语法,彻底消灭回调函数
//图片加载
function loadImg(src){
    return new Promise((resolve,reject)=>{
        const img = document.createElement('img');
        img.onload = () =>{
            resolve(img)
        }
        img.onerror = () =>{
            const err = new Error(`图片加载失败 ${src}`)
            reject(err)
        }
        img.src = src
    })
}

const url = 'https://img.xxx';
loadImg(url).then(img =>{
    console.log(img.width);
    return img;
}).then(img=>{
    console.log(img.height)
}).catch(err=>console.error(err))

//解决回调地狱的问题:
const url1 = 'https://img1.xxx';
const url2 = 'https://img2.xxx';

定义一个匿名函数,await要用async包裹执行
(async function(){
    const img1 = await loadImg(url1)
    console.log(img1.width)
    
    const img2 = await loadImg(url2)
    console.log(img2.width)
    
})()

//相当于下面的函数

loadImg(url1).then(img1 =>{
    console.log(img.width);
    return img2;
}).then(img2=>{
    console.log(img2.width)
})
定义一个匿名函数,await要用async包裹执行
(async function(){
    const img1 = await loadImg(url1)
    console.log(img1.width)
    
    const img2 = await loadImg(url2)
    console.log(img2.width)
    
})()

上面写法可以用下面的写法,意在举例await里面可以调用新的async await函数
async function loadImg1(url1){
    const img1 = await loadImg(url1)
    return img1
}
async function loadImg2(url2){
    const img2 = await loadImg(url2)
    return img2
}
(async function(){
    const img1 = await loadImg1(url1)
    console.log(img1.width)
    
    const img2 = await loadImg2(url2)
    console.log(img2.width)
})()

async/await和Promise 的关系

  • async/await是消灭异步回调的终极武器,但和Promise并不互斥,反而两者相辅相成
  • 执行async函数,返回的是Promise对象
  • await相当于Promise的then
  • try...catch可捕获异常,代替了Promise的catch
async function fn1(){
    //return 100;//return 两句意义相同
    return Promise.resolve(100)
}
const res1 = fn1() //执行async函数,返回的是一个Promise对象
console.log('res1',res1)  //Promise对象
res1.then(data=>{
    console.log('data',data) //100
})

image.png

await相当于Promise.then

!(async function(){//!只是隔绝前面的语句,避免上一句没有';'等出错
    const p1 = Promise.resolve(300)
    const data = await p1 //相当于Promise.then
    console.log('data',data)
})()

!(async function(){
    const data1 = 222 //相当于Promise.resolve(222)
    console.log('data1',data1)
})()


async function fn1(){
    //return 100;//return 两句意义相同
    return Promise.resolve(100)
}
!(async function(){
    const data2 = await fn1()
    console.log('data2',data2)
})()

image.png

使用try...catch

!(async function(){
    const p4 = Promise.reject('err1') 
    try{
        console.log('intry 1')
        const res = await p4
        console.log(res)
    }catch(err){
        console.log('p4err',err)// try...catch 相当于promise catch
    }
})()

!(async function(){
    const p5 = Promise.resolve('hello') 
    try{
        console.log('intry 2')
        const res = await p5
        console.log(res)
    }catch(err){
        console.log('p5err',err)// try...catch 相当于promise catch
    }
})()

打印:
intry 1
intry 2
p4err err1
hello
  • 为什么要async await应该用try..catch?
    • 因为await相当于Promise的then,处理不了catch
    • try..cat可捕获异常,代替了Promise的catch

如下代码

!(async function(){
    const p4 = Promise.reject('err1') //reject 状态
    const res = await p4 //await -> then
    console.log('res',res)
})()
//此代码无法捕获异常

image.png

异步的本质

  • async/await 是消灭异步回调的终极武器
  • JS还是单线程,还得是有异步,还得是基于event loop
  • async/await 只是一个语法糖,但这颗糖真香!
async function async1(){
    console.log('async1 start') //2
    await async2()  
    //await的后面,都可以看做是callback里的内容,即异步
    console.log('async1 end')//5
}
async function async2(){
    console.log('async2')//3
}
console.log('script start') //1
async1()
console.log('script end')//4
//同步代码完则执行event loop

for...of

  • for...in(以及forEach for)是常规的同步遍历
  • for...of常用于异步的遍历
function muti(num){
    return new Promise(resolve => {
        setTimeout(()=>{
            resolve(num * num)
        },1000)
    })
}
const nums = [1,2,3]

nums.forEach(async(i) => {
    const res =await muti[i]
    console.log(res)
})
//for...in同步的循环, 1秒后打印所有结果

!(async function(){
    for( let i of nums){
        const res = await muti(i);
        console.log(res)
    }
})()
//for...of异步的循环, 每一秒打印一个结果