众所周知,JavaScript的一大特色就是单线程,可是咱们有没有思考过它为何不能是多线程的?编程
咱们假定JavaScript有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?因此为了不这种复杂性,从一诞生,JavaScript就是单线程。promise
尽管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):运行时包含的一个待处理的消息队列。当栈为空时,则从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及于是建立了一个初始堆栈帧)oop
针对上面的并发模型和JavaScript的同步异步运行机制,咱们能够看到整个流程大体是这样的:spa
1.全部同步任务都在主线程上执行,造成一个执行栈(并发模型的stack)。
2.主线程以外,还存在一个任务队列(并发模型的queue)。只要异步任务有了运行结果,就在任务队列中放置一个事件。
3.一旦执行栈中的全部同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件和那些对应的异步任务,因而等待结束状态,进入执行栈,开始执行。
4.主线程不断重复上面的第三步。线程
这个过程是循环不断的,因此这种运行机制又称为Event Loop(事件循环)。放一张大神演讲时的图片来更好地理解Event Loop:
咱们能够看到,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各类外部API,它们在任务队列中加入各类事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。
这是一个比较冷门的知识,在并发模型中队列又能够分为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有什么区别呢?
它们的执行过程以下:
JavaScript引擎首先从macrotask queue中取出第一个任务执行完毕后,将microtask queue中的全部任务取出,按顺序所有执行而后再从macrotask queue中取下一个执行完毕后,再次将microtask queue中的所有取出循环往复,直到两个queue中的任务都取完