前端面试题整理-前端异步编程
1. 进程、线程、协程的区别
在并发编程领域,进程、线程和协程是三个核心概念,它们在资源管理、调度和执行上有着本质的不同。
首先,进程是操作系统进行资源分配和调度的独立单位(资源分配基本单位),每个进程拥有自己的内存空间,这使得进程间相互隔离,从而提高了系统的稳定性。然而,这种隔离也带来了进程间通信的复杂性,需要通过特定的机制如管道、共享内存等来实现。进程的创建和销毁涉及到系统资源的分配和回收,因此开销相对较大。适用于需要高隔离性和独立资源的应用,比如浏览器的多进程架构,每个标签页运行在独立的进程中。
接着,线程作为进程的执行单元(CPU调度基本单位),共享进程的资源,包括内存空间,这使得线程间的通信更为简便。线程的创建和销毁开销相对较小,但仍然需要操作系统的调度,因此线程的并发执行能力受到操作系统调度策略和硬件资源的限制。适用于需要高效并发处理的场景,如Web服务器的请求处理、前端的异步操作(如Web Workers)。
最后,协程是一种更轻量级的并发机制,它允许程序在不同的执行点挂起和恢复,通常由程序内部进行调度,而不是依赖操作系统。协程特别适合处理I/O密集型任务,因为它们可以在等待I/O操作时挂起,从而提高资源利用率。协程的创建和销毁开销非常小,这使得它们在处理大量并发任务时非常有用,尤其是在Python、Go等语言中,协程被广泛用于提高并发性能。适用于需要大量并发但不需要多线程的场景,如JavaScript中的异步编程(Promise、async/await)、Python的异步I/O。
2. 说说他们的通信方式
进程、线程和协程之间的通信方式各有特点,下面分别介绍它们的通信机制:
(1) 进程通信(Inter-Process Communication, IPC):
- 管道(Pipes):允许一个进程的输出成为另一个进程的输入。常用于父子进程间的单向或双向通信,简单且高效。
- 命名管道(Named Pipes):类似于管道,但是它们在文件系统中有名字,可以跨会话使用。
- 消息队列(Message Queues):允许进程以消息的形式交换数据,消息被存储在队列中直到被接收。支持异步通信,允许消息在进程间传递,适合复杂的消息传递场景。
- 信号(Signals):一种由操作系统提供的异步通信机制,用于通知进程某个事件已经发生,适合简单的通知机制。
- 共享内存(Shared Memory):允许两个或多个进程共享一个给定的存储区。速度最快的通信方式,进程共享一块内存区域,但需要同步机制来避免竞争条件。
- 套接字(Sockets):支持不同主机之间的进程通信,可以是TCP/IP或UDP/IP协议。不仅用于网络通信,也可以用于本地进程间通信,灵活且强大。
- 信号量(Semaphores)和互斥锁(Mutexes):用于控制对共享资源的访问,防止多个进程同时访问同一资源。
(2) 线程间通信:
- 共享内存:由于线程共享相同的内存空间,它们可以直接通过读取和修改共享变量来通信,效率高。
- 互斥锁(Mutexes):用于同步线程对共享资源的访问,防止数据竞争。
- 条件变量(Condition Variables):允许线程在某个条件为真之前挂起,并在条件变为真时被唤醒。线程可以等待特定条件满足后继续执行,适合复杂的同步场景。
- 信号量(Semaphores):用于控制对共享资源的访问,也可以用于线程间的同步,适合计数资源的同步。
- 屏障(Barriers):同步机制,用于等待一定数量的线程都到达某个点后再继续执行。
(3) 协程间通信:
- 通道(Channels):在支持协程的语言中(如Go),通道是一种同步通信机制,允许协程通过发送和接收数据来通信,通常用于协程间的同步和数据交换。类似于管道,用于协程间传递消息或数据,直观且易于使用。
- 共享变量:类似于线程,协程也可以通过共享变量来通信,但需要小心处理同步问题,以避免竞态条件。
- 事件循环(Event Loop):在JavaScript中,协程(如通过
async/await
实现的异步函数)通常依赖于事件循环来管理异步操作,事件循环负责调度和执行异步任务。 - Future/Promise:用于处理异步操作的结果,提供一种优雅的方式来处理协程间的结果传递。
3. js 是线程还是进程
在浏览器环境中,JavaScript 是单线程的。它在一个线程中执行代码,处理事件和更新用户界面。JavaScript 使用事件循环来管理异步操作。事件循环允许 JavaScript 处理异步任务,如网络请求或定时器,而不会阻塞主线程。虽然 JavaScript 本身是单线程的,但可以通过 Web Workers 创建多线程环境。Web Workers 允许在后台线程中运行代码,从而避免阻塞主线程。这种设计使得 JavaScript 能够高效地管理用户界面交互和后台任务。
4. js 事件循环机制
原理
JS是单线程的,为了防止代码阻塞,把任务分成同步任务和异步任务。
同步任务放入JS引擎直接执行,原地等待结果,其任务放入执行栈中按顺序执行;而异步任务需要放入宿主环境(浏览器、Node),不用原地等待结果,比较耗时,其任务放在任务队列中。
首先,执行栈中的同步任务会按顺序执行。当执行栈为空时,事件循环会检查任务队列。执行所有的微任务。如果调用栈为空,则从任务队列中取出一个宏任务并执行。执行完毕后,事件循环再次检查任务队列,重复这个过程。
在 JavaScript 中,任务可以分为同步任务、宏任务(Task),和微任务(Microtask)。以下是一些常见的例子:
同步任务
这些任务会立即执行,按顺序依次完成,不会被中断。
- 普通的变量声明和赋值
- 函数调用
console.log
宏任务(Task)
这些任务会被添加到任务队列中,等到主线程空闲时执行。
- 整个脚本(初始执行)(指从头到尾执行一段 JavaScript 代码。它是事件循环中第一个被处理的宏任务)
setTimeout
setInterval
- I/O 操作(读取文件或发送网络请求)
- UI 渲染事件
setImmediate
(Node.js)
微任务(Microtask)
这些任务会在当前宏任务执行结束后立即执行,在下一个宏任务开始前完成。
Promise
的回调函数(then
,catch
,finally
)process.nextTick
(Node.js)MutationObserver
async/await
,await
之后的代码都看作微任务!(因为 await 会暂停函数的执行,并让出事件循环。在 await 的 Promise 解决后,函数会继续执行,后续代码作为微任务在当前宏任务完成后执行。)
执行顺序
- 执行同步任务。
- 当前宏任务执行完毕后,执行所有微任务。
- 执行下一个宏任务。
这种机制确保了同步任务优先执行,微任务在宏任务之间迅速处理,保持高效的异步操作。
举例
(1) 同步函数执行顺序
对于同步函数 f1()
和 f2()
:
function f1(){
for(let i=0; i<200; i++){}
}
function f2(){
for(let i=0; i<300; i++){}
}
f1();
f2();
执行顺序是严格按照代码的顺序进行的:
f1()
完成后才会执行f2()
。- 因为它们都是同步代码,所以没有异步调度的干扰。
(2) 异步函数执行顺序
对于异步函数 promise1()
和 promise2()
:
async function promise1(){
await xx;
console.log(1);
}
async function promise2(){
await xx;
console.log(2);
}
promise1();
promise2();
假设 xx
是一个返回 Promise 的操作,执行顺序如下:
promise1()
和promise2()
被调用,返回的 Promise 进入微任务队列。- 在
await xx
处,函数会暂停,控制权返回到事件循环。 - 一旦主线程的同步代码执行完毕,事件循环会处理微任务队列中的任务。
await xx
完成后,console.log(1)
和console.log(2)
分别被放入微任务队列。- 因为
promise1()
先调用,所以console.log(1)
会先执行,接着是console.log(2)
。
因此,输出结果是:
1
2
await
会暂停函数的执行,直到 Promise 解决为止。async/await
使得异步代码看起来像同步代码,但实际执行顺序依赖于事件循环和微任务队列。- 微任务(例如
await
的处理)会在当前宏任务结束后立即执行。
(3) 混合任务
async function promise1() {
console.log('A');
await Promise.resolve(console.log('C'));
console.log(1);
}
async function promise2() {
console.log('B');
await Promise.resolve(console.log('D'));
console.log(2);
}
promise1();
promise2();
在这个代码片段中:
同步任务:
console.log('A')
console.log('C')
console.log('B')
console.log('D')
这些会按照顺序立即执行。
微任务:
console.log(1)
(作为promise1
中的await
后的代码)console.log(2)
(作为promise2
中的await
后的代码)
这些会在当前宏任务完成后,作为微任务执行。
宏任务:
- 每个
promise1()
和promise2()
的调用都是一个宏任务,但在这个上下文中,主要关注的是事件循环的执行顺序。
执行顺序:
- 同步任务先执行,打印
A
、C
、B
、D
。 - 然后执行微任务队列中的
console.log(1)
和console.log(2)
。
5. js 中的 async / defer
属性?平时用那种方式多?(参考掘金《「2021」高频前端面试题汇总之HTML篇》)
其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。
- 没有
defer
和async
: 加载和执行都会阻塞html
的解析
defer
和 async
属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,区别:
defer
: 异步加载JS文件,不会立即执行,不阻塞html
解析,会在解析完成后按顺序执行async
: 异步加载和执行JS文件,不会阻塞html
解析,下载完就执行,没有执行顺序
平时用那种方式多?
用defer
多。使用 defer
进行优化,不阻塞 html
解析;执行会按照顺序来,保证了正确性。
6. 说说异步编程中的 async / await
-
async
: 用于定义一个异步函数,函数返回一个Promise
。即使没有显式返回Promise
,函数也会自动将返回值包装成一个Promise
。 -
await
: 用于暂停异步函数的执行,等待一个Promise
完成,并返回其解析值。await
只能在async
函数中使用。
用法:async function fetchData() {
try {
const response = await fetch(‘https://api.example.com/data’);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(‘Error:’, error);
}
}
工作原理:
- 当函数被调用时,会立即返回一个
Promise
。 - 函数内部遇到
await
时,会暂停执行,直到Promise
解决或拒绝。 await
后的表达式会被转换为Promise.resolve()
。
优势:
- 简化代码: 使异步代码看起来更像同步代码,易于理解和维护。
- 错误处理: 可以使用
try/catch
语句来处理异步操作中的错误。
注意事项:
await
只能在async
函数中使用。- 多个
await
操作会导致顺序执行,可能影响性能。可以使用Promise.all()
来并行执行多个异步操作。
示例对比:
Promise then/catch:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Async/Await:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
结论:
async/await
是基于 Promise
的语法糖(在编程语言中,为了提高可读性和简洁性而提供的语法特性),使得异步代码更简洁和易读,是现代 JavaScript 开发中的重要工具。
7. await
会阻塞js线程加载执行吗
await 不会阻塞 JavaScript 线程的执行。它只是暂停当前异步函数的执行,等待 Promise 完成(被解决或拒绝)。在此期间,JavaScript 事件循环仍然可以处理其他任务,如用户交互、渲染等。
async function example() {
console.log('Start');
const result = await new Promise(resolve => setTimeout(() => resolve('Done'), 1000));
console.log(result);
}
example();
console.log('This runs while waiting');
// 输出
Start
This runs while waiting
Done
8. 说说web worker
待看文章:一文彻底学会使用web worker
什么是 Web Worker
Web Worker 是一种在浏览器中创建后台线程的方法,用于执行复杂或耗时的 JavaScript 任务,而不会阻塞主线程。这有助于保持用户界面的流畅性。
为什么使用 Web Worker?
- 提高性能: 在处理密集计算任务时,Web Worker 可以防止页面卡顿。
- 增强用户体验: 通过避免长时间的 UI 阻塞,用户界面可以保持响应。
Web Worker 的使用场景
- 复杂计算: 比如图像处理、数据分析、加密操作等。
- 长时间运行任务: 如大数据处理、文件解析等。
示例
在一个项目中,我需要对用户上传的图片进行滤镜处理。由于处理过程较为复杂,我使用了 Web Worker 来避免阻塞主线程。
// worker.js
self.onmessage = function(event) {
const imageData = event.data;
// 进行复杂的图像处理
const processedData = applyFilter(imageData);
self.postMessage(processedData);
};
// 主线程
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
const processedData = event.data;
// 显示处理后的图像
displayImage(processedData);
};
worker.postMessage(originalImageData);
注意
- 无法访问 DOM: Web Worker 不能直接操作 DOM。
- 通信成本: 主线程和 Worker 之间通过消息传递进行通信,可能会有一定的延迟。