第8章 JS 异步进阶【想要进大厂,更多异步的问题等着你】

返回章节目录

目录

1.为什么要有Event Loop?

2.请描述event loop(事件循环/事件轮询)的机制,可画图

3.Promise有哪三种状态?如何变化?

Promise小试身手

4.async/await

5.async/await和Promise的关系

async function的函数

async+表达式

5.关于异步独立知识点

6.宏任务与微任务

7.Event loop 和DOM渲染

8.为什么微任务执行时机比宏任务早?

小试牛刀1(过程梳理)

小试牛刀2(字节烂大街的笔试题过程梳理)


 

点赞再看,养成好习惯,总结不易,花了断断续续一个月零散时间才总结出来,老铁多多支持~

除了视频基本内容,本篇加上了我自己的总结,所以会更多一些内容。

 

1.为什么要有Event Loop?

因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞,Event Loop这样的方案应运而生。

 

2.请描述event loop(事件循环/事件轮询)的机制,可画图

因为js是单线程运行的,所以异步要基于回调来实现,而event loop就是异步回调的实现原理

JS先把同步代码执行完再去执行异步代码,如果某一行执行报错,则停止下面代码的执行。

通过例子来讲event loop机制

console.log("Hi");

setTimeout(function cb1() {
    console.log("cb1"); // cb即callback
}, 5000);

console.log("Bye");

运行大致过程如下(本例子缺少了微任务队列,大家对比想象一下)

这个图有点遗漏,因为本代码例子不涉及微任务队列。请大家自行想象一个对比的微任务队列。显示的异步Web APIs只有宏任务,异步任务分为宏任务和微任务。

同步代码(栈里面的代码)顺序执行,遇到异步代码就记录一下,在此过程中异步代码如果是宏任务移动到Web APIs,直到定时的时间到就放入宏任务队列,即图中的Callback Queue,如果是微任务则放入微任务队列(图中没画,大家发挥一下想象力),不会经过Web APIs。如果Call Stack调用栈为空(即同步代码执行完),去查看微任务队列,任务执行完就出队列,直到微任务队列微空后,尝试DOM渲染(如果DOM结构发生变化),然后Event Loop开始工作,然后轮询查找宏任务队列Callback Queue,如有则移动到Call Stack执行... 每执行完一个宏任务,就会去检查微任务队列,若微任务队列有,就执行到微任务为空,再尝试DOM渲染,然后去看宏任务队列,继续轮询查找(永动机一样不停地重复操作)。

注意:

1.这里的Web APIs就是处理定时或者异步API的。

2.微任务是ES6语法规定的,宏任务是由浏览器规定的。

3.执行的顺序是 执行栈中的代码 => 微任务 => 宏任务(后面会展开讲)

那如果代码是如下

<button id="btn1">提交</button>
<script>
    console.log("Hi");
    $("#btn1").click(function(e) {
        console.log("button clicked");
    })
    console.log("Bye");
</script>

这个和上面的例子几乎一样,只不过回调函数放在Web APIs,点击按钮的时候回调函数就放在Callback Queue。从这里可以看出DOM事件的触发也是基于event loop的。

3.Promise有哪三种状态?如何变化?

Promise三种状态:pending、resolved、rejected

状态变化:

1.pending-->resolved(成功了)   

2.pending-->rejected(失败了)

状态变化是不可逆的

状态的表现

1.pending状态不会触发then和catch

2.resolved状态的Promise会触发后续的then回调函数

3.rejected状态的Promise会触发后续的catch回调函数

 

then和catch改变状态

then正常返回resolved的Promise对象,里面有报错则返回rejected的Promise对象

catch正常返回resolved的Promise对象,里面有报错则返回rejected的Promise对象

Promise.reject(reason)返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法catch

Promise.resolve(value)返回一个状态为成功的Promise对象,并将成功信息传递给对应方法then

const p1 = Promise.resolve().then(()=>{
    return 100;
}) //Promise.resolve()返回resolved状态的Promise对象,然后then执行完不报错,还是返回一个resolved状态的Promise
// console.log('p1', p1);

