img

一分钟让你悟到什么是同步/异步、微任务/宏任务、Promise、async/await

2022-10-16 0条评论 1.6k次阅读 JavaScript


JavaScript是一门单线程的语言

JavaScript是一门单线程的语言,因此,JavaScript在同一个时间只能做一件事,单线程意味着,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务。(是的,你就当作在做排队核酸)

为什么是单线程

其实,JavaScript的单线程,与它的用途是有很大关系,我们都知道,JavaScript作为浏览器的脚本语言,主要用来实现与用户的交互,利用JavaScript,我们可以实现对DOM的各种各样的操作,如果JavaScript是多线程的话,一个线程在一个DOM节点中增加内容,另一个线程要删除这个DOM节点,那么这个DOM节点究竟是要增加内容还是删除呢?这会带来很复杂的同步问题,因此,JavaScript是单线程的

同步任务和异步任务是什么

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有等主线程任务执行完毕,”任务队列”开始通知主线程,请求执行任务,该任务才会进入主线程执行

用官方一点的话来讲,当一个js被执行的时候,把同步代码全放到一个执行栈里面,异步代码放到一个任务队列中,当栈执行完毕后开始执行任务队列

    function add(){
        console.log(2)
    }
    function add3(){
        console.log(5)
    }
    function add2(){
        console.log(4)
    }
    console.log(1)
    setTimeout(add,5000);
    console.log(3)
    setInterval(add2,1000)
    setTimeout(add3,1000)

image.png

首先,同步的代码是console.log(1)和log(3),所以先输出了1,3,然后就是异步的地方了。

同步任务

同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。通俗来讲:就是上一个捅完核酸下一个才继续

异步任务

异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。通俗来讲:就是上一个捅完核酸,到下一个的时候发现没网健康码半天转不出来了,那只能让后面有健康码的人先让大白捅了(自己到一边等网来吧)

function fun1() {
  console.log(1);
}
function fun2() {
  console.log(2);
}
function fun3() {
  console.log(3);
}
fun1();
setTimeout(function(){
  fun2();
},0);
fun3();

// 输出
1
3
2

有了异步,就算fun2()里面是文件的读取或ajax这种需要耗时的任务,也不怕fun3()要等到fun2()执行完才能执行啦

异步机制:JavaScript中的异步是怎么实现的呢?

那么,JavaScript中的异步是怎么实现的呢?那要需要说下回调和事件循环这两个概念啦

首先要先说下任务队列,我们在前面也介绍了,异步任务是不会进入主线程,而是会先进入任务队列,任务队列其实是一个先进先出的数据结构,也是一个事件队列,比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO完成后,就会在任务队列中添加一个事件,表示异步任务完成啦,可以进入执行栈啦~但是这时候呀,主线程不一定有空,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务啦

当执行异步任务的时候,异步任务中可能也包含同样的同步任务与异步任务,顺序还是一样的,就算任务队列中没有任务也会去执行,这个过程会无限循环下去,因此也叫作事件循环

异步编程:怎么才能实现异步编程,写出性能更好的代码呢

那么,怎么才能实现异步编程,写出性能更好的代码呢,下面有几种方式

1、回调函数

回调函数我们在使用ajax时应该用的很多啦,其实在使用ajax时,我们就用到了异步

var req = new XMLHttpRequest();
req.open("GET",url);
req.send(null);
req.onreadystatechange=function(){}

2、Promise

Promise 存在的意义是异步问题同步化解决方案

我们都知道Promise()执行是同步的,而then()执行是异步的,为什么?
为什么Promise()执行是同步的,而 then()执行是异步的呢?
让我们用代码来看看吧:

// 异步程序
const data = $.ajax({
    url: "http://localhost:3000/data.json",
    async: false // 同步
});
console.log(data.responseJSON.map(item=>item.name));
console.log("My name is LPieces.");

image.png

当我设置了async: false上面的ajax和下面的打印就形成了同步的关系。虽然实现了我想要的结果,但是打印的My name is LPieces跑到下面去了,它要等着上面ajax执行完才打印出来,那这就不对了哦,这会阻塞了下面所有的代码。既然这样,来看看Promise怎么帮我解决这个问题:

const p = new Promise((resolve, reject) => {
    $.ajax({
        url: 'http://localhost:3000/data.json',
        success (data) {
            resolve(data);
        }
    })
})
p.then(res => {
    console.log(res.map(item=>item.name));
})
console.log("My name is LPieces.");

运行结果:

image.png

是不是完美的解决了阻塞的问题呀,到这里应该能理解为什么Promise()的执行一定是同步的,而 then()的执行一定是异步的了吧,如果then()是同步执行的话,那么就会回到上面阻塞的问题去了,那我设置async: false不香吗?干嘛还要写个Promise包裹ajax的代码呢?

使用 Promise 完成一个异步

function wait(time) {
    return new Promise(function(resolve,reject) {
        setTimeout(resolve,time);
    });
}

这个时候我们就可以使用Promise处理异步任务啦

wait(1000).then(function(){
    console.log(1);
})

异步任务又分为宏任务和微任务

宏任务:普通任务,正常执行。正常的异步任务都是宏任务,最常见的就是主代码块、定时器(setInterval, setImmediate, setTimeout)、IO任务

微任务:优先于宏任务执行(但不会抢断)。微任务出现比较晚,queueMicrotask、Promise和async属于微任务(当然,async就是promise)

我们可以看出来,微任务的优先级比宏任务高,一个任务结束后,事件循环会找到并执行全部微任务,然后再查找其他任务,那么,怎么分别宏任务和微任务呢?

来看个例子

console.log('aaa');

setTimeout(() => console.log(111), 0); //异步任务
queueMicrotask(() => console.log(222)); //异步任务

console.log('bbb');
输出的结果是:
aaa
bbb
222
111

执行顺序就像我们之前说的,先执行同步的,也就是aaa,bbb,然后在执行异步的,但是这里,setTimeout

在queueMicrotask前面,却还是先执行了queueMicrotask,就是因为queueMicrotask是微任务,而setTimeout是宏任务,也就是说在异步中,会先完成所有的微任务,然后再去执行宏任务

为什么会有宏任务与微任务

既然是异步任务,我们当然要选择谁执行第一步谁执行第二步了。按照官方的设想就是,任务之间的不平等。有些任务对于用户而言更加重要,需要先执行,有些任务(类似定时器)晚点执行也没有什么问题

宏任务和微任务相当于一种优先级队列的方式

Promise 的奥特人间体:async/await

  1. 这两个玩意就是要基于Promise实现的,它不能用于普通的回调函数。它是解决回调地狱最好的方法【Promise + async awiat的结合】
  2. sync/await使得异步代码看起来像同步代码,这正是它的魔力所在。

一个函数如果加上 async ,那么该函数就会返回一个Promise

async function async1() {
  return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}

总结

说了这么多其实就是因为js是单线程就有了同步异步,而异步进入任务队列又分为了宏任务和微任务,而微任务里面出了个Promise这玩意来搞同步(你可以当成有5个人健康码转不出来了,这时来了一个热心志愿者组织他们排好队,谁先转好谁再先来😂)

image.png

参考资料:
https://juejin.cn/post/7018011655665614884
https://juejin.cn/post/6962312899960242213
https://blog.csdn.net/qq_51649346/article/details/123953823
https://blog.csdn.net/weixin_43837268/article/details/91386754

🏷️ #面试

💬 COMMENT


🦄 支持markdown语法

👋友