一文详解事件循环,同步任务与异步任务

1,248 阅读6分钟

前言

首先,在学习js中同步异步的问题前,需要明白,js是单线程的,为什么它得是单线程的呢?这得从它的使用场景来看了,它主要是用来让用户与页面进行交互的吧。那么假设js是多线程的,在这个线程里面,用户点击某个按钮会增加一个DOM节点,在另一个线程里面,用户点击这个按钮又会删除一个DOM节点,那么此时js就不知道该听谁的了。那同步异步的出现又是为了什么呢?假设没有异步,那么我们在向服务器请求数据时,可能会因为网络不好,卡了半天,这时候因为是同步的,所以网页必须等待数据请求回来才能继续和用户交互,这样会导致整个网页很奇怪的卡住了,用户体验很不好。

执行栈与任务队列

执行栈

  • 姑且先不说执行栈是什么,先说栈是什么吧。栈就好似一个桶,最先放入的东西肯定是最后拿出来的,就是大家常说的先进后出。

那么执行栈就是把图中的内容块变成代码任务,光说肯定说不明白,还是得上代码:

function fn (count) {
            if (count <= 0) return
            fn(count - 1)
            console.log(count)
        }
fn(3)

这是一段很简单的递归代码,这里我们直接上图解释(这里其实画的不严谨,栈底应该是全局执行上下文):

js中所有的任务都会在主线程上执行然后形成一个执行栈。(请记住这一点!!!)

任务队列

那么队列和栈则是相反的,队列是先进先出的。其实很好理解,和我们平时排队是一个道理,先进入队伍的人肯定先出来。那么任务队列通俗理解就是用来放置异步任务的回调函数的。(也请记住这一点!!!)

同步任务与异步任务

先上一点概念性的东西,打个基础:

同步任务

很多人在理解同步任务的时候会被它的语义搞混,其实同步任务不是同步一起执行的。它是要等待上一个执行任务结束才可以执行下一个任务,这里说起来也并不晦涩难懂,但是还是写段简单的代码解释一下:

console.log(1)
console.log(2)
console.log(3)

代码很简单吧,很明显输出结果是1,2,3,这就是同步代码,那么我们就可以总结了,同步任务就是在主线程上面排队,然后一个一个进入执行栈执行,直到执行栈为空。

异步任务

还是直接举个栗子:

console.log(1)
setTimeout(() => {
    console.log(2)
}, 1000)
console.log(3)

