javascript运行在单线程里,意思就是js每次只能执行一段代码。js有自己的并发模型,由栈、堆和队列组成。如下图

JavaScript 引擎从顶部开始执行脚本,然后向下工作,创建执行上下文并将函数推入和弹出调用堆栈

如果函数需要很长时间才能执行,那么在函数执行期间,用户将无法在 Web 浏览器上执行任何操作,页面卡死

举个例子🌰

function task(message) {
  // 模拟耗时任务
  let n = 10000000000;
  while (n > 0){
    n--;
  }
  console.log(message);
}

console.log('开始执行');
task('耗时任务');
console.log('结束!');

上面的代码执行,每次执行一个函数入栈,在执行 task 函数的时候,由于是一个耗时函数,会阻塞后面代码的执行,直到 task 函数执行完成后,出栈后继续执行最后的 console.log('结束!')

函数调用形成了一个栈帧

给对象分配内存都在堆里实现的

队列

在js执行的过程中,所有的回调函数都会放到消息队列,当栈为空的时候,就从消息队列中取出函数逐一处理,直到栈为空时消息处理结束,例如 setTimeout 等函数

事件循环

不同于c++等多线程语言,js是单线程,所以需要事件循环来检测栈是否为空,然后把函数或者消息进栈执行,类似于这样实现

while (queue.waitForMessage()) {
    queue.processNextMessage();
}

浏览器的并发机制

浏览器提供了Web APIs,事件循环和回调队列,如下图


web APIs:包含ajax,setTimeout,Events等等

回调队列:所有的事件监听都会放到回调队列中去,例如点击,focus事件都放到此队列等待响应

事件循环:监听栈是否为空,然后把回调函数放到栈中执行

console.log(1);
setTimeout(function() {
  console.log(2)
}, 0);
console.log(3);

上面代码执行到setTimeout的时候回放到回调队列里面去,然后其他函数从上到下执行完毕,栈为空的时候就把回调函数放到栈中执行,所以结果输出为 1,3,2

回调队列的分类

执行回调队列的过程,浏览器会有两个任务队列,优先执行微任务队列的回调,再执行宏任务队列的回调。

宏任务

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

微任务

Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)

参考资料