p1.then(()=>{ // 传进来p1返回的100,但是没有使用,打印123
    console.log("123");
})

const p2 = Promise.resolve().then(()=>{
    throw new Error("then error");
}) //Promise.resolve()返回resolved状态的Promise对象,然后then执行完报错了!返回一个rejected状态的Promise

// console.log('p2', p2); // rejected触发后续的catch回调
p2.then(()=>{
    console.log('456');
}).catch(err=>{
    console.error('err100', err);
}) // rejected状态的Promise后续会触发catch而不是then

运行结果

123
err100 Error: then error
    at <anonymous>

const p3 = Promise.reject("my error").catch(err=>{
    console.error(err);
})
console.log('p3', p3);
p3.then(()=>{
    console.log(100);
})

const p4 = Promise.reject('my error').catch(err => {
    throw new Error("catch err");
})

console.log('p4', p4);
p4.then(()=>{
    console.log(200);
}).catch(()=>{
    console.error("some err");
})

这题先打印p3和p4是因为他们是同步代码会先执行,后续都是微任务的代码,这个放到后面再说,后面说完再返回来看这题一目了然。

Promise小试身手

Promise.resolve().then(()=>{
    console.log(1);
}).catch(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
})

1
3

解释:Promise.resolve()返回一个resolved状态的Promise后续触发then回调,然后打印1,then执行完返回resolved状态的Promise,然后再执行then,打印3,返回返回resolved状态的Promise。因为没Error,没有rejected状态的Promise,所以不会触发catch回调。

Promise.resolve().then(()=>{
    console.log(1);
    throw new Error('erro1');
}).catch(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
})

1
2
3

解释:与上例的不同就是多了throw new Error('erro1');

Promise.resolve()返回一个resolved状态的Promise后续触发then回调,然后打印1,执行throw new Error('erro1');返回一个rejected状态的Promise,触发catch回调函数,打印2,接着返回resolved状态的Promise,触发后面then的回调,打印3,返回一个resolved状态的Promise。

Promise.resolve().then(()=>{
    console.log(1);
    throw new Error('erro1');
}).catch(()=>{
    console.log(2);
}).catch(()=>{
    console.log(3);
})

1
2

解释:这个和上题的不同就是最后一个回调是catch的,不是then的回调。

理由和上面一模一样,不用多解释,只不过打印2之后返回的是resolved状态的Promise,后面没有then,所有不打印3。catch里面只要没报错Error,那么就是resolved状态的Promise。

我个人觉得需要额外注意的点:大家不要忽略最后的返回值,返回值会链式传递给下一个回调,只不过我们这里的例子没有强调返回值,等于return undefined;如果then/catch回调函数有形参,而上一个回调函数有返回值,那么返回值会作为下一个回调的形参。

 

4.async/await

 因为是之前的异步回调会有callback hell(回调地狱)的问题,所有ES6出来了Promise,但是Promise的的then/catch也是基于回调函数,后来ES8出来了async/await,看起来用同步语法消灭了回调函数。

举上一章节的一个例子

function loadImg(src) {
    const p = 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
        }
    )
    return p
}

const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
loadImg(src1).then(img => {
    console.log(img.width)
    return img
}).then(img => {
    console.log(img.height)
}).catch(ex => console.error(ex))

// ================上面用Promise也是不断的处理回调=================
// 立即执行函数前面有个!,是为了避免和上面最后一句不写分号导致当成函数的冲突
!(async function() {
    // img1
    const img1 = await loadImg(src1);
    console.log(img1.height, img1.width);

    // img2
    const img2 = await loadImg(src2);
    console.log(img2.height, img2.width);
})();

await后面可以跟Promise对象或者async函数执行

额外提示:立即执行函数前面有个!,是为了避免和上面最后一句不写分号导致当成函数的冲突,比如下面的例子"abc"没有分号,就把它当成了一个函数,因为后面跟着"abc"(...)(),alert换行后跟着括号还是认为是函数。你会发现平时引入js文件的时候,前面可能很多都有!就是这个原因,

 

5.async/await和Promise的关系

1.执行async函数,返回的是Promise对象

2.await相当于Promise的then

