Hello,大家好,我是 Sunday。
【看代码说结果】一直是前端面试中的常见问题。最近在陪几个同学面试过程中,几乎每个中、大厂的面试都会遇到一个或几个这样的问题。
虽然这样的问题如此高频,但是能够回答好的同学却寥寥无几。
每次事后跟同学沟通,得到的结果都是:“实际开发中没有这么写的,NND 奇葩面试题!” 大家是不是也会有相同的感受呢?
是的!实际开发中我们肯定不会写出面试题里的凌乱场景。但是,我们不要忘记,学习的目的是:为了拿到更高薪资的 offer!,所以对很多同学而言 面试 比 实际开发 更重要! 只有很好的解决了 面试 的问题,大家才可以拿到满意的 offer。
所以,解决【看代码说结果】的问题就变得至关重要了。那么咱们今天,就好好地来聊聊 JS 中的执行机制问题,帮大家彻底理解 JS 的执行逻辑!
关于 JavaScript 线程基础逻辑
JavaScript 是一种单线程语言。
虽然最新的 HTML5 中引入了 Web Worker,但 JavaScript 单线程的核心保持不变。
因此,JavaScript中所有的“多线程”都是用单线程模拟的,JavaScript中的所有多线程都是骗人的!
JavaScript 事件循环
由于 JavaScript 是单线程的,它就像一家只有一个窗口的银行,客户需要一一排队来处理交易。
同样,JavaScript 任务也需要按顺序执行,一个接一个。如果一项任务花费太长时间,则下一项任务必须等待。
那么问题就来了:如果我们想浏览新闻,但新闻中的高清图片加载缓慢,我们的网页是否要一直卡住,直到图片完全显示出来?
因此,JS将任务分为两类:
- 同步任务
- 异步任务
当我们打开一个网站时,网页的渲染过程由一堆同步任务组成,例如:骨架屏幕、页面元素。
消耗大量资源且需要很长时间才能完成的任务(例如:加载图像、音乐文件)则是异步的。
图片
- 同步和异步任务进入不同的执行“地方”,同步任务进入主线程,异步任务进入事件表并注册函数。
- 当指定的任务完成时,事件表会将这个函数移动到事件队列中。
- 主线程内的任务完成后,会从Event Queue中读取相应的函数并在主线程中执行。
- 上述过程会不断重复,通常称为事件循环。
那么 JS 是如何知道主线程为空的呢?
在 JavaScript 引擎有一个监控进程,不断检查主线程执行栈是否为空。一旦为空,它就会去事件队列检查是否有任何函数正在等待调用。
如下面的代码所示:
let data = [];
$.ajax({
url:www.lgdsunday.club,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行完成');
上面是一个简单的ajax请求代码:
- ajax进入事件表并注册回调函数success。
- 执行console. log(‘发送成功!’)。
- ajax事件完成,回调函数成功进入事件队列。
- 主线程success从事件队列中读取并执行回调函数。
通过上面的文字和代码,大家应该对JavaScript中的执行顺序有了初步的了解了吧。
那么接下来咱们来看一个 扰乱执行顺序的 “元凶” setTimeout。
万恶的 setTimeout
setTimeout 可以延迟执行代码,比如:
setTimeout(() => {
task();
},3000)
console.log('一个普通的打印');
根据我们之前的结论,setTimeout是异步的。所以,同步任务console.log应该先执行。因此,我们的结论是:
// 一个普通的打印
// task()
但是,这里我们要注意 3000 毫秒并不是 task 的执行时间,而是 task 进入任务队列(主线程)的时间
- 3秒后,计时事件timeout完成。
- task()进入任务队列(主线程)
那么同样的道理,在面试中常见的 setTimeout(fn, 0) 的延迟 0 毫秒 是什么意思呢?
setTimeout(fn ,0)是指定当堆栈中的所有同步任务完成且堆栈变空时,应在主线程上最早可用的空闲时间执行某个任务,而不需要等待任何额外的秒数。
所以,setTimeout(fn, 0) 并不会立刻执行。
宏任务与微任务
宏任务与微任务的概念在这种题目中也是必须要掌握的。
- 宏任务:包括整体脚本代码、setTimeout、setInterval
- 微任务:Promise、process.nextTick
事件循环中事件的顺序决定了JavaScript代码的执行顺序。
- 输入整个脚本(宏任务)后,它开始第一个循环
- 然后它执行所有微任务。接下来,又从宏任务开始,直到一个任务队列完成后,才再次执行所有的微任务
我们通过一段代码来看下这个问题:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务,进入主线程。
- 当遇到 setTimeout 时,其回调函数被注册并调度到宏任务事件队列中。 (注册流程同上,下文不再赘述)。
- 接下来,当遇到 Promise 时,new Promise立即执行,并将该then函数分派到微任务事件队列中。
- 当遇到 console.log() 时,立即执行。
- 在将整个脚本作为第一个宏任务执行之后。我们发现它 then 位于微任务事件队列中并被执行。
- 第一轮事件循环结束。
- 第二轮循环开始;当然是从宏任务Event Queue开始。队列中对应setTimeout的回调函数立即被执行。
- 结尾。