众所周知啊,JS是一个单线程的脚本语言。

那么为什么JS一定要是一个单线程的呢?如果像多线程那样效率是不是会有很大提升呢? 答案当然是否定的,JS为浏览器而生,用户必然要和浏览器存在很多的交互,会在这交互的过程中操作DOM。如果是多线程的话,一个线程要去添加,一个线程要去删除,那么肯定就会“撞车”。

so,对于浏览器来说,单线程一定是比多线程严谨的,那么这样就会有一个问题产生,如果一个操作需要大量的请求时间或者代码进行着大量的计算,我们的页面就会卡死,因为之前的代码根本没有执行结束,不会跑到后面的代码

JS中的同步、异步

所以,如果JS通篇都是同步的话,对于开发者和体验者来说都是一场噩梦

于是就有了异步任务,好的同学,我这就跟你说说异步事件。

  • 执行同步任务,遇到异步任务会推入到事件列表,同步开始执行下一个任务。
  • 异步任务执行事件列表,并注册回调函数,推入到事件队列
  • 同步任务完成时查看事件队列,有的话拿出来到执行
  • 这样就形成了JS中相当重要的事件循环Event Loop

本着简单粗暴的原则,我们打一个比喻。

在一个愉快的周六中午,我准备喝水吃饭写代码,我接了一杯水,我发现我不会做饭,也不想出去吃,因为午饭的问题就会耽误我写代码。这时候我点了一份外卖,把我的午饭问题交给其他人解决,这样我就可以开开心心的写代码。写完代码了可以出门查看一下外卖是否到了,之后就可以开始吃饭了。因为我没有办法一次性同时完成两件事情,所以我订了外卖,将吃饭这个“任务暂时挂起,等待一个送达的通知,送达之后我再去完成吃饭。

宏任务、微任务

先来一道简单的面试题

console.log('我是script开始!')

setTimeout(() => {
    console.log('我是setTimeout!')
}, 0)

new Promise(()=>{
    console.log('我是Promise!')
    resolve()
}).then(()=>{
    console.log('我是Promise.then!')
})
console.log('我是script结束!')

复制代码

有些同学一看,我会啊,JS我熟!

我是script开始!
我是setTimeout!
我是Promise!
我是Promise.then!
我是script结束!
复制代码

好好好,同学你先坐下。

image.png

什么是宏任务、微任务

  • 宏任务:scriptsetTimeoutsetIntervalI/O
  • 微任务:Process.nextTickPromiseMutationObserve

宏任务和微任务都属于异步任务,他们都属于在一个队列中的,他们的不同主要是来自于他们的执行顺序执行顺序。

先来张图!(摘自掘金社区) image.png

看了这张清晰明了的图片我们再来解释一下上面的代码

console.log('我是script开始!')

setTimeout(() => {
    console.log('我是setTimeout!')
}, 0)

new Promise((resolve)=>{
    console.log('我是Promise!')
    resolve()
}).then(()=>{
    console.log('我是Promise.then!')
})
console.log('我是script结束!')

复制代码

一段代码中在所有微任务执行完毕之前是不会去执行宏任务的!!!

先执行script中的同步的任务,console.logPromise。遇到setTimeout将它推到事件列表中同步代码执行结束后查看异步代码执行结束了没有(.then),.then为微任务,到此微任务执行完毕开始执行下一个宏任务setTimeout

所以,最终打印的结果为

image.png

async/await

说到了Promise,在这里我就先简单介绍一下async/await,之后我会出一片文章专门写一下es6~es12常用的新特性。

async/await本质上是Generator的语法糖,它所返回的正是一个Promise对象。

await必须在async function的内部中使用!在遇到await的时候代码会暂停执行,在异步操作完成后才会继续执行。

好的,那我们加入async/await来看一道题(之前学习时摘自其他博主)

        console.log('script start');

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

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

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

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

        console.log('script end');
复制代码

就问你凌乱了没!!!哈哈哈哈哈!!!其实第一次看到我也凌乱

这里就需要对同步、异步、宏任务、微任务的执行顺序有一个清楚的认知。

  • 1.首先执行同步代码,script属于宏任务。
  • 2.所有同步代码执行完毕,去查看是否有异步代码需要执行。
  • 3.如果有的话执行异步代码中的微任务
  • 4.所以微任务执行后,至此第一轮Event Loop就结束了。(之后就是渲染)
  • 5.开始新的一轮Event Loop,开始执行异步任务中的宏任务

这样答案一下子就出来啦

image.png

如果你的答案是这样的,那你就上当了,这里面存在着一个误区!

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

        async function async2() {
            console.log('async2 end');
        }
        async1();
复制代码

有的同学会问 async2()执行结束不应该继续走console.log('async1 end')了吗? 这里就是一个误区,上面讲到async/await其实返回的是一个Promise。将await之后的代码提升一下线程先执行。

那我们把await转换成Promise来看

        console.log('script start');

        async function async1() {
            new Promise(res=>{
                async2()
                res()
            }).then(()=>{
                console.log('async1 end');
            })
        }

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

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

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

        console.log('script end');
复制代码

这样就无比清晰了!!!

最终的打印结果为:

image.png