event loop php,一次弄懂event loop

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

先来一道面试题镇楼

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节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

但单线程容易引起阻塞,比如:

alert(1);

console.log(2);

console.log(3);

console.log(4);

alert弹框只要不点击确定那就永远不会打印出2,3,4。

为了防止主线程堵塞,javaScript有了同步和异步的概念。

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

同步:同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步:异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。这也就是定时器并不能精确在指定时间后输出回调函数结果的原因。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

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

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

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

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

执行栈

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

任务队列

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

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

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

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

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

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

示意图如下:

1460000022024627

宏任务和微任务

以去银行办业务为例,当 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');

先把打印结果呈上

bVbEzyt

再把解释呈上:

第一个 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);

打印结果:

bVbEzAu

首先将 Promise.resolve() 的 then() 方法放到微任务队列,此时微任务队列为 ['2']

然后打印出同步任务 4

接着将 p 的 then() 方法放到微任务队列,此时微任务队列为 ['2', '1']

打印出同步任务 3

最后依次打印微任务 2 和 1

当 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 end 和 promise2

最后打印出宏任务 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

接着将 then11,promise2 添加到微任务队列,此时微任务队列为 ['then11', 'promise2']

打印出 promise3,将 then31 添加到微任务队列,此时微任务队列为 ['then11', 'promise2', 'then31']

依次打印出 then11,promise2,then31,此时微任务队列为空

将 then21 和 then12 添加到微任务队列,此时微任务队列为 ['then21', 'then12'](因为then21和then12都是第二层then)

依次打印出 then21,then12,此时微任务队列为空

将 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

});

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

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值