Javascript 运行机制

当学习成为了习惯,知识也就变成了常识。 感谢各位的 点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

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 示例一

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n94" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">// 脚本

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)</pre>

执行过程

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 示例二

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n141" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">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')</pre>

说明

函数前加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 执行顺序

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n185" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;"> 各个事件类型, 实行顺序自上而下
┌───────────────────────┐
┌─>│ 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', ...)
└───────────────────────┘</pre>

4.4.2 示例

4.4.2.1 基本示例

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n191" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">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)</pre>

这段代码在浏览器中的执行结果为:1 2 timer1 promise1 timer2 promise2

在node.js中的执行结果则为:1 2 timer1 timer2 promise1 promise2

4.4.2.2 setTimeout和setImmediate的顺序

它们两个顺序从上图看显而易见,timers队列在check队列执行运行,但是有个前提,事件已经就绪

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n204" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">setTimeout(() => {
console.log('timeout')
}, 0)

setImmediate(() => {
console.log('immediate')
})</pre>

以上代码在node.js中的运行结果为:immediate timeout,原因如下:

在程序运行时timer事件未就绪,所以第一次去读timer队列时,队列为空,继续向下执行,在check队列读取到了就绪的事件,所以先执行immediate,再执行timeout,因为即使setTimeout的延时时间未 0,但是node.js一般会设置为 1ms, 所以,当node准备Event Loop的时间大于 1ms时,就会先输出timeout,后输出immediate,否则先输出immediate后输出timeout

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="javascript" cid="n212" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">const fs = require('fs')

// 读取文件
fs.readFile('xx.txt', () => {
setTimeout(() => {
console.log('timeout')
})

setImmediate(() => {
console.log('immediate')
})
})</pre>

以上代码的输出顺序一定为:immediate timeout, 原因如下:

setTimeout和setImmediate都写在I/O callback中,意味着处于poll阶段,然后是check阶段,所以,此时无论setTimeout就绪多快(1ms),都会优先执行setImmediate,本质上,从poll阶段开始执行,而不是一个Tick初始阶段。

感谢各位的:点赞收藏评论,我们下期见。


当学习成为了习惯,知识也就变成了常识。 感谢各位的 点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容