细说异步那些事

前言

最近面试也是遇到了很多关于异步的题目,于是自己也是整理了相关的知识点帮助自己理解。本着将输入转为输出的原则,在归纳总结的同时,也希望能帮助到一些对此感到困惑的开发人员。

面试题

先放个据说是今日头条的面试题压压惊,之后再根据整理的知识点慢慢分析。

  // 异步面试题
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'); 

这段代码是对JS异步的综合性考察,如果你不对event loop,宏任务/微任务等知识烂熟于心,你未必能写出这个题的正确答案。至少我看到已经晕过去了。没关系,我们由浅入深,仔细唠嗑唠嗑在代码执行过程中,他们究竟怎么划分先后顺序。

JS是如何执行的

首先大家都应该知道JS是单线程的,通俗来说就是,一次只做一件事。那么当事件遇见了例如setTimeout这样的等待事件时,不可能真的去等它的计时器计完然后再往下执行,这样太没效率了。这就涉及到了异步。
例如:

console.log('1');
setTimeout(()=>{
    console.log('2')
}, 0);
console.log('3');
// 1
// 3
// 2

这里的执行结果就是1,3,2。我们发现,这个延时即使设置为0,它也是先执行了3,再去执行2,也就是说无论如何,JS都是先执行同步代码,再执行异步代码。那么除了上文中的setTimeout还有哪些是会触发异步操作的代码呢?有以下几个:setInterval、Ajax、promise和async/await,如果还有求补充!
那么问题来了,异步是怎么实现的?异步是基于回调实现的,而event loop是实现异步回调的基本原理。

event loop

上文提到JS是单线程的,所以这就意味着它的代码就是要一步步去执行的,我们将其称为主线程。当主线程遇到异步操作时,我们会将异步任务暂存在任务队列中,然后继续执行主线程。当主线程执行完毕,通过event loop去请求任务队列,这时候再执行异步的代码。
event loop也叫事件轮询机制,其实就是一个主线程不断请求回调任务队列的一个机制。一旦查询到
有可以执行的异步任务,就会将任务推到调用栈并执行。
值得一提的是,虽然Dom操作不属于异步,但是它也是基于event loop实现的回调。

宏任务和微任务

在讲宏任务和微任务之前,我们来看看一个例子。

console.log('1');
setTimeout(()=>{
    console.log('2')
}, 0);
Promise.resolve().then(()=> {
    console.log('3')
})
console.log('4');
// 打印顺序为 1,4,3,2

Interesting!按理说异步也会按照先后的顺序执行,但上面的例子中我们可以看到3比2先打印了,也就是执行过程中,先执行了promise再执行setTimeout。那么为什么会这样呢?这就引出了微任务和宏任务的概念,以及他们的执行顺序。
这里可以先说几个结论

  • 宏任务包括:Ajax、Dom操作、setTimeout/setInterval
  • 微任务包括:promise、async/await
  • 微任务的执行时机要比宏任务早
    前两点记住就好。我详细讲讲第三点的原因,还是通过代码演示一下。


    
        
    
    
        
const $tag = $('

1

'); $('#container').append($tag); console.log($('#container').children().length) // 同步代码添加dom节点 // 利用alert 的阻断机制,来判断是否渲染 Promise.resolve().then(()=> { console.log('执行微任务时的length', $('#container').children().length) // 1 alert('promise') }); setTimeout(() => { console.log('执行宏任务时的length', $('#container').children().length) // 1 alert('settimeout') }, 0);

运行之后不难发现,在触发promise时,虽然同步代码中已经为container新增了新的节点,但是dom仍然没有渲染。


image

在点击确定之后,也就该执行setTimeout的时候则已经渲染成功。


image

综上所述,微任务和宏任务的根本区别在于执行的时机,微任务执行于dom渲染前,宏任务反之。

再顾面试题

回到文章开头的那个面试题。在梳理了这些知识点之后,突然发现豁然开朗,曙光就在眼前!
我大致的写下思路。首先,定义的async函数时是不执行的,按照先执行同步代码,再执行异步代码的原则,第一个打印出来的是script start,接着到了settimeout,它是一个宏任务,先放着,接着执行async1()函数,接着打印async1 start,接着触发async2(),然后打印async2,由于await的下一句是相当于promise中的then,所以实际上它属于一个异步回调,这里并不会执行async1 end,而是先暂存于任务队列中,而初始化 Promise时相当于是一个同步代码,它直接执行了第一个函数参数,也就是打印了promise1,这时的then里面的内容也是一个异步,我们将之存入任务队列,此时同步代码执行完毕,开始执行异步的部分。
异步第一个任务则是打印async1 end,接着打印promise then中的promise2,最后再执行宏任务,setTimeout
所以打印顺序为,script start、async1 start、async2、promise1、async1 end、promise2、setTimeout

总结

JS执行的步骤如下
1、先一行一行执行同步代码
2、遇到异步,先暂存于任务队列
3、当同步执行完毕,则开始执行任务队列中的任务
4、执行微任务
5、dom渲染
6、执行宏任务
只要记住这个步骤,相信任何关于异步的代码题都可以得心应手的写出来。
Ps. 这是我第一次写技术文章,很有意思,本来看了其他人的文章之后感觉自己已经理解了,但实际去写的时候发现也是个头脑风暴的过程,比如思考文章的结构,比如该怎么去讲述才更容易理解等等。这才体会到从输入到输出真的挺难的,如果哪里有错误,请大佬们指正,欢迎讨论!
码字不易,如需转载,请联系在下!

你可能感兴趣的:(细说异步那些事)