目录
1. ES6
1.1. promise.all、promise.allSettled、promise.race对比
Promise.all()
, Promise.allSettled()
, 和 Promise.race()
都是 JavaScript 中用于处理多个 Promise 的实用函数,但它们各自有不同的行为和用途。下面是对这三个函数的对比:
1. Promise.all()
- 功能:
Promise.all()
接收一个 Promise 数组作为参数,返回一个新的 Promise。这个新的 Promise 在所有输入的 Promise 都成功解决时才会解决,返回一个数组,其中的元素是各个 Promise 解决后的值。 - 行为:如果输入的任何一个 Promise 被拒绝 (
rejected
),那么Promise.all()
返回的 Promise 会立即被拒绝,且不会等待其他 Promise 完成。这意味着Promise.all()
实际上是有“短路”行为的。 - 使用场景:当你需要确保一系列异步操作全部成功完成时使用,例如,从多个 API 端点获取数据,且只有当所有数据都可用时才继续后续操作。
2. Promise.allSettled()
- 功能:
Promise.allSettled()
同样接收一个 Promise 数组作为参数,但与Promise.all()
不同的是,它会等待所有的 Promise 完成,无论它们是解决还是被拒绝。 - 行为:它返回一个 Promise,这个 Promise 在所有输入的 Promise 都已完成(无论是解决还是被拒绝)时解决,返回一个数组,其中的元素是各个 Promise 的最终状态对象(包括
status
和value
或reason
)。 - 使用场景:当你需要收集一系列异步操作的所有结果,无论它们是否成功,以便进行统一处理或错误管理时使用。
3. Promise.race()
- 功能:
Promise.race()
接收一个 Promise 数组作为参数,返回一个新的 Promise,这个新的 Promise 会在输入的任意一个 Promise 解决或被拒绝时立即解决或被拒绝。 - 行为:它不关心其他 Promise 是否完成,只关注第一个完成的 Promise。如果第一个完成的 Promise 被解决,那么
Promise.race()
返回的 Promise 也会被解决;如果被拒绝,同样如此。 - 使用场景:当你需要在多个异步操作中选择最快完成的那个,或者有一个超时机制时使用。例如,你可能想在一定时间内获取数据,但如果超时则放弃尝试。
概念总结
Promise.all()
用于等待所有 Promise 成功解决。Promise.allSettled()
用于等待所有 Promise 结束,无论结果如何。Promise.race()
用于响应最快完成的 Promise。
1.2 ES6 模块化
ES6(ECMAScript 2015)模块化是一种在JavaScript中组织和管理代码的新方法。在ES6之前,JavaScript没有内置的模块系统,开发者通常会使用一些社区标准如CommonJS(主要用于Node.js环境)、AMD(异步模块定义,用于浏览器环境)或CMD(Sea.js使用的模块定义)来实现模块化。
ES6模块化的主要特性包括:
-
导入和导出(import/export):
export
语句用于导出模块的成员,可以是变量、函数、类或其他表达式。import
语句用于从其他模块导入成员,可以是具体的导出成员或默认导出成员。
-
默认导出与默认导入:
- 每个模块可以有一个默认导出,通过
export default
语句定义。 - 默认导出可以通过任何名字导入,使用
import
语句时不需要加花括号{}
。
- 每个模块可以有一个默认导出,通过
-
命名导出与命名导入:
- 多个成员可以被命名导出,使用
export
关键字前缀。 - 这些成员可以通过带有花括号
{}
的import
语句导入,并且可以重命名。
- 多个成员可以被命名导出,使用
-
静态分析:
- ES6 模块支持静态分析,这意味着在编译阶段就能确定模块之间的依赖关系,以及输入和输出的变量。
-
模块作用域:
- 每个模块都有自己的作用域,这意味着模块内部定义的变量和函数不会泄露到全局作用域。
-
动态导入(虽然不在原始的ES6规范中,但后来被添加):
- 动态导入允许在运行时按需加载模块,使用
import()
函数。
- 动态导入允许在运行时按需加载模块,使用
示例:
假设我们有两个模块文件,math.js
和 app.js
:
math.js
:
export function add(x, y) {
return x + y;
}
export function subtract(x, y) {
return x - y;
}
// 默认导出
export default function multiply(x, y) {
return x * y;
}
app.js
:
import { add, subtract } from './math.js';
import multiply from './math.js'; // 导入默认导出的multiply函数
console.log(add(1, 2)); // 3
console.log(subtract(5, 3)); // 2
console.log(multiply(2, 3)); // 6
ES6 模块化提供了更好的封装性和可维护性,减少了全局命名空间的污染,并使代码更易于理解和调试。在现代的前端开发中,ES6 模块已经成为标准的代码组织方式。在服务器端,如Node.js,也已经完全支持ES6模块。不过需要注意的是,使用ES6模块在Node.js中需要指定文件扩展名,并且从Node.js v14.5.0开始,你可以通过设置"type": "module"
在package.json
中启用对ES6模块的支持。
基础原理
事件循环
JavaScript的事件循环(Event Loop)是其异步处理机制的核心部分。在JavaScript中,所有的执行都是单线程的,这意味着在任何给定的时间点,JavaScript只能做一件事情。然而,JavaScript可以通过事件循环来实现非阻塞的异步操作,这样就不会因为等待I/O操作(如网络请求、文件读写等)而停止执行其他代码。
事件循环主要基于以下两个概念:
-
调用栈(Call Stack):
调用栈是一个后进先出(LIFO)的数据结构,它保存了函数调用的上下文。每当一个函数被调用时,它的上下文就会被压入调用栈,当该函数执行完毕后,它的上下文会从调用栈中弹出。 -
任务队列(Task Queue):
当JavaScript引擎遇到异步操作时,比如setTimeout
或setInterval
,这些操作会被挂起,并将一个回调函数放入任务队列中。一旦异步操作完成,相应的回调函数会被放入任务队列等待执行。
事件循环的工作流程如下:
-
执行上下文创建:
JavaScript引擎开始执行时,会创建全局执行上下文并将其压入调用栈。 -
解析和执行代码:
引擎开始从上到下解析和执行代码,遇到函数调用时,会将其压入调用栈;遇到异步操作时,不会立即执行,而是将相关的回调函数放入任务队列。 -
检查调用栈:
引擎会持续检查调用栈,如果调用栈为空,则会检查任务队列。 -
执行任务队列中的任务:
如果调用栈为空且任务队列中有待处理的任务,事件循环会从任务队列中取出第一个任务,将其放入调用栈中执行。这个过程会重复进行,直到任务队列为空。 -
微任务与宏任务:
除了上述的宏任务(如setTimeout
),JavaScript还有微任务(microtasks),例如Promise的回调。微任务会在当前宏任务结束之后,但在控制权交给下一个宏任务之前执行。这通常意味着它们具有更高的优先级。 -
循环:
这个过程会一直重复,直到所有任务都被处理完毕,或者JavaScript引擎被显式地关闭。
通过理解事件循环,你可以更好地设计和优化异步代码,避免潜在的阻塞和性能问题。
在JavaScript中,任务可以分为两种类型:宏任务(macrotasks)和微任务(microtasks)。宏任务和微任务的区别在于它们在事件循环中的执行时机和优先级。
宏任务 (Macrotasks)
宏任务包括常见的同步代码执行、setTimeout
、setInterval
、I/O操作、UI渲染等。每个宏任务都会占用事件循环的一个完整轮次。这意味着,当一个宏任务开始执行时,它会运行直到完成,期间不会让位给其他宏任务或微任务。只有当当前宏任务完全执行完毕后,事件循环才会检查是否有其他待处理的宏任务或微任务。
微任务 (Microtasks)
微任务,例如Promise
的回调、MutationObserver
、process.nextTick
(在Node.js环境中)、以及queueMicrotask()
API,它们的执行时机是在当前宏任务结束之后,但在控制权交还给下一个宏任务之前。这意味着,即使有多个微任务,它们也会在当前宏任务的生命周期内按顺序执行完。
示例:
假设你有以下JavaScript代码:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
new Promise((resolve) => {
console.log('Promise');
}).then(() => {
console.log('Then');
});
console.log('End');
输出结果将是:
Start
Promise
End
Then
Timeout
解释如下:
- 同步代码
console.log('Start')
首先执行。 setTimeout
注册了一个宏任务,但不会立即执行。Promise
创建并立即执行,打印"Promise",然后在微任务阶段执行.then
回调。- 最后的
console.log('End')
是同步代码,所以紧接着执行。 - 接下来,由于当前宏任务已经结束,所有的微任务(这里是
.then
回调)被执行,打印"Then"。 - 最后,
setTimeout
的回调作为下一个宏任务执行,打印"Timeout"。
因此,微任务具有较高的优先级,可以在当前宏任务结束后立即执行,而不需要等待新的宏任务周期。这使得微任务非常适合用于需要在当前执行环境结束前完成的操作,例如更新DOM或执行某些异步操作的结果处理。