JavaScript运行机制

一、JavaScript为什么是单线程?

JavaScript语言的一大特点就是单线程,这意味着JS代码在执行的时候,都只有一个主线程来处理所有的任务。这与它最初设计时的主要用途有关。

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。假如JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

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

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、JavaScript运行时概念

各个JavaScript引擎实现的运行时模型,主要有三部分组成:栈、堆和队列。下图来自MDN:

(一)堆

我们知道JavaScript内存空间分为栈内存和堆内存。占用空间较小的6中基本类型数据(Sting、Number、Boolean、null、undefined、Symbol)存储在栈内存中。其余的引用类型数据(Object、Array、Function)的实体存储在堆内存中,但它们的引用指针是存储在栈内存中的。这部分详细内容可参考这篇文章。本文要讲的栈是函数调用栈,与栈内存还不是完全一致。相同点:都是栈这种先进后出、后进先出特点的数据结构。

(二)栈

函数调用形成了一个由若干帧组成的栈,有时我们也会叫调用栈、函数调用栈、执行栈、执行上下文栈(Execution Context Stack,ECS)。

当一个函数被调用时会形成执行上下文环境,其中存放着这个函数的变量对象(Variable Object, VO),作用域链(Scope Chain),调用该函数的this对象,函数实参。执行上下文作为一个调用帧被压入函数调用栈。

function fun1(x) {
  const t = 9;
  return fun2(x * t );
}

function fun2(y) {
  return y + 11;
}

function fun3() {
  return 100;
}

fun1(7)
fun3()

示例中先调用函数fun1,将其压入执行栈内,fun1内又调用了函数fun2,再把fun2压入执行栈顶。fun2执行完后被弹出执行栈,然后执行fun1,fun1执行完之后也会被弹出栈。此时调用函数fun3,再把fun3压入执行栈,fun3执行完后弹出执行栈。

JavaScript的同步任务才会进入主线程的执行栈中。

(三)队列

除了同步任务之外,JavaScript的异步任务不进入主线程,而是进入任务队列(task queue)或称消息队列(message queue),比XHR网络请求、文件读写、键盘输入、鼠标点击。任务队列又分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task 与 job 。异步任务执行完成后有了结果,就会在任务队列之中放置一个事件,该事件会告知主线程,异步任务执行状态已完成,可以将其回调函数放入主线程的执行栈中去执行了。

主线程和事件循环只有一个,但任务队列会有多个。微任务队列优先执行,宏任务队列后执行。任务队列是一个先进先出的数据结构,排在前面的事件,优先被主线程读取执行。

(四)Event Loop 事件循环

JavaScript异步执行的运行机制如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在多个任务队列。只要异步任务运行状态完成,就在任务队列之中放置一个事件。
(3)一旦执行栈中的所有同步任务执行完毕,主线程就会读取任务队列,看看里面有哪些事件,这些事件对应的异步任务回调函数,会进入执行栈依次执行。
(4)主线程不断重复第三步,这个过程循环不断,所以整个机制又叫做 Event Loop 事件循环。

下图展示了事件循环机制

栈中的代码调用各种外部API,它们执行后往任务队列塞入各种事件,栈中的代码执行完后循环不断地去获取任务队列中的事件,将各种回调函数依次取回主线程执行栈去执行。

三、宏任务和微任务

宏任务方法是由JS宿主环境提供的,微任务方法是由JS引擎自身提供的。

宏任务有:setTimeout 早于 setImmediate(Node.js)、setInterval、postMessage、Web Worker中的MessageChannel、I/O、UI rendering

微任务有:process.nextTick(Node.js) 早于 Promise.then[.cache .finally]、MutaionObserver

微任务队列优先执行,宏任务队列后执行。

 题目1

console.log("start");

setTimeout(() => {
  console.log(2);

  Promise.resolve().then(() => {
    console.log(3);
  });

}, 0);

new Promise(function (resolve, reject) {
  console.log(4);

  setTimeout(function () {
    console.log(5);
    resolve(6);
  }, 0);

}).then((res) => {
  console.log(7);

  setTimeout(() => {
    console.log(res);
  }, 0);
});

// 依次输出:start、4、2、3、5、7、6

 题目2

setImmediate(() => {
  console.log(1);
});
setTimeout(() => {
  console.log(2);
}, 0);
new Promise(((resolve) => {
  console.log(3);
  resolve();
  console.log(4);
})).then(() => {
  console.log(5);
});
process.nextTick(() => {
  console.log(7);
});
console.log(6);
console.log(8);

// Node环境中输出:3 4 6 8 7 5 2 1

上面示例中,process.nextTick 也会放入 microtask quque,为什么优先级比 promise.then 高呢?

原因在于,Node 中的 _tickCallback 在每次执行完任务队列中的一个任务后被调用,调用 _tickCallback 实质上干了两件事:

1、nextTickQueue中所有任务执行掉;

2、然后执行 _runMicrotasks 函数,即执行微任务队列(promise.then 注册的回调位于此)。

所以很明显 process.nextTick 永远优先于 promise.then。

 

 

 

参考:

JavaScript 运行机制详解:再谈Event Loop

My Promise

Event loops

posted on 2021-02-16 16:36  dawnxuuu  阅读(140)  评论(0编辑  收藏  举报

导航