1. 单线程的JavaScript

JavaScript是单线程的语言这,由它的用处决定的,作为浏览器的脚本语言,次要负责和用户交互,操作DOM。
如果JavaScript是多线程的,有两个线程同时操作一个DOM节点,一个负责删除DOM节点,一个在DOM节点上增加内容,浏览器该以哪个线程为规范呢?
所以,JavaScript的用处决定它只能是单线程的,过来是,未来也不会变。
HTML5的Web Worker容许JavaScript主线程创立多个子线程,然而这些子线程齐全受主线程的管制,且不可操作DOM节点,所以JavaScript单线程的实质并没有产生扭转。

2. 同步工作和异步工作

JavaScript是单线程语言,就意味着工作须要排队执行,只有前一个执行实现,后一个才能够执行。
如果前一个工作十分耗时呢?比方操作IO设施、网络申请等,前面的工作就会被阻塞,页面就会被卡住,甚至解体,用户体验十分差。
如果JavaScript的主线程在遇到这些耗时的工作时,将其挂起,先执行前面的工作,等挂起的工作有后果当前再回头执行,这样就能够解决耗时工作阻塞主线程的问题了。
于是,所有的工作就能够分为两种,同步工作和异步工作,同步工作放在主线程中执行,异步工作被挂起,不进入主线程执行(让主线程阻塞期待),当其有后果了,再放入主线程中执行。

3. 工作队列和Event Loop

3.1 工作队列

工作队列是一个事件队列,也能够了解成音讯队列,当挂起的异步工作就绪当前就会在工作队列中搁置相应的事件,示意该工作能够进入主线程中执行了。
工作队列中的事件,除了IO设施的事件,还有网络申请,鼠标点击、滚动等,只有为事件指定过回调函数,这些事件产生时就会进入工作队列,期待主线程来读取,而后执行相应的回调函数。
回调函数其实就是被挂起来的异步工作,比方:Ajax申请,申请胜利或失败当前执行的回调函数就是异步工作。
工作队列是一个先进先出的数据结构,排在后面的事件,只有主线程一空,就会优先被读取。

3.2 Event Loop

主线程从工作队列读取事件,这个过程是循环不断的,所以JavaScript这种运行机制又称为Event Loop(事件循环)

4. 宏工作和微工作

异步工作可进一步划分为宏工作和微工作,相应的工作队列也有两种,别离为宏工作队列和微工作队列。

4.1 宏工作

setTimeout、setInterval、setImmediate会产生宏工作

4.2 微工作

requestAnimationFrame、IO、读取数据、交互事件、UI render、Promise.then、MutationObserve、process.nextTick会产生微工作

4.3 浏览器中的JavaScript脚本执行过程

4.3.1 过程形容

a. JavaScript脚本进入主线程, 开始执行
b. 执行过程中如果遇到宏工作和微工作,别离将其挂起,只有当工作就绪时将事件放入相应的工作队列
c. 脚本执行实现,执行栈清空
d. 去微工作队列顺次读取事件,并将相应的回调函数放入执行栈运行,如果执行过程中遇到宏工作和微工作,解决形式同 b, 直到微工作队列为空
e. 浏览器执行渲染动作, GUI渲染线程接管,直到渲染完结
f. JS线程接管,去宏工作队列顺次读取事件,并将相应的回调函数放入执行栈, 开始下一个宏工作的执行,过程为b -> c -> d -> e -> f, 如此循环
g. 直到执行栈、宏工作队列、微工作队列都为空,脚本执行完结

4.3.2 示例

4.3.2.1 示例一

// 脚本console.log(1)setTimeout(() => {  console.log(2)}, 0)const p = new Promise((resolve) => {  setTimeout(() => {    console.log(3)    resolve()  }, 1000)  console.log(4)})p.then(() => {  console.log(5)})console.log(6)

执行过程

a. 脚本放入执行栈开始履行
b. 执行到console.log(1), 输出1
c. 执行到setTimeout,遇到宏工作,将其挂起,因为延时 0ms,将在 4ms后在宏工作队列产生一个定时事件, 咱们叫定时A
d. 程序持续向下执行,执行new Promise(),并运行其参数,遇到第二个定时工作(宏工作),叫它定时B,并将其挂起,执行console.log(4), 输入4
e. 遇到微工作p.then(), 将其挂起
f. 向下执行遇到console.log(6), 输入6
g. 执行栈清空,读取微工作队列,发现为空,因为p.then()含没有就绪,它的就绪依赖与第一个定时工作(定时A)的执行
h. 执行栈为空,微工作队列为空,执行浏览器的渲染动作
i. 读取宏工作队列,读取第一个就绪的宏工作,为定时工作A,将其回调函数放入执行栈开始执行,执行console.log(2), 输出2
j. 执行栈清空,微工作队列为空,渲染
k. 开始执行下一个就绪的宏工作,定时工作B,并将其回调函数放入执行栈执行,执行console.log(3), 输入3,并执行resolve(), p.then()就绪,在微工作队列放入相应的事件
o. 执行栈清空,读取微工作队列,发现不为空,读取第一个就绪的事件,并将其对应的回调函数放入执行栈执行,执行console.log(5), 输入5
p. 执行栈清空,微工作队列为空,渲染,而后发现宏工作队列为空,本次脚本执行彻底完结
输入后果为: 1 4 6 2 3 5

