面试题
js
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
大家看看上面这道题!能告诉我输出结果是什么吗?
如果你还不能确定,那么学完今天的内容,你就知道这道题该怎么做了!我们会在今天的结尾为大家揭示答案,及分析过程!
那么,接下来就开始我们今天的学习吧!
一、进程和线程
1、什么是进程?
在计算机科学中,进程是指在系统中运行的程序的实例。它是计算机中的一个独立执行单元,包含了程序代码、数据以及程序的执行状态。每个进程都有其独立的内存空间,不同进程之间一般是相互隔离的,这样可以确保它们不会直接干扰彼此的执行。
就好比你打开电脑的任务管理器,你看到一个又一个的程序就是你的电脑的进程!也可以理解为CPU运行指令和保存上下文所需的时间!
2、什么是线程?
进程中的更小的单位,描述了一段指令执行所需的时间,在计算机科学中,线程是指进程内的一个执行单元,它包含了执行程序的代码和相关的上下文信息。线程是进程的一部分,一个进程可以包含多个线程,这些线程共享相同的资源,如内存空间和文件句柄。线程之间的切换相对于进程切换来说更加轻量级,因为它们共享相同的地址空间。
有一点我们要记住一点JS引擎线程和浏览器的渲染进程是互斥的,会造成不安全的渲染!
面试题:打开一个tap页面,输入一个url回车到页面展现的过程。问:这中间发生了什么?
我们新开的一个页面,就是新开一个进程,需要多个线程配合才能完成页面的展示,其中有很多的细节,
大致可以分为三步:1、渲染线程(GPU) ,2、http请求进程,3、JS引擎线程
**什么是GPU?**GUP它是属于浏览器的,具有绘制功能,将页面上的展示绘制出来!就比如找到哪个物理发光点 应该亮什么灯,我们可以把它理解成一个画笔,作用就是把整个页面画出来。
3、JS是单线程
JavaScript语言是一门单线程语言,这意味着,在同一时间只能执行一个任务。
正是因为JS单线程的这种特点,给它带来一些优点!
- 简单性: 单线程模型使得编写和调试JavaScript代码相对简单。开发人员不必担心多线程的同步和竞态条件问题,这降低了代码的复杂性。
- 避免竞态条件: 多线程环境下,如果没有正确处理同步,可能会导致竞态条件(Race Condition)。单线程避免了这种情况,因为在任何给定时刻只有一个任务在执行。
- 前端开发中的简便性: 在Web开发中,JavaScript主要用于操作DOM、处理用户交互等,这些任务通常不需要多线程。单线程的模型对于处理这些任务是足够的,并且简化了前端开发的复杂性。
- 更好的可控性: 单线程模型使得代码的执行流程更加可控。事件循环(Event Loop)机制确保任务按照特定的顺序执行,使得开发者更容易理解代码的执行流程。
- 资源节省: 单线程模型节省了在多线程切换上的一些开销。在多线程环境中,线程的切换可能会引入额外的开销,而单线程模型避免了这个问题。
- 避免死锁: 多线程应用程序中,死锁是一个潜在的问题,可能会导致程序无响应。单线程模型避免了这个问题,因为只有一个执行线程。
但是,在某些情况下,也可能导致性能问题,特别是在处理大量计算或需要等待的异步任务时。为了克服这些限制,JavaScript使用异步编程模型,允许开发者利用单线程的同时,通过异步操作提高并发性。
有人就要问了,为什么JS不设计成多线程的?
这是因为JS设计之初是打算设计成浏览器的脚本语言,多线程 高并发高功耗,多线程运行的时候会多占一些内存,是为了遏制同时干多个事情的能力,节约进程内存的开销。
接下来,我们就为大家介绍JavaScript中异步 编程!(我们在之前的文章,为大家初介绍过异步和它的Promise处理机制:[ES6]新方法--Promise 手搓异步,拳打回调 ,拿捏同步 - 掘金 (juejin.cn))
二、JavaScript中的异步
JavaScript异步编程中存在两个概念!宏任务和微任务!
1、宏任务(异步很凶的代码)
- 整体的代码(Script): 整体的 JavaScript 代码作为一个宏任务执行。
- 定时器事件(setTimeout、setInterval): 设置定时器的回调函数会作为一个宏任务执行。
- 事件监听器(Event Listeners): 通过事件监听器绑定的回调函数会作为宏任务执行。
- I/O 操作: 执行某些 I/O 操作时,例如文件读写、网络请求,会作为宏任务执行。
- 用户交互事件: 用户的交互事件,例如点击、键盘输入,会触发相应的回调函数,作为宏任务执行。
- UI-rendering: 页面渲染 重要
2、微任务
- Promise 的回调函数: Promise 的
then
和catch
方法中的回调函数会作为微任务执行。 - MutationObserver: 使用 MutationObserver 监听 DOM 变化的回调函数会作为微任务执行。
- process.nextTick(Node.js): 在 Node.js 中,
process.nextTick
中的回调函数会作为微任务执行。 - queueMicrotask:
queueMicrotask
函数添加的回调函数会作为微任务执行。 - Object.observe(已废弃):
Object.observe
中的回调函数会作为微任务执行(注意:Object.observe
已被废弃)。
我们了解这两个概念有什么用呢?
接下来就介绍我们今天的应用!
三、事件循环机制event-loop 面试必考
事件循环机制的步骤如下:
- 执行同步代码(这属于宏任务)
- 当执行栈为空,去查询是否有异步代码需要执行
- 执行微任务
- 如果有需要,会渲染页面d
- 执行宏任务,(这也叫下一轮的event-loop的开启)
接下来我们看看这个案例:
js
let a = 1
console.log(a);
// v8决定定时器耗时
setTimeout(()=>{
console.log(a);
},1000)
let b = 2
//for循环是由我们CPU说要耗时的,v8眼里它是不耗时的
for(let i = 0;i<10000;i++)
{
console.log(b);
}
在这个案例当中,在浏览器引擎眼里,for
循环是不会耗时的,但是我们的CPU运行需要耗时,我们假设for
执行完需要1s,那么,我们的setTimeout
需要耗时多少?答案是2s!for
执行完的时间由我们的CPU决定,它是一个同步代码。
你也许会疑惑,这里也没有用到事件循环机制阿?对!那我们用事件循环机制来分析下面这个案例
js
console.log('starting');
setTimeout(()=>{
console.log('settimeout');
setTimeout(()=>{
console.log('inner');
})
console.log('end');
},1000)
new Promise((resolve,reject)=>{
console.log('promise');
resolve()
})
.then(()=>{
console.log('then');
})
.then(()=>{
console.log('then2');
})
.then(()=>{
console.log('then3');
})
这个案例的执行结果是什么?
我们后台数据存在两个队列!
我们开始分析这个案例:
浏览器从上往下执行,根据事件循环机制的步骤:
第一轮事件循环机制
一、执行同步代码
1、第一行console.log('starting');
这是一个同步代码,直接执行!
2、往下是第一个定时器,我们把它推入到宏任务队列:宏任务队列:setTimeout
3、然后来到new Promise
这个语句是一个同步代码,我们执行它的内部:console.log('promise');
同步代码,输出promise
,执行 resolve()
4、第一个.then
,这个是微任务,推入到微任务队列当中(我们用数字[1]表示先后,区别相同的标签):微任务队列:.then()[1]
5、第二个.then
,这个是微任务,推入到微任务队列当中:微任务队列:.then()[2],.then()[1]
6、第三个.then
,这个是微任务,推入到微任务队列当中:微任务队列:.then()[3],.then()[2],.then()[1]
二、当执行栈为空,去查询是否有异步代码需要执行(意思就是如果没有同步代码需要执行了,就接着往下走)
三、执行微任务
1、获取微任务队列微任务队列:.then()[3],.then()[2],.then()[1]
,我们知道队列是先进先出,所以我们从头开始执行!
2、执行.then()[1]
,输出then
此时微任务队列:微任务队列:.then()[3],.then()[2]
3、执行.then()[2]
,输出then2
此时微任务队列:微任务队列:.then()[3]
4、执行.then()[3]
,输出then3
此时微任务队列:微任务队列:
四、如果有需要,会渲染页面d
五、执行宏任务,(这也叫下一轮的event-loop的开启)
1、获取宏任务队列:宏任务队列:setTimeout
2、新的一轮事件循环:宏任务队列:
第二轮事件循环
一、执行同步代码
1、console.log('settimeout');
同步代码直接输出settimeout
2、setTimeout
,推入宏任务队列:宏任务队列:setTimeout
3、console.log('end');
同步代码直接输出end
二、当执行栈为空,去查询是否有异步代码需要执行(意思就是如果没有同步代码需要执行了,就接着往下走)
三、执行微任务(没有微任务)
四、如果有需要,会渲染页面d
五、执行宏任务,(这也叫下一轮的event-loop的开启)
1、获取宏任务队列:宏任务队列:setTimeout
2、新的一轮事件循环:宏任务队列:
第三轮事件循环
一、执行同步代码
1、console.log('inner');
同步代码,直接输出:inner
由于没有代码了,后续内容结束!
总结下来,我们的输出应该是:
js
输出:
starting
promise
then
then2
then3
settimeout
end
inner
没错!这就是我们想要的答案!
值得注意的是!
我们看看这个案例:
js
function a(){
setTimeout(()=>{
console.log('a');
},1000)
}
function b(){
setTimeout(()=>{
console.log('b');
},500)
}
a()
b()
//输出:
//b
//a
为什么这里是先输出b
再输出a
呢?
这是因为setTimeout
定时器比较特殊,在宏队列中,几乎是同步执行,时间短的先输出,像上述案例,执行完成总共花费的时间为1s
好了学到这里,你也就基本懂了事件循环机制,接下来为解决我们今天的"大人物"面试题而准备吧!
async函数
我们参考MDN官方文档介绍!async 函数 - JavaScript | MDN (mozilla.org)
async 函数是使用async
关键字声明的函数。async 函数是 AsyncFunction
构造函数的实例,并且其中允许使用 await
关键字。async
和 await
关键字让我们可以用一种更简洁的方式写出基于 Promise
的异步行为,而无需刻意地链式调用 promise
。
返回值
一个 Promise
,这个 promise 要么会通过一个由 async 函数返回的值被解决,要么会通过一个从 async 函数中抛出的(或其中没有被捕获到的)异常被拒绝。
描述
async 函数可能包含 0 个或者多个 await
表达式。await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。使用 async
/await
关键字就可以在异步代码中使用普通的 try
/catch
代码块。
备注: await
关键字只在 async 函数内有效。如果你在 async 函数体之外使用它,就会抛出语法错误 SyntaxError
。
备注: async
/await
的目的为了简化使用基于 promise 的 API 时所需的语法。async
/await
的行为就好像搭配使用了生成器和 promise。
async 函数一定会返回一个 promise 对象。如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。
面试题答案
js
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
我们拿到面试题,然后进行分析。
根据我们的事件循环机制进行分析!
第一轮事件循环
一、执行同步代码
1、console.log('script start')
同步代码,直接输出 script start
2、async函数async1
和async2
的声明,不用管
3、async1()
函数调用,进入函数内部,碰到await async2()
,原本我们会把它和它之后当作微任务放入微任务队列当中,现在它只会把它后面的代码放入微任务当中,而它接着的代码,被强行提前 !所以这里会先调用async2
,而console.log('async1 end')
进入微任务队列:微任务队列:console.log('async1 end')
,而async2()
会立即调用
4、紧接着第三步,进入async2
函数,里面没有awiat
,console.log('async2 end')
为同步代码,输出 async2 end
5、跳出函数调用,往下执行,一个定时器的声明。加入宏任务:宏任务队列:setTimeout[1]
6、new promise
为同步代码,执行里面的逻辑,console.log('Promise')
输出 Promise
,再调用resolve()
7、.then
,推入微任务队列:微任务队列:.then[1],console.log('async1 end')
8、又一个.then
,推入微任务队列:微任务队列:.then[2],.then[1],console.log('async1 end')
9、console.log('script end')
同步代码,输出 script end
二、当执行栈为空,去查询是否有异步代码需要执行(意思就是如果没有同步代码需要执行了,就接着往下走)
三、执行微任务
1、获取微任务队列:微任务队列:.then[2],.then[1],console.log('async1 end')
2、开始执行,输出 async1 end
然后微任务队列:.then[2],.then[1]
3、再执行.then[1]
,内部有一个输出代码console.log('promise1')
输出 promise1
此时微任务队列:.then[2]
4、执行.then[2]
,内部有一个输出console.log('promise2')
,输出 promise2
,此时微任务队列:
四、如果有需要,会渲染页面d
五、执行宏任务,(这也叫下一轮的event-loop的开启)
1、获取宏任务队列:宏任务队列:setTimeout
2、新的一轮事件循环:宏任务队列:
第二轮事件循环
d定时器当中只有一个console.log('setTimeout')
直接输出 setTimeout
输出结束!!
最终结果为:
js
输出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
这样,我们这道面试题就解决啦!!是不是很简单呢?
不知道大家,看完这篇文章有没有对事件循环机制有一定的理解!希望大家看完能够有所收获!
有哪里不懂或者有异议欢迎大家再评论区留言!!
Coding不易,点个小小的赞鼓励支持一下吧!🥺🥺🥺