3

一、JavaScript的单线程

众所周知,JavaScript的一大特点就是单线程,但是我们有没有思考过它为什么不能是多线程的?

我们假定JavaScript有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以为了避免这种复杂性,从一诞生,JavaScript就是单线程。

尽管HTML5提出Web Worker,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,并没有改变JavaScript单线程的本质。

二、定时器

定时器主要是setTimeout()和setInterval()这两个函数,这也是平时编程时候用到最多的。

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

上面代码的执行结果是1,3,2。但如果将setTimeout()的第二个参数设为0,就表示当前代码执行完以后,立即执行(0毫秒延迟)指定的回调函数。setTimeout(fn,0)的含义是,它在任务队列的尾部添加一个事件,在主线程最早得到空闲时去执行,也就是说,尽可能早得执行。

需要注意的是,定时器只是将事件插入了任务队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。如果当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。这也引申出JavaScript的并发模型

三、并发模型

我们先看一下理论上的并发模型:

图片描述

栈(stack):函数调用会形成了一个堆栈帧
堆(heap):对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域
队列(queue):运行时包含的一个待处理的消息队列。当栈为空时,则从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)

四、Event Loop

针对上面的并发模型和JavaScript的同步异步运行机制,我们可以看到整个流程大致是这样的:

1.所有同步任务都在主线程上执行,形成一个执行栈(并发模型的stack)。
2.主线程之外,还存在一个任务队列(并发模型的queue)。只要异步任务有了运行结果,就在任务队列中放置一个事件。
3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件和那些对应的异步任务,于是等待结束状态,进入执行栈,开始执行。
4.主线程不断重复上面的第三步。

这个过程是循环不断的,所以这种运行机制又称为Event Loop(事件循环)。放一张大神演讲时的图片来更好地理解Event Loop:

图片描述

我们可以看到,主线程运行的时候,产生(heap)和(stack),中的代码调用各种外部API,它们在任务队列中加入各种事件(click,load,done)。只要中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。

五、Macrotask 和 Microtask

这是一个比较冷门的知识,在并发模型中队列又可以分为Macrotask 和 Microtask,它们都属于异步任务。先来看一个例子:

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    console.log('promise1');

    setTimeout(function() {
        console.log('setTimeout in microtask');
    }, 0);
}).then(function() {
    console.log('promise2');
});

console.log('script end');

输出:

script start
script end
promise1
promise2
setTimeout
setTimeout in microtask

Macrotask 和 Microtask有什么区别呢?

  • Macrotasks:setTimeout, setInterval, setImmediate, I/O, UI rendering
  • Microtask:process.nextTick, Promises, Object.observe(废弃), MutationObserver

它们的执行过程如下:

JavaScript引擎首先从macrotask queue中取出第一个任务
执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行
然后再从macrotask queue中取下一个
执行完毕后,再次将microtask queue中的全部取出
循环往复,直到两个queue中的任务都取完


Mr_Nice
94 声望3 粉丝

致力于前端开发、