JavaScript内功修炼:前端异步编程规范
异步编程背景和Promise
的引入原因
异步编程的前置知识
异步编程在JavaScript中出现和发展的原因,主要是由JavaScript的执行环境和其单线程的特性所决定。这里有几个关键点来解释为什么异步编程变得如此重要。
- 单线程执行环境
- JavaScript最初被设计为一种在浏览器中运行的脚本语言,用于添加交互性和动态性。它在设计之初就是单线程的,这意味着在任何给定时刻,JavaScript在同一执行上下文中只能执行一个任务。这种设计简化了事件处理和DOM操作,因为它避免了多线程编程中常见的复杂性,如数据竞争和锁定问题。
- 非阻塞I/O
- 由于JavaScript是单线程的,阻塞式操作(如长时间运行的计算或网络请求)会冻结整个程序,导致不良的用户体验。为了避免这种情况,JavaScript环境提供了非阻塞I/O操作,这意味着可以在等待某些操作(如数据从服务器加载)完成时,继续执行其他脚本。
- 事件循环和回调函数
- JavaScript利用事件循环和回调函数来实现异步编程。事件循环允许JavaScript代码、事件回调和系统I/O等任务在适当的时候从任务队列中被取出执行,而不会阻塞主线程。这种模型支持了异步的回调形式,使得开发者可以编写非阻塞的代码,从而提高应用性能和响应速度。
- 提高性能和响应性
- 异步编程允许在等待操作完成(如从服务器获取数据)的同时,继续处理用户界面的交互和其他脚本,从而提高了Web应用的性能和响应性。用户不需要等待所有数据都加载完成才能与页面交互,这对于创建流畅的用户体验至关重要。
- 发展需求
- 随着Web技术的发展和应用越来越复杂,对于更高效、更可靠的异步编程模式的需求也随之增加。这推动了诸如
Promise
、async/await
等新的异步编程模式的出现,使得管理复杂的异步操作和链式调用更加简单和直观。相关文章:
Promise
的引入原因
随着Web应用程序变得越来越复杂,传统的回调方式开始显得力不从心。虽然回调函数提供了一种处理异步操作的手段,但它们也带来了所谓的”回调地狱”(Callback Hell),尤其是在处理多个异步操作时,代码会变得难以理解和维护。因此,为了解决这些问题,
Promise
应运而生。
- 简化异步代码:
Promise
提供了一种更优雅的方式来处理异步操作。通过使用Promise
,可以避免深层嵌套的回调函数,使代码结构更加清晰和简洁。- 链式调用:
Promise
支持链式调用(thenable链),这意味着可以按顺序排列异步操作,而不需要嵌套回调函数。这使得读写代码变得更加直观,也便于理解异步操作的流程。- 错误处理:在传统的回调模式中,错误处理往往比较复杂且容易出错。
Promise
通过catch
方法提供了一种集中处理错误的机制,使得错误处理更加一致和可靠。- 状态管理:
Promise
对象有三种状态:pending(等待中)、fulfilled(已成功)和rejected(已失败)。这种状态管理让异步操作的结果和状态变得可预测,并且只能从pending
状态转换到fulfilled
或rejected
状态,且状态一旦改变就不会再变,这为异步编程提供了更稳定的基础。- 改进的并发控制:
Promise
还提供了Promise.all
和Promise.race
等静态方法,使得并发执行和管理多个异步操作变得更加简单和高效。
Promise
的引入是为了解决回调模式中存在的问题,同时提供了一种更强大、更灵活、更易于管理的异步编程解决方案。随后,ES2017标准引入的async/await
语法进一步简化了异步操作的编写,但底层机制仍然基于Promise
,说明了Promise
在现代JavaScript异步编程中的核心地位。
Promise的拆解
拆解resolve和reject
1 |
|
执行了resolve或者reject后状态会发生改变,分别对应fulfilled和rejected,状态不可逆转,除了Pending状态其他的两个状态只要为其中一个后就不会再发生变更。
Promise中有throw相当于执行了reject
实现resolve与reject
初始状态为Pending,this指向执行它们的MyPromise实例,防止随着函数执行环境的改变而改变。
1 |
|
测试代码:状态变更
1 |
|
测试代码:状态不可变更
1 |
|
测试代码:捕获Promise回调内的异常
1 |
|
拆解then方法
1 |
|
根据上述代码可以确定:
- then接收两个回调,一个是成功回调,一个是失败回调;
- 当Promise状态为fulfilled执行成功回调,为rejected执行失败回调;
- 如resolve或reject在定时器里,则定时器结束后再执行then;
- then支持链式调用,下一次then执行受上一次then返回值的影响;
如何实现?
结构和初始化
首先,
MyPromise
的构造函数需要接收一个执行器函数,此执行器立即执行,并接收两个参数:resolve
和reject
。我们需要定义三种状态(pending
,fulfilled
,rejected
),以及用于存储成功/失败回调的数组。
then
方法和状态变更
then
方法应返回一个新的MyPromise
对象,以支持链式调用。在then
方法中,我们需要检查MyPromise
的当前状态,以决定立即执行回调还是将回调存储起来待状态改变后执行。对于定时器或异步操作,当
resolve
或reject
在这些操作内部调用时,then
注册的回调应在操作完成后执行。这意味着我们需要在状态仍为pending
时收集这些回调,并在resolve
或reject
被调用时按顺序执行它们。链式调用和值的传递
为了支持链式调用,每次调用
then
时都应创建并返回一个新的MyPromise
对象。这个新的MyPromise
对象的解决或拒绝应基于前一个then
回调的返回值。如果回调函数返回一个值,这个值应传递给链中下一个
then
的成功回调。如果回调函数抛出异常,则应将异常传递给链中下一个then
的失败回调。如果回调函数返回一个新的MyPromise
,则该Promise
的结果应决定链中下一个then
的调用。
实现then
#executeCallbacks
执行缓存的promiseresolvePromise
处理不同的返回值类型onFulfilledCallbacks
和onRejectedCallbacks
存储对应状态的执行任务
1 |
|
测试用例
1 |
|
queueMicrotask
相关链接:queueMicrotask
GPT的解释
queueMicrotask
是一个在现代浏览器和Node.js环境中内置的全局函数,用于将一个函数安排在所有正在执行的宏任务(例如setTimeout、setInterval、I/O操作等)和当前正在执行的微任务(例如Promise的回调)之后、但在下一个宏任务开始之前执行。它提供了一种方式来异步执行代码,而不会延迟到下一个宏任务,从而能够在当前任务和下一个事件循环之间快速地运行一个任务。
queueMicrotask
的主要用途是安排微任务(microtask),这是执行异步操作的一种方式,比起宏任务来说,微任务具有更高的优先级。在Promise相关操作中使用queueMicrotask
可以确保按照正确的顺序执行异步代码,尤其是在实现自定义Promise或处理与Promise相关的微任务队列时。
1 |
|
手写queryMicrotask
- 写法是渡一袁老师的写法,学习如何实现一个
queueMicrotask
1 |
|
Promise A+ 规范实现
1 |
|
测试案例
基本的链式调用
1 |
|
返回新的 Promise 对象
1 |
|
抛出错误
1 |
|
返回新的 Promise 对象,并且处理错误
1 |
|
异步操作
1 |
|
使用 all 方法
1 |
|
使用 any 方法
1 |
|
使用 race 方法
1 |
|
使用 allSettled 方法
1 |
|