简单而面试中又常见的知识点:JS执行机制

在开始讲解之前,我们先来看一段代码:


console.log('1');

setTimeout(function() {

console.log('2');

    process.nextTick(function() {

console.log('3');

    })

new Promise(function(resolve) {

console.log('4');

        resolve();

    }).then(function() {

console.log('5')

    })

});

process.nextTick(function() {

console.log('6');

})

new Promise(function(resolve) {

console.log('7');

    resolve();

}).then(function() {

console.log('8')

});

setTimeout(function() {

console.log('9');

    process.nextTick(function() {

console.log('10');

    })

new Promise(function(resolve) {

console.log('11');

        resolve();

    }).then(function() {

console.log('12')

    })

});

各位小伙伴觉得上面的结果输出会是多少呢?如果你没有了解过javascript的执行机制的话,上面的题目可能会让你崩溃。
不过别着急,先往下看,我保证你看到最后,能轻轻松松写出上面代码的答案,并且完全了解其中的原理。
首先,希望大家记住一个要点,javascript是单线程的语言。
因此,所有的javascript的异步特性都是基于单线程实现的,记住了这个特点,我们再去理解javascript的很多机制就容易很多了。
我们先从简单的代码说起,来引出今天的概念。


console.log('程序开始执行~');

setTimeout(() => {

console.log('执行setTimeout~');

}, 1000);

console.log('程序执行结束~');

// 输出结果:

// 程序开始执行~

// 程序执行结束~

// ...1s(这里表示等待时间)

// 执行setTimeout~

我想小伙伴们对上面结果都不会有疑问,setTimeout是我们常用来做延迟执行的全局函数。它接受两个参数,要执行的函数a和等待的秒数x,函数a会在程序经过x秒后执行。
这里引出我们的第一个概念:同步函数和异步函数。上面的函数a就是异步函数了,它不是立刻执行的函数,而是要等待一段时间,或者说满足一定的条件之后才执行的函数。
不过,有时候我们明明设置了3秒的定时,但是却发现函数并没有在3秒后执行,有时候会更久,这又是为什么呢?
这要从javascript的执行原理说起,js执行的时候,有一个专门存放异步函数的地方,称之为Event Table,而当异步函数已经满足回调的执行条件之后(比如时间过了x秒,异步请求返回了结果等等),原本放在Event Table的异步函数就会被放进一个队列中,这个队列称为Event Queue。
不要觉得这个队列很深奥,其实就是一个排队,里面放的都是回调函数,它们正一个个等待着按顺序执行自身呢。来看下面的代码:


​console.log('程序执行开始~')

setTimeout(() => {

console.log('setTimeout执行啦~')

}, 3000);

sleep(5000);

console.log('程序执行结束~')

// 注:这里的sleep函数不是js的标准函数,只是表示一个执行需要5秒的函数。

// 输出结果:

// 程序执行开始~

// ...5s后

// 程序执行结束~

// setTimeout执行啦~

从上面结果我们可以看出,setTimeout并非是在setTimeout调用之后经过3秒就马上输出结果"setTimeout执行啦~",而是等待下方的sleep函数执行完毕后才输出的结果。
前面我已经说过,要牢记javascript是单线程,那么它就一次只能运行一个一段代码。
因此,即使处于异步队列的setTimeout函数已经满足执行条件了,但是它还是得等待在Event Queue中,直到主线程执行完毕才能执行。
所以请记住,js会先执行主线程的同步代码,遇到setTimeout就将其回调函数注册在Event Table中,然后当异步函数满足执行条件之后,就会被放入Event Queue中,但是并不能马上执行,而是得等待主线程剩余代码执行完毕,队列中的函数才能按顺序执行。
我想小伙伴们看到这里,已经明白一点js的执行机制了,那么我们一鼓作气,继续深入一下(其实也很简单),Promise和process又是怎样的执行机制呢?
在放代码之前,我先介绍两个基本概念:

  • process.nextTick,我们知道浏览器环境下的setTimeout,那么process.nextTick就相当于在node环境下执行的setTimeout。
  • 宏任务和微任务,主线程一直在执行script代码,还有setTimeout、setInterval函数就是宏任务,而Promise.then,process.nextTick则是微任务。
    接下来,我们看一段代码:

console.log('程序执行开始~')

setTimeout(() => {

console.log('setTimeout执行啦~')

}, 3000);

new Promise((resolve) => {

  console.log('promise开始执行~');

  resolve(); 

}).then(() => {

  console.log('promise执行结束~')

});

console.log('程序执行结束~')

// 输出结果:

// 程序执行开始~

// promise开始执行~

// 程序执行结束~

// promise执行结束~

// ...3s后

// setTimeout执行啦~

emmm,这里的结果是不是就有点微妙了。
记得我们刚才说的宏任务和微任务吗,js的执行机制中,先是执行完宏任务中的同步代码,接着执行微任务,接着执行宏任务的异步代码。这样说可能有点绕,我们结合上面的代码来看
* 代码一开始执行,执行的就是全局代码,也就是宏任务的同步代码;
* 遇到console.log,直接执行,输出"程序执行开始~";
* 接着执行,遇到setTimeout函数,将其回调函数注册进宏任务的Event Queue(注意:宏任务和微任务分别有自己的Event Queue);
* 接着遇到new Promise,立刻执行(new Promise里面的函数是立刻执行的,只有.then函数里面才是放到微任务去执行的,不要搞混咯),输出"promise开始执行";
* 接着遇到promise.then函数,将其回调函数注册到微任务的Event Queue;
* 接着继续执行,遇到console.log,直接输出"程序执行结束~"
* 到这里,宏任务的同步代码就全部执行完毕了,这时候,js引擎会去检查微任务的Event Queue中是否存在回调函数,这时微任务的Queue中还有一个函数未执行,因此在这时候执行,输出"promise执行结束~";
* 当微任务的所有回调函数被执行完了之后,一次事件循环就结束了。
* 这时候js引擎会检查宏任务的Event Queue中是否还有未执行的函数,如果还有,将会开启下一轮的事件循环。由于此时我们宏任务的Event Queue中还有未执行的setTimeout,所以开启下一轮事件循环,执行setTimeout回调,输出"setTimeout执行啦~"
能坚持到这里的小伙伴,相信你已经学到了不少,给自己点个赞吧
接下来,我们再来看一下加上process.nextTick之后的一个例子:


console.log('1')

setTimeout(() => {

console.log('2')

})

new Promise((resolve) => {

console.log('3')

    resolve()

}).then(() => {

console.log('4')

})

process.nextTick(() => {

console.log('5')

})

new Promise((resolve) => {

console.log('6')

    resolve()

}).then(() => {

console.log('7')

})

process.nextTick(() => {

console.log('8')

})

console.log('9')

// 输出结果

// 1 3 6 9 5 8 4 7 2

是不是有一点一开始那块代码的味道了,上面的输出结果也很容易理解:先是执行了同步代码,输出:1 3 6 9,然后输出微任务中的process.nextTick的回调:5 8,然后输出Promise.then中的回调:4 7,最后输出setTimeout的2,是不是一目了然。
上面唯一要注意点的就是:process.nextTick是要比Promise.then先执行的(也许不同node版本环境下不同,这个要看具体执行结果)。
好啦!终于这篇文章也要接近尾声了,还在看的小伙伴再给自己点个赞吧,当然也可以给我点个赞~你每一个小小的支持都是我坚持下去的最大动力。
接下来要进入最后的重头戏,按照我们前面所讲的知识,分析刚开始的代码的执行结果。这里再贴下一开始的代码,最终结果我会在文章最后再贴出来,所以小伙伴们也可以自己先看下,最后比对结果是否和文中的一致。


console.log('1');

setTimeout(() => {

console.log('2');

    process.nextTick(() => {

console.log('3');

    })

new Promise((resolve) => {

console.log('4');

        resolve();

    }).then(() => {

console.log('5')

    })

});

process.nextTick(() => {

console.log('6');

})

new Promise((resolve) => {

console.log('7');

    resolve();

}).then(() => {

console.log('8')

});

setTimeout(() => {

console.log('9');

    process.nextTick(() => {

console.log('10');

    })

new Promise((resolve) => {

console.log('11');

        resolve();

    }).then(() => {

console.log('12')

    })

});

接下来是分析过程:

  • 程序开始,执行宏任务同步代码,遇到console.log,输出:1;

  • 遇到setTimeout1,将其放入宏任务Event Queue中;

  • 遇到process.nextTick1,放入微任务Event Queue中;

  • 遇到new Promise,直接执行其中的代码,输出:7;

  • 遇到Promise.then1函数,将其放入微任务Event Queue;

  • 继续执行,遇到setTimeout2,放入宏任务Event Queue;

  • 此时任务队列状态:

    • 宏Queue: setTimeout1,setTimeout2;
    • 微Queue: process.nextTick1、Promise.then1;
  • 至此,宏任务同步代码执行完毕,检测微任务队列是否存在任务,由于存在两个微任务,所以这时候执行微任务;

  • 先执行process.nextTick1,输出:6;

  • 接着执行Promise.then1,输出: 8;

  • 微任务执行完毕后,一次事件循环结束,js引擎持续检测宏任务中是否存在任务,存在的话开启下一次事件循环;由于存在两个setTimeout,所以在满足setTimeout执行条件后,开启下一次事件循环,执行回调函数;

  • 先执行setTimeout1,遇到console.log,输出:2;

  • 接着遇到process.nextTick2,放入微任务Event Queue;

  • 继续执行遇到new Promise,直接执行,输出:4;

  • 然后遇到Promise.then2,放入微任务Event Queue;

  • 至此setTimeout1执行完毕,此时任务队列状态:

    • 宏Queue: setTimeout2;
    • 微Queue: process.nextTick2、Promise.then2;
  • js引擎检查微任务Event Queue中还存在两个微任务,因此执行这两个微任务;

  • 先执行process.nextTick2,输出:3;

  • 接着执行Promise.then2,输出:5;

  • 微任务执行完毕,第二次事件循环结束;

  • js引擎持续检查宏任务Event Queue中是否还有未执行函数,检测到还有setTimeout2未执行,因此开启第三轮的事件循环;

  • 执行setTimeout2,遇到console.log,输出:9;

  • 又遇到process.nextTick3,放入微任务队列;

  • 遇到new Promise,直接执行,输出:11;

  • 遇到Promise.then3,放入微任务队列;

  • 至此,setTimeout2执行完毕,此时任务队列状态:

    • 宏Queue: 无;
    • 微Queue: process.nextTick3、Promise.then3;
  • js引擎在检测是否存在未执行的微任务,由于还有两个微任务未执行,因此将其执行;

  • 先执行process.nextTick3,输出:10;

  • 接着执行Promise.then3,输出:12;

  • 至此,微任务执行完毕,事件循环结束;
    最后程序输出结果:1 7 6 8 2 4 3 5 9 11 10 12

    看到这里的小伙伴们,给自己点第三个赞吧。
    怎么样,是不是觉得已经完全掌握了js的执行机制,其实宏任务和微任务除了上文提到的那些,还有一些其他的,可以下来自己再去了解下~
    最后,感谢大家的阅读,如果觉得文章写的还可以的话,可以给我点个赞、点个关注、或者直接关注本人,我会持续分享更多优质的技术文章,我们一起加油吧!

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