4.3.2.2 示例二

async function async1 () {  console.log('async1_1')  await async2()  console.log('async1_2')}async function async2 () {  console.log('async2')}console.log('script start')setTimeout(() => {  console.log('setTimeout')}, 0)async1()new Promise(resolve => {  console.log('promise executor')  resolve()}).then(() => {  console.log('promise then')})console.log('script end')

阐明

函数前加async,实际上返回的是一个promise,比方这里的async2函数,返回的是一个立刻resoved  promise
await会将前面的同步代码执行实现(async2),而后让出线程,将异步工作(Promise.then)挂起,这里的立刻resolved promise,所以会在微工作队列增加一个事件,且排在上面的Promise.then之前

输入后果

如果上一个示例看懂了,再饥饿和该示例的阐明信息,答案就跃然纸上了:
script start => async1_1 => async2 => promise executor => script end => async1_2 => promise then => setTimeout

4.3.3 外链

外链

4.3.4 总结

如果把JavaScript脚本也当作初始的宏工作,那么JavaScript在浏览器端的执行过程就是这样:
先执行一个宏工作, 而后执行所有的微工作
再执行一个宏工作,而后执行所有的微工作
...
如此重复,执行执行栈和工作队列为空

4.4 node.js中JavaScript脚本的执行过程

JavaScript脚本执行过程在node.js和浏览器中有些不同, 造成这些差别的起因在于,浏览器中只有一个宏工作队列,然而node.js中有好几个宏工作队列,而且这些宏工作队列还有执行的先后顺序,而微工作时穿插在这些宏工作之间执行的

4.4.1 执行程序

  各个事件类型, 履行程序自上而下   ┌───────────────────────┐┌─>│        timers         │<————— 执行 setTimeout()、setInterval() 的回调│  └──────────┬────────────┘|             |<-- 先执行process.nextTick, 再执行MicroTask Queue 的回调│  ┌──────────┴────────────┐│  │     pending callbacks │<————— 执行由上一个 Tick 提早下来的 I/O 回调│  └──────────┬────────────┘|             |<-- 先执行process.nextTick, 再执行MicroTask Queue 的回调│  ┌──────────┴────────────┐│  │     idle, prepare     │<————— 外部调用(可疏忽)│  └──────────┬────────────┘     |             |<-- 先执行process.nextTick, 再执行MicroTask Queue 的回调|             |                   ┌───────────────┐│  ┌──────────┴────────────┐      │   incoming:   │ - (执行简直所有的回调,除了 close callbacks 以|  |                       |      |               |     及 timers 调度的回调和 setImmediate() 调度|  |         poll          |<-----|   connections,|        的回调,在失当的机会将会阻塞在此阶段)│  │                       │      |               │ │  └──────────┬────────────┘      │   data, etc.  │ │             |                   |               | |             |                   └───────────────┘|             |<-- 先执行process.nextTick, 再执行MicroTask Queue 的回调|  ┌──────────┴────────────┐      │  │        check          │<————— setImmediate() 的回调将会在这个阶段执行│  └──────────┬────────────┘|             |<-- 先执行process.nextTick, 再执行MicroTask Queue 的回调│  ┌──────────┴────────────┐└──┤    close callbacks    │<————— socket.on('close', ...)   └───────────────────────┘

4.4.2 示例

4.4.2.1 根本示例

console.log(1)setTimeout(() => {  console.log('timer1')  Promise.resolve().then(() => {    console.log('promise1')  })}, 0)setTimeout(() => {  console.log('timer2')  Promise.resolve().then(() => {    console.log('promise2')  })}, 0)console.log(2)
这段代码在浏览器中的执行后果为:1 2 timer1 promise1 timer2 promise2
在node.js中的执行后果则为:1 2 timer1 timer2 promise1 promise2

4.4.2.2 setTimeout和setImmediate的程序

它们两个程序从上图看不言而喻,timers队列在check队列执行运行,然而有个前提,事件曾经就绪
setTimeout(() => {  console.log('timeout')}, 0)setImmediate(() => {  console.log('immediate')})
以上代码在node.js中的运行后果为:immediate timeout,起因如下:
在程序运行时timer事件未就绪,所以第一次去读timer队列时,队列为空,持续向下执行,在check队列读取到了就绪的事件,所以先执行immediate,再执行timeout,因为即便setTimeout的延时工夫未 0,然而node.js个别会设置为 1ms, 所以,当node筹备Event Loop的工夫大于 1ms时,就会先输入timeout,后输入immediate,否则先输入immediate后输入timeout
const fs = require('fs')// 读取文件fs.readFile('xx.txt', () => {  setTimeout(() => {    console.log('timeout')  })  setImmediate(() => {    console.log('immediate')  })})
以上代码的输入程序肯定为:immediate timeout, 起因如下:
setTimeout和setImmediate都写在I/O callback中,意味着处于poll阶段,而后是check阶段,所以,此时无论setTimeout就绪多快(1ms),都会优先执行setImmediate,实质上,从poll阶段开始执行,而不是一个Tick初始阶段。