3.try...catch可捕获异常,代替了Promise的catch

 我们来证明第一点执行async函数,返回的是Promise对象

async function fn1() {
    return 100;
}
const res1 = fn1(); // 执行async函数,返回的是一个Promise对象
console.log(res1);
res1.then(data => {
    console.log('data', data);
})

可以看到res1却是是一个Promise对象

我们把async函数的返回改一下,直接返回一个Promise对象试试

async function fn1() {
    return Promise.resolve(100);
}
const res1 = fn1(); // 执行async函数,返回的是一个Promise对象
console.log(res1);
res1.then(data => {
    console.log('data', data);
})

可以看到打印结果data 100不变,只是Promise当时的状态有点变化

接着看

!(async function() {
    const p1 = Promise.resolve(300);
    console.log(p1);
    const data = await p1; // 这里await后面的语句相当于Promise的then里面的回调函数
    console.log('data', data);
})()

这里可以看出await后面跟着一个Promise,而执行了这一行之后,相当触发了于Promise.then回调,拿到了300的值最后打印。

async function fn1() {
    return Promise.resolve(100);
}

!(async function() {
    const data2 = await fn1();
    console.log('data2', data2);
})()

这里需要说明一下,这里await后面跟着一个Promise对象,执行这一行相当于Promise.then回调,而且await这一行不执行完毕是不会去执行后面的语句。

!(async function() {
    const data1 = await 400; // 若后面不是Promise对象,则直接返回表达式执行结果,这里是400
    console.log('data1', data1);
})()

若await后面不是Promise对象,比如字符串、函数、数字,则直接返回该表达式执行结果,这里是400

再看一个

!(async function() {
    const p4 = Promise.reject('err1'); // rejected 状态
    try {
        const res = await p4;
        console.log(res);
    } catch (ex) {
        console.error(ex); // try...catch相当于Promise的catch
    }
})()

这里的try...catch相当于Promise的catch

接着看一个重要例子

!(async function() {
    const p4 = Promise.reject('err1');
    const res = await p4; // await后面的语句相当于then里面的回调,但是这里需要catch,这里会报错,后面不会执行
    console.log('res', res);
})()

可以看到这里并没有打印res,并没有执行后面一句console.log,如果要解决这个问题,那么就需要try...catch执行catch中的逻辑,就像上一个例子。改为如下即可

 

综上所述

如果await等待的是一个Promise对象,那么它只想等resolved状态的Promise,后续的语句相当与then回调才会执行

如果等来的是rejected状态的Promise,await接不住,必须try...catch,在catch中接住它,然后可以进行一定的自定义说明。

 

async function的函数

返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)

async+表达式

await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
await 后续跟非 Promise 对象:会直接返回

(async function () {
    const p1 = new Promise(() => {})
    await p1
    console.log('p1') // 不会执行
})()

 这里的Promise里面没有resolve(), 导致await后面的表达式得不到结果,而await后面的语句都相当于Promise的then回调,只要await这里不执行,那么后面所有的callback都不会执行,所以不会打印"p1"

 

5.关于异步独立知识点

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);
})

根据上图可以看到,等待1s后3个结果同时打印,那是因为forEach循环3次已经结束了,1s的时间其实是3次循环执行到await这里卡住了,await后面的语句相当于callback,await这里不执行完是不会执行后面的,之后3次循环的await几乎同时结束,瞬间打印出1,4,9

那么如果我想要每间隔1s打印一个结果应该怎么做呢,执行异步的循环可以用for...of

function muti(num) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(num * num);
        }, 1000)
    })
}
const nums = [1, 2, 3];

!(async function() {
    for (let i of nums) {
        const res = await muti(i);
        console.log(res);
    }
})()

 

6.宏任务与微任务

宏任务:setTimeout、setInterval、Ajax、I/OUI交互事件(比如DOM事件)

微任务:Promise回调、async/await、process.nextTick(Node独有,注册函数的优先级比Promise回调函数要高)MutaionObserver

微任务执行时机比宏任务要早(记住)

注意:script全部代码、(这个是执行栈的代码,属于同步代码),包括new Promise(function(){...})里面的代码,只有then、catch回调才是微任务

 

