共计 5153 个字符,预计需要花费 13 分钟才能阅读完成。
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 初始阶段。