这段代码的输出和上面同步代码的输出不一样,它的输出顺序是1,3,2,这就是异步代码,它并不会按照执行顺序去执行,同样我们用官方话语总结一下:异步任务指的是,不进入主线程、而进入"任务队列"(Event queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 如果不理解没有关系,后面说到了事件循环就会醍醐灌顶了。

js的执行机制

先上比较晦涩难懂的概念:

  • 1. 同步任务由JavaScript 主线程按顺序执行。
  • 2. 异步任务委托给宿主环境执行。
  • 3. 异步任务完成后,对应的回调函数会被加入到任务队列中等待执行,任务队列又被分为宏任务队列和微任务队列,优先执行微任务队列,常见的微任务有new Promise().then,常见的宏任务有定时器
  • 4. JavaScript 主线程的执行栈被清空后,会读取任务队列中的回调函数,次序执行。
  • 5. JavaScript 主线程不断重复上面的第4 步,在执行回调函数时又会按照上面的四步去执行。

js一直从任务队列中取回调函数,然后放入主线程中执行,这是一个循环不断的过程,所以把它叫做事件循环。

!!上代码练习!!

这个还是要简单粗暴的来段代码会更直观一点:

const promise = new Promise((resolve, reject) => {
      console.log(1);
      setTimeout(() => {
          console.log("timerStart");
          resolve("success");
          console.log("timerEnd");
       }, 0);
      console.log(2);
  });
  promise.then((res) => {
      console.log(res);
  });
  console.log(4);

现在我们根据上面的规则一步一步分析这段代码,如果不懂Promise也没有关系,我保证这并不影响你对事件循环的理解。现在你就把自己当成js代码的检察官,要正确把它们放在合适的“位置”

  • 检察官的第一步就是判断哪些是同步代码,哪些是异步代码,OK,首先从上往下看,Promise本身是同步的,所以它应该在主线程上排队,然后继续看pomise.then是个异步任务,并且是属于微任务的,它的回调函数应该在微任务队列中(此时还不在),最后一句输出语句是同步代码,应该在主线程上排队。
  • 第二步,执行主线程上的同步代码,首先有Promise排着队呢,所以先输出1,随后有个定时器,所以应该把它挂起执行,由于它没有时间延迟,所以回调函数直接被放入宏任务队列,继续执行代码,遇到打印,直接输出2。现在主线程还有其他的同步代码不?是不是还有一个输出语句,所以输出4,现在主线程上的同步代码执行完了
  • 第三步读取任务队列,由于微任务队列上没有东西(Promise的状态并没有改变,不会执行promise.then()),所以读取宏任务队列上的回调函数,回调函数进入主线程执行,首先输出timerStart,然后promise状态发生改变,然后又遇到一个输出语句,输出timerEnd。现在主线程上又没有东西了,又得去看任务队列上有没有东西了
  • 第四步,由于promise状态发生改变了,所以微任务队列上有回调函数了,执行输出语句,res为success,输出success

一道题怎么够呢,再来一道,保证如果你做完这道,百分之八十的事件循环题目都不在话下

setTimeout((a) => {
    console.log(0);
});
new Promise(resolve => {//P1
    console.log(1);
    setTimeout((b) => {
        resolve();
        var p1=new Promise((n1,n2)=>{n1(20)})
        p1.then(() => console.log(2));
        console.log(3);
    });
    new Promise((n1,n2)=>{n1(20)}).then(() => console.log(4));//P2
}).then(() => {
    console.log(5);
    var p2=new Promise((n1,n2)=>{n1(20)})
    p2.then(() => console.log(8));
    setTimeout((c) => console.log(6));
});
console.log(7);

为了好辨认,我在定时器回调函数里面添加一些参数,但是这些参数并没有什么用,还有在Promise后面加了注释,防止大家晕掉,那么接下来就开始分析了。

1.首先遇到定时器a,并且没有时间限制所以入队(宏任务队列),然后遇到P1,P1本身是同步代码,所以输出1,又遇到定时器b,一样没有时间限制,入队(宏队列),然后又遇到P2,执行内部代码,由于P2状态发生改变了,所以P2.then进入微任务队列,随后又遇到输出语句7,所以输出7

2.现在第同步代码执行完毕,执行栈为空,主线程去找任务队列要东西了,首先找微任务队列,微任务队列里面现在只有一个P2.then,所以执行输出4,随后微任务队列执行完毕,执行宏任务队列,宏任务队列里面首先执行定时器a,输出0,随后执行定时器b,此时P1的状态发生改变,P1.then进入微任务队列,继续执行定时器b,遇到p1,执行内部代码,p1的状态发生改变,同样p1.then进入微任务队列,然后遇到输出语句,输出3,现在宏任务队列为空,第二轮结束了

3.执行栈又为空了,现在主线程又去找任务队列要东西了,微任务队列里有东西,是P1.then,所以执行P1.then,遇到输出语句,输出5,然后执行p2内部代码,p2状态发生改变,所以p2.then进入微任务队列,随后遇到定时器c,同样没有时间限制,所以回调函数进入宏任务队列,现在P1的回调函数执行完了,微任务队列里还有一个p1的回调函数,所以执行p1.then,输出2,此时微任务队列里还有没有东西?现在不要晕,刚刚我们执行P1.then的时候是不是又有一个p2.then进入微任务队列了,所以继续执行p2.then,输出8,现在微任务队列才为空,OK,现在去找宏任务队列,只有一个定时器c,输出6,分析完毕!!

--------------------------------------你学费了吗--------------------------------

结语

由于我也是初学者,如果理解有误,欢迎指正我哟