前端宝典二十一:前端异步编程规范手写Promise、async、await
本文主要探讨前端异步编程的处理方式、处理场景,并且手写Promise的全家桶,介绍async、await方法使用
一、异步处理方式有:
1. 回调函数
function fetchDate(callback) {
setTimeout(() => {
const date = new Date();
callback(date);
}, 1000);
}
fetchDate((function(date) {
console.log
}));
2. promise
const fetchPromise = (callback) => {
return new Promise((resolve, reject) => {
resolve(callback);
reject('Error');
}).then((date) => {
console.log(date);
}).catch((err) => {
console.log(err);
})
}
3. async、await
async function name(params) {
try {
const data1 = await fecth('https://jsonplaceholder.typicode.com/posts');
const data2 = await fecth('https://jsonplaceholder.typicode.com/users');
const data3 = await fecth('https://jsonplaceholder.typicode.com/comments');
} catch (error) {
console.log(error);
}
}
4. 发布订阅模式
这里手写一个EventEmitter
使用发布订阅模式实现异步操作:
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName,...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(...args));
}
}
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb!== callback);
}
}
}
const eventEmitter = new EventEmitter();
function asyncOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Async operation completed!');
}, 2000);
});
}
async function performAsyncWithPubSub() {
eventEmitter.on('operationCompleted', result => {
console.log(result);
});
const result = await asyncOperation();
eventEmitter.emit('operationCompleted', result);
}
performAsyncWithPubSub();
在这个例子中,我们创建了一个EventEmitter
类来实现发布订阅模式。asyncOperation
函数模拟一个异步操作,在两秒后返回一个结果。performAsyncWithPubSub
函数使用发布订阅模式,在异步操作完成后发布一个事件,然后订阅这个事件的回调函数会被执行并打印出结果。
5. generator函数
6. promise all
const fetchRes = (res) => {
return new Promise((resolve, reject) => {
resolve(res);
}).then((data) => {
return data
}).catch((error) => {
console.log(error);
})
}
const fetchAll = () => {
return Promise.all([fetchRes('res1'), fetchRes('res2')]).then((data) => {
console.log(data);
}).catch((error) => {
console.log(error);
}).finally(() => {
console.log('done');
})
}
7. 预加载资源
import { Image } from 'react-native';
// 预加载图像
Image.prefetch('https://example.com/image.jpg')
.then(() => console.log('Image preloaded successfully'))
.catch(error => console.error('Error preloading image:', error));
8. addEvent Listener事件监听
二、异步处理场景
- 网络请求
- 定时任务
- 事件绑定
- 大量数据处理web worker
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
// 创建 Web Worker
const worker = new Worker('worker.js');
// 生成大量数据
const largeData = Array.from({ length: 1000000 }, (_, i) => i);
// 向 Web Worker 发送数据进行处理
worker.postMessage(largeData);
// 监听 Web Worker 的消息
worker.addEventListener('message', event => {
console.log('处理后的数据:', event.data);
});
</script>
</body>
</html>
Web Worker 文件(worker.js):
self.addEventListener('message', event => {
const data = event.data;
// 模拟大量数据处理,这里只是简单地将每个元素乘以 2
const processedData = data.map(item => item * 2);
// 将处理后的数据发送回主页面
self.postMessage(processedData);
});
主页面生成了一个包含一百万个数字的大量数据数组,并将其发送给 Web Worker 进行处理。Web Worker 接收到数据后,对每个元素进行简单的乘以 2 的操作,并将处理后的数据发送回主页面。主页面通过监听 Web Worker 的消息事件来获取处理后的数据并进行输出。
通过使用 Web Worker,可以在后台线程中异步处理大量数据,避免阻塞主页面的 UI 线程,提高应用的性能和响应性。
三、Promise
1、特点
- Promise状态有
Pending进行中
、Rejected已失败
、Fullfilled已成功
- 状态不可逆,第一次成功就永久,是fullfilled,第一次失败就永久是rejected
- Promise中有thorw的话,就相当于reject
2、代码实现一个Promise
要实现一个自建的MyPromise
实例,需要根据Promise的特点进行几个方面的处理:
- Promise的初始状态是pending
- resolve、reject需要绑定this,确保永远指向当前的
MyPromise
实例 - 状态非pending时不可变
- 如果有throw相当于执行了reject,这里用try、catch实现
实现了以上的功能点,基本可以自建一个MyPromise
实例了,不过还缺少then
- then是绑定在Promise上的Promise.prototype.then
- 第一个参数是resolved的回调函数,第二个参数是rejected的回调函数
- then还可以链式调用
- then是微任务,要在主任务完成后执行,因为都在微任务列表中
大体上有以下几个步骤:
- 声明一个类class HePromise,HePromise里创建私有属性:
状态
、value
(resolve和reject处理传入then的value值)、resolves列表
、rejects列表
。 - constructor内绑定resolve、reject到this,保证永远指向当前的实例。
- resolve函数,状态不可逆,非pending时返回否则改为fulfilled,将传入的参数设置为this.value, 留作then时使用,将resolves内的resolve函数都从头一个个取出,执行函数。
- reject函数的思路和resolve一样,也是非pending时返回,否则状态改为rejected
- then函数返回一个新的实例对象HePromise,先处理状态,pengding时将resolve和reject传入对应的private列表,否则fulfilled执行resolve逻辑,rejected执行reject逻辑;resolve逻辑:如果传入的resolveFunc函数执行结果resolvedVal是HePromise对象,那么继续递归then,否则resolve(resolvedVal), 执行resolve函数;reject的部分逻辑也一样处理即可。
官方提示注意:
在使用then方法是,不要在then方法中定义rejected状态的回调函数,而应总是使用catch方法
type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;
enum STATUS {
PENDING = 'pending',
FULFILLED = 'fulfilled',
REJECTED = 'rejected',
}
export class HePromise {
private status = STATUS.PENDING;
private value = undefined;
private resolves: FuncType[] = [];
private rejects: FuncType[] = [];
constructor(executor: ExecutorFunc) {
const { resolve, reject } = this;
// executor是一个函数,它接受两个参数,通常被命名为 resolve 和 reject。
//这两个参数本身也是函数
//分别用于在异步操作成功时调用以改变 Promise 的状态为 fulfilled(完成)并传递结果值
//以及在异步操作失败时调用以改变 Promise 的状态为 rejected(拒绝)并传递错误原因。
executor(resolve, reject);
}
private resolve = (resolvedVal: any) => {
const { resolves, status } = this;
// 状态不可逆
if (status !== STATUS.PENDING) return;
this.status = STATUS.FULFILLED;
// 将结果值设置为this.value,用于传入then
this.value = resolvedVal;
while (resolves.length) {
const cb = resolves.shift();
if (cb) cb(resolvedVal);
}
};
private reject = (rejectedVal: any) => {
const { rejects, status } = this;
if (status !== STATUS.PENDING) return;
this.status = STATUS.REJECTED;
this.value = rejectedVal;
while (rejects.length) {
const cb = rejects.shift();
if (cb) cb(rejectedVal);
}
};
then(resolveFunc: FuncType, rejectFunc?: FuncType): HePromise {
// then函数传入两个方法,相当于resolve和reject
// 如果不是方法,返回参数为value的resolveFunc函数
typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;
typeof rejectFunc !== 'function'
? (rejectFunc = reason => {
throw new Error(reason instanceof Error ? reason.message : reason);
})
: null;
return new HePromise((resolve, reject) => {
const resolvedFn = (val: any) => {
try {
const resolvedVal = resolveFunc(val);
resolvedVal instanceof HePromise
? resolvedVal.then(resolve, reject)
: resolve(resolvedVal);
} catch (error) {
if (reject) reject(error);
}
};
this.resolves.push(resolvedFn);
const rejectedFn = (val: any) => {
if (rejectFunc) {
try {
const rejectedVal = rejectFunc(val);
rejectedVal instanceof HePromise
? rejectedVal.then(resolve, reject)
: resolve(rejectedVal);
} catch (error) {
if (reject) reject(error);
}
}
};
switch (this.status) {
case STATUS.PENDING:
this.resolves.push(resolvedFn);
this.rejects.push(rejectedFn);
break;
case STATUS.FULFILLED:
resolvedFn(this.value);
break;
case STATUS.REJECTED:
rejectedFn(this.value);
break;
}
});
}
}
3、Promise.all
1.Promise.all的特点:
- 接受一个Promise数组,如果数组中有非Promise项,此项当做成功
- 如果所有Promise都成功,则返回成功的结果数组
- 如果有一个Promise失败,则返回这个失败结果
2.代码实例
const fetchDate = (str)=>{
return new Promise((resolve,reject)=>{
fetch(str)
.then((response)=>{
if(!response.ok){
reject(new Error(`HTTP error! Status: ${response.status}`));
}
return response.json;
}).then((data)=>{
resolve(data);
}).catch((error)=>{
console.log(error)
})
})
}
const fetchAll = ()=>{
return new Promise.all([fetchDate1,fetchDate2])
.then((data)=>{
console.log(data);
}).catch((error)=>{
console.log(error)
})
}
3. 手写代码实现一个Promise.all
这个方法是一个静态方法,可以在实例上获取
new Promise().all([])
也可以在类上直接获取
Promise.all()
具体实现如下:
看到上面的代码实例,我们在手写实现的时候需要给每个promise实例加上两个参数status
和value
static all(promises){
let result = [];
let count = 0;
return new Promise((resolve,reject)=>{
const addData = (index, value)=>{
result[index] = value;
count++;
if(count === promises.length){
resolve(result);
}
}
promises.forEach((promise, index)=>{
if(promise instanceof Promise){
promise.then((data)=>{
addData(index,data)
}).catch((e)=>
reject(e);
return;
)
} else {
addData(index,promise)
}
})
})
}
4、Promise.race
1. Promise.all的特点:
- 接受一个Promise数组,如果数组中有非Promise项,此项当做成功
- 哪个Promise最快得到结果,就返回那个结果,无论成功失败
2.代码实例
const arr = [fetchData1, fetchData2]
const promiseRace = (arr)=>{
return new Promise.race((arr))
.then((res)=>{
return res
}).catch((e)=>
reject(e);
)
}
3. 手写代码实现一个Promise.race
一样,我们还是用static格式来手写一个race方法
static race(promises){
return new Promise((resolve,reject)=>{
promises.forEach((promise)=>{
if(promise instanceof Promise){
promise.then((res)=>{
resolve(res);
return
}).catch((err)=>{
reject(err);
return;
})
} else {
resolve(err);
}
})
})
}
5、Promise.allSettled
1. 与Promise.all()
区别
与Promise.all()
不同的是,Promise.allSettled()
不会因为其中一个 promise 被拒绝而立即拒绝,而是会等待所有的 promise 都有结果后,无论结果是成功还是失败,都会将每个 promise 的状态和值或错误信息记录下来并返回。
2、代码实例
const allSettled = (promises) => {
return Promise.allSettled(promises)
.then((promise) => {
if (promise.status === 'fulfilled') {
console.log(promise.value);
} else {
console.log(promise.reason);
}
})
}
3、手写代码实现allSettled
看到上面的代码实例,我们在手写实现的时候需要给每个promise实例加上两个参数status
和value
const myAllSettled = (promises)=>{
let res = [];
let count = 0;
return new Promise((resolve,reject)=>{
const addData = (status, value, i)=>{
res[i]={
status,
value
};
count++;
if(count === promises.length){resolve(res)};
}
})
promises.forEach((promise,index)=>{
if(promise instanceof Promise){
promise.then(res=>{
addData('fullfilled', res, index);
},err=>{
addData('rejected', res, index);
})
} else {
addData('fullfilled', promise, index);
}
})
}
6、Promise.any
1. Promise.any 介绍
Promise.any()
方法接收一个可迭代对象(例如数组),其中包含多个 Promise 实例。这个方法返回一个新的 Promise,只要可迭代对象中的其中一个 Promise 成功,这个新的 Promise 就会成功,并且返回第一个成功的 Promise 的值。如果可迭代对象中的所有 Promise 都失败了,那么这个新的 Promise 就会失败,并返回一个AggregateError
错误对象,这个错误对象包含所有失败的 Promise 的错误信息。
2.代码实例
any和all的区别就在于,any是任意一个就行,所以只返回一个
Promise.any([promise1, promise2, promise3])
.then(result => {
console.log(result); // 'Success from promise3'
})
.catch(error => {
console.error(error);
});
3、手写代码实现
function myPromiseAny(promises) {
return new Promise((resolve, reject) => {
const errors = [];
let fulfilledCount = 0;
if (!Array.isArray(promises) || promises.length === 0) {
return reject(new AggregateError([], "No Promises provided"));
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then(value => {
resolve(value);
})
.catch(error => {
errors.push(error);
fulfilledCount++;
if (fulfilledCount === promises.length) {
reject(new AggregateError(errors));
}
});
}
});
}
四、手写一个scheduler
class Scheduler{
constructor(maxCurrent){
this.maxCurrent = maxCurrent;
this.runningNum = 0;
this.queue = []
}
add = (task)=>{
return new Promise((resolve,reject)=>{
this.queue.push({task,resolve,reject});
this.runQueue()
})
}
runQueue = ()=>{
if(this.maxCurrent>this.runningNum&&this.queue.length>0){
const{task, resolve,reject} = this.queue.shift();
this.runningNum++;
task()
.then((res)=>{
resolve(res);
}).catch((err)=>{
reject(err)
}).finally(()=>{
this.runningNum--;
this.runQueue()
})
}
}
}
const scheduler = new Scheduler(2);
在这个实现中,Scheduler
类接受一个参数 maxConcurrent
,表示同时运行的最大任务数。通过维护一个任务队列和当前正在运行的任务计数,确保在任何时候都不会超过指定的最大并发数。当一个任务完成时,会从任务队列中取出下一个任务并执行。
五、async/ await
1、使用
- 用同步方式执行异步操作,防止then不停嵌套
- await只能在async函数中使用
- 如果想起到同步效果,await后面最好跟Promise
- async执行完会返回一个fullfilled状态的Promise,如果想得到返回值,就加一个return,有没有值就看return了
const fn = async(url)=>{
const res1 = await fetchData(url);
const res2 = await fetchData(res1.nextUrl);
return res2;
}
上面的代码就是一个一个执行,并且第一个执行的返回值传给了第二次作为参数,最后return
返回最终结果。
2、手写代码实现async/ await
async/ await是generator的语法糖,区别在于
- generator函数返回不是Promise,async返回Promise
- generator需要执行相应的操作,才能等同于async排队效果
- generator函数执行操作是不完善的,不确定有几个yield,不确定嵌套几次
因此我们基于generator函数手写async/ await的功能实现
首先是一个generator函数:
const f1 = (nums) => {
return new Promise((resolve)=>{
setTimeout(()=>{
resolve(nums*2)
},1000)
})
}
const gen = function*() {
const nums = yield f1(10);
const num2 = yield f1(nums);
return num2;
}
使用递归方法,将生成器函数产生的数据不断调用next(),直到执行完成,这里有一个判断是否执行完成的方法generator.next().done
const asyncPromise = (generatonFn) => {
const generator = generatonFn();
try {
handleResult(generator.next());
} catch (error) {
return Promise.reject(error);
}
const handleResult = (result) => {
if (result.done) {
return Promise.resolve(result.value);
}
return Promise.resolve(result.value).then((res)=>{
handleResult(generator.next(res))
}).catch((error)=>{
generator.throw(error);
})
}
}
四、为什么Promise没有取消机制
在JavaScript中,Promise是用于处理异步操作的对象,它代表一个异步操作的最终完成(或失败)及其结果值。然而,JavaScript的Promise并不提供内置的取消(cancel)机制。
Promise是经过了深思熟虑,才不自带取消功能的!!!
这篇文章,将围绕着设计的哲学,以及从状态机的角度,解释为什么不需要cancel。
即使如此,文章最后部分,还是会提供一些方法,来实现一下cancle。
设计的哲学
设计理念
Promise的设计初衷是为了简化回调函数的使用,使得处理异步操作的代码更加简洁和可读。其设计重点在于处理异步操作的成功和失败,而不是控制操作的生命周期。
取消机制会引入复杂性,尤其是对于依赖于多个Promise的情况,例如Promise.all或Promise.race。如果某个Promise被取消,其影响可能会传递给其他依赖于它的Promise,导致意外的行为和难以调试的问题。
资源管理
异步操作通常涉及到外部资源,如网络请求、定时器等。Promise取消机制需要能够正确管理和释放这些资源。实现一个通用且可靠的资源管理机制非常复杂,并且可能因不同的资源类型而异。
取消语义不明确
如果一个Promise可以被取消,那么需要明确如何处理其已完成的状态。特别是,处理已经部分完成或即将完成的操作,可能会导致不一致的状态。
状态机:简单就是美
Promise的状态机
在输入一个状态时,只得到一个固定的状态。
一个Promise可以被看作是一个简单的状态机,它有以下几种状态:
- Pending(进行中) :初始状态,表示异步操作尚未完成。
- Fulfilled(已完成) :表示异步操作成功完成,并返回了一个值。
- Rejected(已拒绝) :表示异步操作失败,并返回了一个原因(错误)。
状态转换规则如下:
- 从Pending状态可以转换到Fulfilled状态。
- 从Pending状态可以转换到Rejected状态。
一旦转换到Fulfilled或Rejected状态,Promise的状态就不可再改变。
取消功能的复杂性
引入取消功能意味着需要增加一个新的状态——“Cancelled(已取消)”。这会使状态机的设计变得更加复杂,因为需要考虑更多的状态转换和边界情况。
如果我们引入“Cancelled”状态,状态机的状态和转换规则将变成:
- Pending(进行中) :
可以转换到Fulfilled。
可以转换到Rejected。
可以转换到Cancelled。
- Fulfilled(已完成) :状态不可变。
- Rejected(已拒绝) :状态不可变。
- Cancelled(已取消) :状态不可变。
这种增加的复杂性会导致以下问题:
- 状态转换冲突:需要明确地处理在Pending状态下多次转换的情况。例如,如果一个Promise在Pending状态下同时尝试转换到Fulfilled和Cancelled,应该优先处理哪一个?
- 副作用处理:许多异步操作(如网络请求、文件读写等)具有副作用。取消这些操作需要确保所有相关的资源都被正确地清理,这不仅增加了实现的复杂性,还可能导致不一致的状态。
- 链式操作:Promise通常被链式调用( .then().catch() )。如果一个中间的Promise被取消,如何处理后续链式操作也是一个难题。例如,Promise.all或Promise.race的行为如何改变?
如何实现取消功能
尽管标准的Promise没有内置的取消功能,可以通过一些方法来实现类似的功能。例如,使用AbortController来取消网络请求,或者使用自定义的Promise包装器来支持取消。
使用AbortController
对于Fetch API,可以使用AbortController来取消请求:
const controller = new AbortController();
const signal = controller.signal;
fetch('https://www.baidu.com', { signal })
.then(response => response)
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', err);
}
});
// 取消请求
controller.abort();
自定义Promise包装器
也可以创建一个支持取消的自定义Promise包装器:
class CancellablePromise {
constructor(executor) {
this._hasCanceled = false;
this._promise = new Promise((resolve, reject) => {
executor(
value => this._hasCanceled ? reject({ canceled: true }) : resolve(value),
reason => this._hasCanceled ? reject({ canceled: true }) : reject(reason)
);
});
}
cancel() {
this._hasCanceled = true;
}
then(onFulfilled, onRejected) {
return this._promise.then(onFulfilled, onRejected);
}
catch(onRejected) {
return this._promise.catch(onRejected);
}
}
// 使用自定义的CancellablePromise
const cancellablePromise = new CancellablePromise((resolve, reject) => {
setTimeout(() => resolve('Completed!'), 1000);
});
cancellablePromise.then(
result => console.log(result),
err => {
if (err.canceled) {
console.log('Promise was canceled');
} else {
console.error('Promise error:', err);
}
}
);
// 取消Promise
cancellablePromise.cancel();
虽然标准的Promise没有内置取消功能,但可以通过这些方法来实现取消逻辑,根据实际需求选择合适的方案。
结语
虽然JavaScript的Promise没有内置取消功能,但这并不意味着我们无法实现取消功能。通过理解Promise的设计哲学和状态机模型,我们可以更好地掌握其使用方法,并通过巧妙的编程技巧实现我们需要的功能