前端宝典二十一:前端异步编程规范手写Promise、async、await

本文主要是介绍前端宝典二十一:前端异步编程规范手写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事件监听

二、异步处理场景

  1. 网络请求
  2. 定时任务
  3. 事件绑定
  4. 大量数据处理web worker
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
</head><body><script>// 创建 Web Workerconst 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;// 模拟大量数据处理,这里只是简单地将每个元素乘以 2const 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是微任务,要在主任务完成后执行,因为都在微任务列表中

官方提示注意:
在使用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 = 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和rejecttypeof 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实例加上两个参数statusvalue

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实例加上两个参数statusvalue

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

  • 用同步方式执行异步操作,防止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返回最终结果。

四、为什么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”状态,状态机的状态和转换规则将变成:

  1. Pending(进行中) :

可以转换到Fulfilled。
可以转换到Rejected。
可以转换到Cancelled。

  1. Fulfilled(已完成) :状态不可变。
  2. Rejected(已拒绝) :状态不可变。
  3. 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的设计哲学和状态机模型,我们可以更好地掌握其使用方法,并通过巧妙的编程技巧实现我们需要的功能

这篇关于前端宝典二十一:前端异步编程规范手写Promise、async、await的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1126695

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份

《纳瓦尔宝典》是纳瓦尔·拉维坎特(Naval Ravikant)的智慧箴言

《纳瓦尔宝典》是一本由埃里克·乔根森(Erik Jorgensen)编著的书籍,该书于2022年5月10日由中信出版社出版。这本书的核心内容围绕硅谷知名天使投资人纳瓦尔·拉维坎特(Naval Ravikant)的智慧箴言,特别是关于财富积累和幸福人生的原则与方法。 晓北斗推荐 《纳瓦尔宝典》 基本信息 书名:《纳瓦尔宝典》作者:[美] 埃里克·乔根森译者:赵灿出版时间:2022

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