35

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

 3 years ago
source link: https://mp.weixin.qq.com/s/hgjNC9YKlbbMSWOg8atFXQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
bmmQzqU.gif

点击前端充电营,关注我们

32Mbqaq.gif

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

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的执行机制的话,上面的题目可能会让你崩溃。

不过别着急,先往下看,我保证你看到最后,能轻轻松松写出上面代码的答案,并且完全了解其中的原理。

zIniMfU.gif

首先,希望大家记住一个要点, 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执行啦~ "

能坚持到这里的小伙伴,相信你已经学到了不少,给自己点个赞吧

ZJzAf2r.jpg!web

接下来,我们再来看一下加上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的执行机制,其实宏任务和微任务除了上文提到的那些,还有一些其他的,可以下来自己再去了解下~

最后,感谢大家的阅读,如果觉得文章写的还可以的话,可以给我点个赞、点个关注、或者直接关注本人的公众号:" 前端充电营 ",我会持续分享更多优质的技术文章,我们一起加油吧!

uqyQRnJ.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK