重点: javascript
从诞生之日起就是一门单线程的非阻塞的脚本语言
我们先来聊下 JavaScript
这两个特点:
JavaScript
是单线程的,单线程是指 JavaScript
引擎中解析和执行 JavaScript
代码的线程只有一个(主线程),每次只能做一件事情。单线程存在是必然的,在浏览器中, 如果 javascript
是多线程的,那么当两个线程同时对 dom
进行一项操作,例如一个向其添加事件,而另一个删除了这个 dom
,这个时候其实是矛盾的Javascript
代码运行一个异步任务的时候(像 Ajax
等),主线程会挂起这个任务,然后异步任务返回结果的时候再根据特定的结果去执行相应的回调函数如何做到非阻塞呢?这就需要我们的主角——事件循环(Event Loop
)
我们看一个很经典的图,这张图基本可以概括了事件循环(该图来自演讲—— 菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014[1])后面演示用的 Loupe[2] 也是该演讲者写的((Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响))
当 javascript
代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap
)和栈(stack
)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针
执行栈(call stack
):当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈
比如,如下是一段同步代码的执行
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
}
a();
我们通过 Loupe 演示下代码的执行过程:
同步代码的执行过程是相对比较简单的,但涉及到异步执行的话,又是怎样的呢?
事件队列(callback queue): js
引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js
会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列
被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码
看 Loupe
官方的一个例子:
$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});
console.log("Hi!");
setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);
console.log("Welcome to loupe.");
我们分析一下这个执行的过程:
Web Api
中setTimeout
,异步执行,将其挂载起来setTimeout
执行回调,将回调放入到事件队列中,一旦主线程空闲,则取出运行setTimeout
setTimeout
执行回调,将回调放入到事件队列中,一旦主线程空闲,则取出运行再回头看看这张图,应该有种豁然开朗的感觉
以上的过程按照类似如下的方式实现,queue.waitForMessage()
会同步地等待消息到达(如果当前没有任何消息等待被处理),故我们称之为事件循环(Event Loop
)
while (queue.waitForMessage()) {
queue.processNextMessage();
}
常见的 micro-task
:new Promise().then(callback)
、MutationObserve
等(async
和 await
)实际上是 Promise
的语法糖
常见的 macro-task
:setTimeout
、setInterval
、script
(整体代码)、 I/O
操作、UI 交互事件、postMessage
等
异步任务的返回结果会被放到一个事件队列中,根据上面提到的异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去
Eveent Loop
的循环过程如下:
script
)),如果没有可选的宏任务,则直接处理微任务GUI
线程接管渲染,进行浏览器渲染如下图所示:
执行顺序总结:执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环
为了更好的理解,我们来看一个例子
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
我们来分析一下:
script
,输出 startsetTimeout
压入 macrotask
队列,promise.then
回调放入 microtask
队列,最后执行 console.log('end')
,输出 end
script
属于宏任务,执行完成那接下来就是执行 microtask
队列的任务了,执行 promise
回调打印 promise1
promise
回调函数默认返回 undefined
,promise
状态变为 fullfill
触发接下来的 then
回调,继续压入 microtask
队列,event loop
会把当前的microtask 队列一直执行完,此时执行第二个
promise.then` 回调打印出promise2microtask
队列已经为空,接下来主线程会去做一些 UI
渲染工作(不一定会做),然后开始下一轮 event loop
,执行 setTimeout
的回调,打印出 setTimeout
故最后的结果如下:
start
end
promise1
promise2
setTimeout
增加这个环境在于,现在面试笔试都会出事件循环的题目,实际上的可能比上面的例子难,原因在于微任务和宏任务涉及的知识点不少,这就需要我们进一步巩固我们的基础知识,我相信能够认真对待以下题目的,都能够更好的掌握事件循环
我就暂不做分析,大家不懂的有疑问的可以在评论区一起交流
答案在评论区内
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);
new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
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')
let resolvePromise = new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise);
// 提示:resolve(resolvedPromise) 等同于:
// Promise.resolve().then(() => resolvedPromise.then(resolve));
})
resolvePromise.then(() => {
console.log('resolvePromise resolved')
})
let resolvedPromiseThen = Promise.resolve().then(res => {
console.log('promise1')
})
resolvedPromiseThen
.then(() => {
console.log('promise2')
})
.then(() => {
console.log('promise3')
})
console.log('script start');
setTimeout(() => {
console.log('Gopal');
}, 1 * 2000);
Promise.resolve()
.then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
async function foo() {
await bar()
console.log('async1 end')
}
foo()
async function errorFunc () {
try {
// Tips:参考:https://zh.javascript.info/promise-error-handling:隐式 try…catch
// Promise.reject()方法返回一个带有拒绝原因的Promise对象
// Promise.reject('error!!!') === new Error('error!!!')
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))
function bar() {
console.log('async2 end')
}
console.log('script end');
new Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(() => {
console.log(2)
new Promise((resolve, reject) => {
console.log(3)
setTimeout(() => {
reject();
}, 3 * 1000);
resolve()
})
.then(() => {
console.log(4)
new Promise((resolve, reject) => {
console.log(5)
resolve();
})
.then(() => {
console.log(7)
})
.then(() => {
console.log(9)
})
})
.then(() => {
console.log(8)
})
})
.then(() => {
console.log(6)
})
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})
Promise.reject().then(() => {
console.log('13');
}, () => {
console.log('12');
})
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
})
setTimeout(() => {
console.log('9');
Promise.resolve().then(() => {
console.log('10');
})
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
})
本文从 JS
的两个特点:单线程以及非阻塞介绍了事件循环的必要性,因为事件循环在浏览器和 Node.js
的表现是很大不一样的,本人只谈论到了浏览器中的事件循环,并介绍了微任务和宏任务,以及它们的执行流程,最后通过 7 道题目帮助大家巩固知识
大家喜欢的话,别忘了点赞关注~
详解JavaScript中的Event Loop(事件循环)机制[9]
深入理解NodeJS事件循环机制[10]
并发模型与事件循环[11]
【前端体系】从一道面试题谈谈对EventLoop的理解[12]
菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014[13]
JavaScript中的Event Loop(事件循环)机制[14]
JS事件循环机制(event loop)之宏任务/微任务[15]
深入理解js事件循环机制(浏览器篇)[16]
从面试题看 JS 事件循环与 macro micro 任务队列[17]
[1]
菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ
[2]
Loupe: http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D
[3]
一个合格的中级前端工程师应该掌握的 20 个 Vue 技巧: https://juejin.im/post/6872128694639394830
[4]
【Vue进阶】——如何实现组件属性透传?: https://juejin.im/post/6865451649817640968
[5]
前端应该知道的 HTTP 知识【金九银十必备】: https://juejin.im/post/6864119706500988935
[6]
最强大的 CSS 布局 —— Grid 布局: https://juejin.im/post/6854573220306255880
[7]
如何用 Typescript 写一个完整的 Vue 应用程序: https://juejin.im/post/6860703641037340686
[8]
前端应该知道的web调试工具——whistle: https://juejin.im/post/6861882596927504392
[9]
详解JavaScript中的Event Loop(事件循环)机制: https://zhuanlan.zhihu.com/p/33058983
[10]
深入理解NodeJS事件循环机制: https://juejin.im/post/6844903999506923528
[11]
并发模型与事件循环: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
[12]
【前端体系】从一道面试题谈谈对EventLoop的理解: https://juejin.im/post/6868849475008331783
[13]
菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ
[14]
JavaScript中的Event Loop(事件循环)机制: https://segmentfault.com/a/1190000022805523
[15]
JS事件循环机制(event loop)之宏任务/微任务: https://juejin.im/post/6844903638238756878
[16]
深入理解js事件循环机制(浏览器篇): http://lynnelv.github.io/js-event-loop-browser
[17]
从面试题看 JS 事件循环与 macro micro 任务队列: https://juejin.im/post/6844903796754104334