从一道题浅谈JavaScript 运行机制

前言

想要了解一门语言,最好的办法就是了解它的运行机制。掌握了运行机制,能够让我们在开发中少走许多弯路,写出高质量的代码。本文从一道题浅谈JavaScript运行机制,给刚刚接触JavaScript的小白一个初步的了解,为将来打好基础。

//请写出输出内容
async function async1() {
  console.log('2 async1 start')
  await async2()
  console.log('6 async1 end')
}

async  function async2() {
  console.log('3 async2')
}

console.log('1 script start')

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

async1()

new Promise(function(resolve) {
  console.log('4 promise1')
  resolve()
}).then(function() {
  console.log('7 promise2')
})

console.log('5 script end')

在`Chrome 80`和`node v12`中,正确输出是:
1 script start
2 async1 start
3 async2
4 promise1
5 script end
6 async1 end
7 promise2
undefined
8 setTimeout

分析:

  1. console.log('1 script start')是同步任务,直接打印 1 script start
  2. setTimeout 是异步任务,且是宏任务源,放到宏任务队列中,等待下次 Event Loop 才会执行;
  3. 执行 async1(),await 之前的代码立即执行,打印2 async1 start,await 后面的表达式执行一遍,打印3 async2,await 后面的代码是 console.log('6 async1 end') 是微任务源,放到微任务队列中,接着跳出 async1 函数来执行后面的代码。
  4. new Promise 是同步任务,直接执行,打印4 promise1
  5. Promise.then 是微任务,将 console.log('7 promise2')放到微任务队列
  6. console.log('5 script end')是同步任务,直接执行,打印'5 script end 7.此时主线程任务执行完毕,检查微任务队列中,此时,微任务中, Promise 队列有的两个任务 console.log('6 async1 end') 和 console.log('7 promise2'),因此按先后顺序输出 6 async1 end7 promise2
  7. 微任务执行完毕,第一次循环结束;
  8. 第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出8 setTimeout,至此整个流程结束。

这道题主要考察的是事件循环中函数执行顺序的问题,其中包括 async ,await,setTimeout,Promise 函数。下面来说一下本题中涉及到的知识点。

Promise 和 async 中的立即执行

  • Promise 中的异步体现在 then 和 catch 中
  • Promise 中的代码是被当做同步任务立即执行的。
  • await 出现之前,代码也是立即执行的
  • await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到 microtask 中,然后就会跳出整个 async 函数来执行后面的代码。
    因此
    await async2()可以改成以下代码:
return Promise.resolve(async2()).then(res => {
    console.log('6 async1 end')
})

进程与线程

  • 进程是 CPU 资源分配的基本单位,进程是由一个或多个线程组成
  • 线程是 CPU 调度和分配的基本单位,同个进程之中的多个线程之间是共享该进程资源的

多线程的浏览器内核

多线程的浏览器内核

GUI 渲染线程

  • 渲染页面、解析 DOM,页面重绘时会调起该线程
  • 和 JS 引擎线程互斥,当 JS 引擎线程在工作时,GUI 渲染线程挂起,GUI 更新被放 入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。

JS 引擎线程

  • 单线程工作、解析和运行 js 脚本

事件触发线程

  • 当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。

定时器触发线程

  • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
  • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。

http 请求线程

  • http 请求的时候会开启一条请求线程。
  • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

单线程的 JS 引擎

JS 单线程原因:如果多线程,假如有 2 个线程,同时对一个 dom 操作,一个删除,另一个编辑,浏览器不能确定 dom 状态,无法下达 2 个矛盾的命令,所以,js 设计之初就是单线程。
好处:简单,没有线程切换维护开销,省内存。

任务队列和事件循环

  • JS 分为同步任务和异步任务;
  • 同步任务都在主线程上执行,形成一个执行栈;
  • 主线程之外,事件触发线程管理着一个任务队列,当异步任务有了运行结果后,将注册的回调函数放在任务队列之中。
  • 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
  • 一直循环读取任务队列,并执行的操作,就形成了事件循环

根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。

任务队列

定时器

  • 定时器会开启一条定时器触发线程来触发计时
  • 定时器会在等待了指定的时间后将事件放入到任务队列中等待读取到主线程执行。
  • 定时器指定的延时毫秒数其实并不准确,因为定时器只是在到了指定的时间时将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中的事件全部执行完成之后,才会去读取定时器的事件到主线程执行,中间可能会存在耗时比较久的任务,那么就不可能保证在指定的时间执行。

宏任务

(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得 JS 内部(macro)task 与 DOM 任务能够有序的执行,会在一个(macro)task 执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

(macro)task 主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

微任务

microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。

所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。

microtask 主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

运行机制

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)

流程图如下:


代码示例

第一个例子

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

执行结果: 1 3 2

