JavaScript 运行机制
单线程(单一主线程)
想必大家都了解,JavaScript语言最大的特点之一就是单线程,什么是单线程呢?
就是同一时间只能执行一件事情,这么设计的主要原因是为了防止用户在操作时出现冲突,例如两个线程同时处理一个DOM那么以谁为准呢?
在H5中新增了一个后台运行的线程Web Workers,但是这个线程是受主线程控制的并且不能操作DOM。
Event Loop(事件环)
图片转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》

存放在堆(heap)内存中的都是对象,栈里面的变量实际保存的是一个指针,这个指针指向堆(heap)内存中的对象。
执行栈里面的代码开始执行,栈里面的方法可能调用webAPI(操作DOM, ajax, 定时器)
将他们的回调加入callback queue(任务队列)
例如 在stack里面执行了ajax,当ajax运行完成后在callback queue里面加ajax的回调,
定时器一样,必须得定时器到时间才会将其回调函数加入callback queue
任务队列里面的回调都是异步的回调
console.log(1);
let fn = () => { console.log(2) }
setTimeout(fn)
console.log(3)
// 执行 console.log(1) console.log(3) 等待定时器到期将fn加入事件队列
// 只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
//
需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
callback queue遵循先进先出的逻辑,先被加入队列的回调会被先执行
宏任务与微任务
看一段代码
// 思考一下执行结果的顺序会是什么
Promise.resolve().then(() => {
console.log(1);
})
console.log(2);
setTimeout(() => {
console.log(3);
})
// 结果是 2 1 3
惊不惊喜?难不成promise.then和setTimeout一样?那我们换一个顺序
console.log(2);
setTimeout(() => {
console.log(3);
})
Promise.resolve().then(() => {
console.log(1);
})
// 结果依旧是 2 1 3
这是宏任务与微任务的原因
Promise.then是微任务,setTimeout是宏任务,
微任务在执行栈中代码走完后立即执行,在宏任务之前执行,所有微任务执行完再执行宏任务
宏任务
setTimeout setInterval (setImmediate)
微任务
Promise.then,浏览器把它的实现放到了微任务中,MutationObserve不兼容, MessageChannel微任务(vue中nextTick实现原理)
再看一段代码
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise')
})
})
setTimeout(function(){
console.log(3);
})
带浏览器中输入结果的顺序是 1 2 prmise 3
先走执行栈 console.log(1); 先走第一个setTimeout,将微任务放到队列中,执行微任务,微任务执行完再走宏任务 (浏览器过程)
但是在node里面执行就不是这么回事了
node里面执行结果是1 2 3 promise
node是将当前任务队列里面的所有回调走完再走微任务的回调队列
你没听错,微任务有一个自己的执行队列
Web Workers
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面, 意思就是在执行Web Worker里面脚本时,页面不会假死。
简单说一下专用worker用法,一个专用worker仅仅能被生成它的脚本所使用
main.js(主线程文件)
if (window.worker) { // 处理错误兼容
let myWorker = new Worker('worker.js'); // 指定一个脚本的URI
// 获取两个input输入框
let first = document.getElementById('first')
let first = document.getElementById('second')
// workers的方法通过postMessage()方法和onmessage事件处理函数生效
first.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}
second.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}
myWorker.onmessage = function(e) { // 获取worker.js 的返回结果
result.textContent = e.data;
console.log('Message received from worker');
}
}
worker.js
onmessage = function(e) {
// 传进来的参数都在e.data里面
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult); // 返回数据
}
node
node 的 Event Loop
当Node.js启动时,它会初始化事件循环,这可能会调用异步API调用,定时器或调用 process.nextTick(),然后开始处理事件循环。
下图显示了事件循环的操作顺序的简化概述。

每个阶段都有一个执行回调的FIFO(先入先出 first in,first out)队列。
阶段概述
- 定时器: 此阶段执行由setTimeout()和setInterval()定义的回调。
- I/O回调: 此阶段执行几乎所有的回调,除了关闭回调,定时器和setImmediate().
- idle, prepare: 只在内部使用。
- 轮询: 检索新的I / O事件。检查是否有定时器到期。
- 检查: setImmediate()在这里调用回调。
- 关闭回调: socket.on('close', ...)等。
详细阶段
计时器
计时器指定时间之后可以执行提供的回调,但不会立即执行,只是把回调放入timers阶段的队列中。
注意:技术上讲,轮询阶段控制何时执行定时器
I/O回调
此阶段为某些系统操作(读写文件等)执行回调。
轮询
该阶段有两个主要功能
1.执行以及到时间的定时器
2.处理轮询队列中的事件
当进入此阶段并且没有定时器时:
-
如果轮询队列不为空,则事件循环将遍历其回调队列,同步执行它们,直到队列耗尽或达到系统相关硬限制。
-
如果轮询队列为空,则会发生以下两件事之一:
- 如果setImmediate()已经被调用,那就结束轮询阶段进入检查阶段
- 如果没有setImmediate()调用,事件循环将等待回调被添加到队列中,然后立即执行它们
一旦轮询队列为空,事件循环将检查已达到时间的定时器。如果一个或多个定时器准备就绪,则事件循环将回退到定时器阶段以执行这些定时器的回调。
检查
执行setImmediate()的阶段
node里面的微任务
node 里面的微任务有 then 和 nextTick
他们不是事件循环里面的一部分微任务有自己的队列,无论事件循环在任何阶段,微任务队列都将在当前操作完成后处理。也就是当前阶段结束,下一个阶段开始之前清空微任务队列。
最后
有理解不对的地方还请多多指教。