console.log(100);

setTimeout(()=>{
    console.log(200);
})
// 微任务
Promise.resolve().then(()=>{
    console.log(300);
})
console.log(400);

  

 

7.Event loop 和DOM渲染

JS是单线程的,而且和DOM渲染公用一个线程,JS执行的时候,得留一些时机供DOM渲染

8.为什么微任务执行时机比宏任务早?

宏任务:DOM渲染后触发,如setTimeout

微任务:DOM渲染前触发,如Promise

为什么微任务在渲染前,宏任务在渲染后?

- 微任务:ES 语法标准之内,JS 引擎来统一处理。即不用浏览器有任何干预,可一次性处理完,更快更及时。
- 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。

综上所述,代码执行顺序如下:

1.call Stack清空,即同步任务执行完(执行栈内的代码,执行完弹栈清空)

2.执行当前的微任务队列的任务

3.尝试DOM渲染(如果DOM结构有改变则重新渲染)

4.触发Event Loop,执行宏任务队列的任务

5.每执行一个宏任务会回到步骤2,检查执行微任务,依次轮询。

小试牛刀1(过程梳理)

setTimeout(() => {
    console.log('timeout1')
    Promise.resolve().then(() => {
        console.log('promise1')
    })
    Promise.resolve().then(() => {
        console.log('promise2')
    })
}, 100)

setTimeout(() => {
    console.log('timeout2')
    Promise.resolve().then(() => {
        console.log('promise3')
    })
}, 200)
  1. 先将两个setTimeout塞到宏任务队列中
  2. 当第一个setTimeout1时间到了执行的时候,首先打印timeout1,然后在微任务队列中塞入promise1promise2
  3. 当第一个setTimeout1执行完毕后,会去微任务队列检查发现有两个promise,会把两个promise按顺序执行完
  4. 尝试DOM渲染
  5. 执行下一个宏任务,两个promise执行完毕后会微任务队列中没有任务了,会去宏任务中执行下一个任务 setTimeout2
  6. setTimeout2 执行的时候,先打印一个timeout2,然后又在微任务队列中塞了一个promise3
  7. setTimeout2执行完毕后会去微任务队列检查,发现有一个promise3,会将promise3执行
  8. 会依次打印 timeout1 promise1 promise2 timeout2 promise3

注意:当setTimeout定时时间间隔一样的时候,旧版本的node可能与浏览器端的运行结果不一样。 

高手想挑战更多,请见这篇文章:Event Loop的规范和实现,这是蚂蚁金服·数据体验技术团队掘金号的一篇文章,例子讲的很细,有兴趣的同学可以去看看。

小试牛刀2(字节烂大街的笔试题过程梳理)

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function (){
    console.log('setTimeout');
}, 0)

async1();

new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
})

console.log('script end');
  1. 从上到下,先是2个函数定义
  2. 再打印一个script start
  3. 看到setTimeout,里面回调函数放入宏任务队列等待执行
  4. 接着执行async1(),打印async1 start,看到await async2(),执行后打印async2,await后面的语句相当于Promise的then回调函数,所以是微任务,console.log('async1 end')放入微任务队列
  5. 执行new Promise,直接执行这个函数,打印promise1,执行resolve(),后续触发的then回调是微任务,放入微任务队列
  6. 打印script end,同步代码执行完了
  7. 检查微任务队列,依次打印async1 endpromise2
  8. 尝试DOM渲染(如果DOM结构有变化)
  9. 检查宏任务队列,打印setTimeout
  10. 检查微任务队列为空,尝试DOM渲染,检查宏任务队列为空,执行结束

综上,打印依次为

为什么这里有返回undefined之后才会打印setTimeout,因为前面是同步代码和微任务执行完了,JS引擎工作结束,开始返回值。后面打印的setTimeout是浏览器处理的。

这就解释了经常在chrome看到有了返回值再打印后续内容的问题,这个问题一般人我不告诉他,所以你赶紧偷偷存起来哈哈。

 

 

关注、留言,我们一起学习。

 

===============Talk is cheap, show me the code================

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章