分析:

  1. console.log(1)是同步任务,直接打印 1;
  2. setTimeout 是异步任务,且是宏函数,放到宏函数队列中,等待下次 Event Loop 才会执行;
  3. console.log(3)是同步任务,直接打印 3;
  4. 主线程执行完毕,没有微任务,那么执行第二个宏任务 setTimeout,打印 2;
  5. 结果:1,3,2

第二个例子

setTimeout(function(){
    console.log(1)
});

new Promise(function(resolve){
    console.log(2);
    for(var i = 0; i < 10000; i++){
        i == 9999 && resolve();
    }
}).then(function(){
    console.log(3)
});

console.log(4);

执行结果:2 4 3 1

分析:

  1. setTimeout 是异步,且是宏函数,放到宏函数队列中;
  2. new Promise 是同步任务,直接执行,打印 2,并执行 for 循环;
  3. promise.then 是微任务,放到微任务队列中;
  4. console.log(4)同步任务,直接执行,打印 4;
  5. 此时主线程任务执行完毕,检查微任务队列中,有 promise.then,执行微任务,打印 3;
  6. 微任务执行完毕,第一次循环结束;从宏任务队列中取出第一个宏任务到主线程执行,打印 1;
  7. 结果:2,4,3,1

第三个例子

console.log(1);

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

Promise.resolve().then(function() {
  console.log(3);
}).then(function() {
  console.log('4.我是新增的微任务');
});

console.log(5);

执行结果:1 5 3 4.我是新增的微任务 2

分析:

  1. console.log(1)是同步任务,直接执行,打印 1;
  2. setTimeout 是异步,且是宏函数,放到宏函数队列中;
  3. Promise.resolve().then 是微任务,放到微任务队列中;
  4. console.log(5)是同步任务,直接执行,打印 5;
  5. 此时主线程任务执行完毕,检查微任务队列中,有 Promise.resolve().then,执行微任务,打印 3;
  6. 此时发现第二个.then 任务,属于微任务,添加到微任务队列,并执行,打印 4.我是新增的微任务;
  7. 这里强调一下,微任务执行过程中,发现新的微任务,会把这个新的微任务添加到队列中,微任务队列依次执行完毕后,才会执行下一个循环;
  8. 微任务执行完毕,第一次循环结束;取出宏任务队列中的第一个宏任务 setTimeout 到主线程执行,打印 2;
  9. 结果:1,5,3,4.我是新增的微任务,2

第四个例子

function add(x, y) {
  console.log(1)
  setTimeout(function() { // timer1
    console.log(2)
  }, 1000)
}
add();

setTimeout(function() { // timer2
  console.log(3)
})

new Promise(function(resolve) {
  console.log(4)
  setTimeout(function() { // timer3
    console.log(5)
  }, 100)
  for(var i = 0; i < 100; i++) {
    i == 99 && resolve()
  }
}).then(function() {
  setTimeout(function() { // timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

执行结果:1 4 8 7 3 6 5 2

分析:

  1. add()是同步任务,直接执行,打印 1;
  2. add()里面的 setTimeout 是异步任务且宏函数,记做 timer1 放到宏函数队列;
  3. add()下面的 setTimeout 是异步任务且宏函数,记做 timer2 放到宏函数队列;
  4. new Promise 是同步任务,直接执行,打印 4;
  5. Promise 里面的 setTimeout 是异步任务且宏函数,记做 timer3 放到宏函数队列;
  6. Promise 里面的 for 循环,同步任务,执行代码;
  7. Promise.then 是微任务,放到微任务队列;
  8. console.log(8)是同步任务,直接执行,打印 8;
  9. 此时主线程任务执行完毕,检查微任务队列中,有 Promise.then,执行微任务,发现有 setTimeout 是异步任务且宏函数,记做 timer4 放到宏函数队列;
  10. 微任务队列中的 console.log(7)是同步任务,直接执行,打印 7;
  11. 微任务执行完毕,第一次循环结束;
  12. 检查宏任务 Event Table,里面有 timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即 Event Queue:timer2、timer4、timer3、timer1,取出排在第一个的 timer2;
  13. 取出 timer2 执行,console.log(3)同步任务,直接执行,打印 3;
  14. 没有微任务,第二次 Event Loop 结束; 15.取出 timer4 执行,console.log(6)同步任务,直接执行,打印 6;
  15. 没有微任务,第三次 Event Loop 结束;
  16. 取出 timer3 执行,console.log(5)同步任务,直接执行,打印 5;
  17. 没有微任务,第四次 Event Loop 结束;
  18. 取出 timer1 执行,console.log(2)同步任务,直接执行,打印 2;
  19. 没有微任务,也没有宏任务,第五次 Event Loop 结束;
  20. 结果:1,4,8,7,3,6,5,2

第五个例子

setTimeout(function() { // timer1
  console.log(1);
  setTimeout(function() {  // timer3
    console.log(2);
  })
}, 0);
setTimeout(function() {  // timer2
  console.log(3);
}, 0);

执行结果:1 3 2

分析:

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

推荐阅读更多精彩内容