@frank-shaw
2020-02-01T22:01:44.000000Z
字数 3746
阅读 925
javaScript
JavaScript语言的一大特点就是单线程。也就是说,同一个时间只能做一件事。从JavaScript引擎的角度来看,就是任何时刻只能有一段代码在执行。
那么,为什么JavaScript不使用多个线程呢?毕竟,多线程的效率更高呢。
其实,JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。如果用过类似 JAVA 语言的多线程编程,会了解到多线程在提供便利性的同时,也带来了问题:线程切换带来性能开销、线程死锁情况等。
于是,为了避免复杂性,从一诞生,JavaScript就是单线程,这已成为这门语言的核心特征,将来也不会改变。
单线程意味着什么呢?意味着:程序启动后,在JavaScript引擎中,只会有一个调用栈(call stack),同时任何时刻只能有一段代码在执行。
JavaScript引擎的任务是严格遵循ECMAScript规范,解析对应的JS语句并执行。仅此而已。
再来说JavaScript执行环境。如果宿主环境是浏览器,那么JavaScript执行环境就包含了很多东西了:JavaScript引擎、WEB API、Event Loop等。(宿主环境变成Node环境也类似)
于是可以看到,在宿主环境中,JavaScript执行环境是包含了JavaScript引擎的。两者是包含关系,而不是对等关系。这个必须要清楚。
我们看到,在JavaScript执行环境中包含了 Event Loop。那么,这个 Event Loop 是干嘛用的呢? 简单讲,Event Loop 是 JavaScript 执行环境中实现异步调用的一种实现方式。
先来看看 whatwg 的网站对于 Event Loop 的定义与规范(https://html.spec.whatwg.org/multipage/webappapis.html#event-loops)。有人将这些规范变成了伪代码,如下:
eventLoop = {
taskQueues: {
events: [], // UI events from native GUI framework
parser: [], // HTML parser
callbacks: [], // setTimeout, requestIdleTask
resources: [], // image loading
domManipulation[]
},
microtaskQueue: [
],
nextTask: function() {
// Spec says:
// "Select the oldest task on one of the event loop's task queues"
// Which gives browser implementers lots of freedom
// Queues can have different priorities, etc.
for (let q of taskQueues)
if (q.length > 0)
return q.shift();
return null;
},
executeMicrotasks: function() {
if (scriptExecuting)
return;
let microtasks = this.microtaskQueue;
this.microtaskQueue = [];
for (let t of microtasks)
t.execute();
},
needsRendering: function() {
return vSyncTime() && (needsDomRerender() || hasEventLoopEventsToDispatch());
},
render: function() {
dispatchPendingUIEvents();
resizeSteps();
scrollSteps();
mediaQuerySteps();
cssAnimationSteps();
fullscreenRenderingSteps();
animationFrameCallbackSteps();
while (resizeObserverSteps()) {
updateStyle();
updateLayout();
}
intersectionObserverObserves();
paint();
}
}
// how it work:
while(true) {
task = eventLoop.nextTask();
if (task) {
task.execute();
}
eventLoop.executeMicrotasks();
if (eventLoop.needsRendering())
eventLoop.render();
}
以上代码粗略地描述了 Event Loop 的构成与用法。这当中有两个概念:taskQueues、microtaskQueue。taskQueues 即是我们常说的宏任务,它是一个 Set,将不同的任务分放到不同的队列中(当然,队列之间有不同的执行优先级)。我们看到,这其中既有UI事件队列,也有 回调函数队列等。microtaskQueue 即是我们常说的微任务,它的结构就是一个队列。
Event Loop 是怎么工作的呢?看最后的代码:
while(true) {
task = eventLoop.nextTask();
if (task) {
task.execute();
}
eventLoop.executeMicrotasks();
if (eventLoop.needsRendering())
eventLoop.render();
}
Event Loop 是一直都在运行的。在一次循环中,从宏任务 taskQueues 中找到一个 task 去执行;在每个宏任务执行完之后,再去执行 microtaskQueue 微任务队列中所有的微任务。具体如下图所示:
那么,你可能会问了:伪代码里并没有写清楚,这些宏任务与微任务是怎么放到 Event Loop 对应的队列中的呀?
不急。我们接着往下看。
当宿主(浏览器或者 Node 环境)拿到一段 JavaScript 代码时,首先做的就是:传递给 JavaScript 引擎,并且要求它去执行。
所以,我们首先应该形成一个感性的认知:一个 JavaScript 引擎会常驻于内存中,它等待着宿主把 JavaScript 代码或者函数传递给它执行。
然而,执行 JavaScript 并非一锤子买卖。
宿主环境当遇到一些事件时,会将事件放入到 Event Loop 对应的队列中,并设置对应的Watcher。Event Loop 在轮询时,会询问对应Watcher所关联的事件是否完成,若完成,就会将该事件对应的回调函数或代码(如果有),传递给 JavaScript 引擎去执行。
此外,我们可能还会提供 WEB API 给 JavaScript 引擎,比如 setTimeout 这样的 API, JavaScript 引擎会将其放入到Event Loop 对应的队列中,并设置对应的定时器 Watcher。并依据类似的机制,Event Loop 会询问定时器 Watcher 对应的时间是否已到。若时间已到,那么就会将监控到 setTimeout 对应的回调函数传递给 JavaScript 引擎去执行。
上面这个过程,Event Loop 在实际过程中用到了** Watcher 观察者**。上一章节的问题:这些宏任务与微任务是怎么放到 Event Loop 对应的队列中的呀?答案就是 Watcher 观察者。关于其实现的详细过程,在此不敷述。
在 ES3 和更早的版本中,JavaScript 引擎本身还没有异步执行代码的能力。这也就意味着,宿主环境(通过 Event Loop)传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。
但是,在 ES5 之后,JavaScript 引入了 Promise。这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。
采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏任务,把 JavaScript 引擎发起的任务称为微任务。于是,这个时候,Event Loop 的微任务队列也正式被使用了起来。
因此我们知道,在 Promise 出现以前,所有的异步事件与异步 Web API 的实现都属于 Event Loop 中的宏任务。
如果我们弄清楚了这些概念,那么关于 Event Loop 相关的知识点,就可以较好解决了。
那么做个小测试吧~ 下面代码的输出是什么呢?
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")