一次弄懂event loop

Event Loop 是 JavaScript 异步编程的核心思想,也是前端进阶必须跨越的一关。同时,它又是面试的必考点,特别是在 Promise 出现以后,各类各样的面试题层出不穷,花样百出。这篇文章从现实生活中的例子入手,让你完全理解 Event Loop 的原理和机制,并能游刃有余的解决此类面试题。

先来一道面试题镇楼html

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提及

首先明确一点,js是一门单线程语言。也就是说同一时间只能作一件事。
JavaScript为何是单线程?与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?java

因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。
但单线程容易引发阻塞,好比:面试

alert(1);
console.log(2);
console.log(3);
console.log(4);

alert弹框只要不点击肯定那就永远不会打印出2,3,4。
为了防止主线程堵塞,javaScript有了同步和异步的概念。编程

接下来咱们说说同步、异步

同步:同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步:异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。这也就是定时器并不能精确在指定时间后输出回调函数结果的缘由。promise

具体来讲,异步执行的运行机制以下。(同步执行也是如此,由于它能够被视为没有异步任务的异步执行。)浏览器

(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。数据结构

(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。异步

(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。async

(4)主线程不断重复上面的第三步。

上文提到了“执行栈、任务队列”,下面咱们来唠唠他俩是啥
执行栈

当咱们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有做用域、上层做用域(做用域链)、方法的参数,以及这个做用域中定义的变量和 this 的指向,而当一系列方法被依次调用的时候。因为 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是所谓执行栈。

任务队列

"任务队列"是一个事件的队列(也能够理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务能够进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件之外,还包括一些用户产生的事件(好比鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。可是,因为存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

了解了前面的知识后,咱们再来看啥是事件循环(event loop)

咱们注意到,在异步代码完成后仍有可能要在一旁等待,由于此时程序可能在作其余的事情,等到程序空闲下来才有时间去看哪些异步已经完成了。因此 JavaScript 有一套机制去处理同步和异步操做,那就是事件循环 (Event Loop)。
示意图以下:

宏任务和微任务

以去银行办业务为例,当 5 号窗口柜员处理完当前客户后,开始叫号来接待下一位客户,咱们将每一个客户比做 宏任务接待下一位客户 的过程也就是让下一个 宏任务 进入到执行栈。

因此该窗口全部的客户都被放入了一个 任务队列 中。任务队列中的都是 已经完成的异步操做的,而不是注册一个异步任务就会被放在这个任务队列中(它会被放到 Task Table 中)。就像在银行中排号,若是叫到你的时候你不在,那么你当前的号牌就做废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来之后还须要从新取号。

在执行宏任务时,是能够穿插一些微任务进去。好比你大爷在办完业务以后,顺便问了下柜员:“最近 P2P 暴雷很严重啊,有没有其余稳妥的投资方式”。柜员暗爽:“又有傻子上钩了”,而后叽里咕噜说了一堆。

咱们分析一下这个过程,虽然大爷已经办完正常的业务,但又咨询了一下理财信息,这时候柜员确定不能说:“您再上后边取个号去,从新排队”。因此只要是柜员可以处理的,都会在响应下一个宏任务以前来作,咱们能够把这些任务理解成是 微任务

大爷听罢,扬起 45 度微笑,说:“我就问问。”

柜员 OS:“艹...”

这个例子就说明了:你大爷永远是你大爷 在当前微任务没有执行完成时,是不会执行下一个宏任务的!

总结一下,异步任务分为 宏任务(macrotask)微任务 (microtask)。宏任务会进入一个队列,而微任务会进入到另外一个不一样的队列,且微任务要优于宏任务执行。

常见的宏任务和微任务

宏任务:script(总体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)

微任务:Promise.then、 MutaionObserver、process.nextTick (Node.js)

来几道题试试?
setTimeout(() => {
        console.log('A');
    }, 0);
    var obj = {
    func: function() {
        setTimeout(function() {
        console.log('B');
        }, 0);
        return new Promise(function(resolve) {
        console.log('C');
        resolve();
        });
    },
    };
    obj.func().then(function() {
        console.log('D');
    });
    console.log('E');

先把打印结果呈上
image.png
再把解释呈上:

  • 第一个 setTimeout 放到宏任务队列,此时宏任务队列为 ['A']
  • 接着执行 obj 的 func 方法,将 setTimeout 放到宏任务队列,此时宏任务队列为 ['A', 'B']
  • 函数返回一个 Promise,由于这是一个同步操做,因此先打印出 'C'
  • 接着将 then 放到微任务队列,此时微任务队列为 ['D']
  • 接着执行同步任务 console.log('E');,打印出 'E'
  • 由于微任务优先执行,因此先输出 'D'
  • 最后依次输出 'A''B'

再来一个?

let p = new Promise(resolve => {
        resolve(1);
        Promise.resolve().then(() => console.log(2));
        console.log(4);
    }).then(t => console.log(t));
    console.log(3);

打印结果:
image.png

  • 首先将 Promise.resolve() 的 then() 方法放到微任务队列,此时微任务队列为 ['2']
  • 而后打印出同步任务 4
  • 接着将 p 的 then() 方法放到微任务队列,此时微任务队列为 ['2', '1']
  • 打印出同步任务 3
  • 最后依次打印微任务 21
当 Event Loop 遇到 async/await

async/await 仅仅是生成器的语法糖,因此不要怕,只要把它转换成 Promise 的形式便可。下面这段代码是 async/await 函数的经典形式。

async function foo() {
    // await 前面的代码
    await bar();
    // await 后面的代码
   }
    async function bar() {
    // do something...
    }
    foo();

其中 await 前面的代码 是同步的,调用此函数时会直接执行;而 await bar(); 这句能够被转换成 Promise.resolve(bar())await 后面的代码 则会被放到 Promise 的 then() 方法里。所以上面的代码能够被转换成以下形式,这样是否是就很清晰了?

function foo() {
        // await 前面的代码
        Promise.resolve(bar()).then(() => {
            // await 后面的代码
        });
    }
    function bar() {
        // do something...
    }
    foo();

最后咱们回到开篇那个题目

function async1() {
  console.log('async1 start'); // 2

  Promise.resolve(async2()).then(() => {
    console.log('async1 end'); // 6
  });
}

function async2() {
  console.log('async2'); // 3
}

console.log('script start'); // 1

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

async1();

new Promise(function(resolve) {
  console.log('promise1'); // 4
  resolve();
}).then(function() {
  console.log('promise2'); // 7
});
console.log('script end'); // 5
  • 首先打印出 script start
  • 接着将 settimeout 添加到宏任务队列,此时宏任务队列为 ['settimeout']
  • 而后执行函数 async1,先打印出 async1 start,又由于 Promise.resolve(async2()) 是同步任务,因此打印出 async2,接着将 async1 end 添加到微任务队列,,此时微任务队列为 ['async1 end']
  • 接着打印出 promise1,将 promise2 添加到微任务队列,,此时微任务队列为 ['async1 end', promise2]
  • 打印出 script end
  • 由于微任务优先级高于宏任务,因此先依次打印出 async1 endpromise2
  • 最后打印出宏任务 settimeout
Node.js 与 浏览器环境下事件循环的区别

Node.js 在升级到 11.x 后,Event Loop 运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval 和 setImmediate) 就马上执行微任务队列,这点就跟浏览器端一致。

案例
案例1
const p1 = new Promise((resolve, reject) => {
        console.log('promise1');
        resolve();
    })
   .then(() => {
        console.log('then11');
        new Promise((resolve, reject) => {
            console.log('promise2');
            resolve();
        })
        .then(() => {
            console.log('then21');
        })
        .then(() => {
            console.log('then23');
        });
  })
  .then(() => {
    console.log('then12');
  });

    const p2 = new Promise((resolve, reject) => {
    console.log('promise3');
        resolve();
    }).then(() => {
        console.log('then31');
    });
  • 首先打印出 promise1
  • 接着将 then11promise2 添加到微任务队列,此时微任务队列为 ['then11', 'promise2']
  • 打印出 promise3,将 then31 添加到微任务队列,此时微任务队列为 ['then11', 'promise2', 'then31']
  • 依次打印出 then11promise2then31,此时微任务队列为空
  • then21then12 添加到微任务队列,此时微任务队列为 ['then21', 'then12'](由于then21和then12都是第二层then)
  • 依次打印出 then21then12,此时微任务队列为空
  • then23 添加到微任务队列,此时微任务队列为 ['then23']
  • 打印出 then23
案例2

这道题实际在考察 Promise 的用法,当在 then() 方法中返回一个 Promise,p1 的第二个完成处理函数就会挂在返回的这个 Promise 的 then() 方法下,所以输出顺序以下。

const p1 = new Promise((resolve, reject) => {
        console.log('promise1'); // 1
        resolve();
    })
    .then(() => {
        console.log('then11'); // 2
        return new Promise((resolve, reject) => {
            console.log('promise2'); // 3
            resolve();
        })
        .then(() => {
            console.log('then21'); // 4
        })
        .then(() => {
            console.log('then23'); // 5
        });
    })
    .then(() => {
        console.log('then12'); //6
    });

将不断更新完善,欢迎批评指正!

参考

http://www.ruanyifeng.com/blo...
https://juejin.im/post/5cbc0a...

相关文章
相关标签/搜索