js中异步编程的实现方式【详细】
一:回调函数
通过将函数作为参数传递到异步任务中,在任务完成后执行回调。
优点:实现简单,兼容性极佳(支持所有JS环境)
缺点:嵌套过深时会导致“回调地狱”,代码可读性和可维护性差
示例:
function fetchData(callback) {setTimeout(() => callback("数据"), 1000);
}
fetchData(data => console.log(data));
二:事件监听
异步任务的执行由事件触发,通过addEventListener绑定回调。
优点:支持多事件绑定,实现模块化解耦
支持多事件绑定
一个目标对象(如DOM元素)可同时绑定多个不同类型的事件(click/mouseover等),> 彼此互不干扰。
代码组织更灵活,例如:
element.addEventListener(‘click’, handleClick);
element.addEventListener(‘mouseenter’, showTooltip);
模块化解耦
生产者-消费者分离:事件触发方(如按钮)无需知道谁在处理事件,只需发出事件。
低耦合架构:不同模块通过事件通信,避免直接相互调用。例如:
// 模块A:发布事件
document.dispatchEvent(new CustomEvent(‘dataLoaded’, { detail: data }));
// 模块B:订阅事件(无需知道模块A的存在)
document.addEventListener(‘dataLoaded’, (e) => updateUI(e.detail));
缺点:流程控制不直观,需依赖事件驱动架构
document.addEventListener('click', () => console.log("事件触发"));
三:发布订阅模式
通过事件中心调度订阅者和发布者,实现完全解耦
优点:支持多对多通信,扩展性强
缺点:需手动管理订阅关系,调试复杂度较高
【本菜鸟还不太熟悉,后续总结】
四、Promise【详解】
Promise是异步编程的一种解决方案,Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,
1:先手写一个简单的Promise构造函数,从手写的过程可以大致理解Promise的原理
const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";function MyPromise (fn) {var self = this;// 保存初始化状态this.state = PENDING;// 初始化状态this.value = null;// 用于保存 resolve 或者 rejected 传入的值this.resolvedCallbacks = [];// 用于保存 resolve 的回调函数this.rejectedCallbacks = [];// 用于保存 reject 的回调函数// 状态转变为 resolved 方法function resolve (value) {// 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变if (value instanceof MyPromise) {return value.then(resolve, reject);}// 保证代码的执行顺序为本轮事件循环的末尾setTimeout(() => {// 只有状态为 pending 时才能转变,if (self.state === PENDING) {self.state = RESOLVED; // 修改状态self.value = value; // 设置传入的值// 执行回调函数self.resolvedCallbacks.forEach(callback => {callback(value);});}}, 0);}function reject (value) {setTimeout(() => {if (self.state === PENDING) {self.state = REJECTED; self.value = value; self.rejectedCallbacks.forEach(callback => {callback(value);});}}, 0);}// 将两个方法传入函数执行try {fn(resolve, reject);} catch (e) {// 遇到错误时,捕获错误,执行 reject 函数reject(e);}}MyPromise.prototype.then = function (onResolved, onRejected) {// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数onResolved =typeof onResolved === "function"? onResolved: function (value) {return value;};onRejected =typeof onRejected === "function"? onRejected: function (error) {throw error;};// 如果是等待状态,则将函数加入对应列表中if (this.state === PENDING) {this.resolvedCallbacks.push(onResolved);this.rejectedCallbacks.push(onRejected);}// 如果状态已经凝固,则直接执行对应状态的函数if (this.state === RESOLVED) {onResolved(this.value);}if (this.state === REJECTED) {onRejected(this.value);}};
看上去有点复杂,时间有限,所以我决定放弃这个promise的手写和原理,将各种用法弄清楚。
2:promise使用示例:
构造函数Promise接收一个函数作为参数,这个函数带有两个参数,一个是resolve,一个是reject,这个函数内部的代码本身是同步的立即执行函数, 只有执行resolve或者reject的时候是异步操作, 实际会先执行对应的then/catch等,将then/catch里的代码放进微任务队列中,当主栈完成后,才会去调用resolve/reject中存放的方法执行。
// Promise构造函数接受一个函数(执行器函数)作为参数,// 该函数的两个参数分别是resolve和reject。它们是两个函数,// 由 JavaScript 引擎提供,不用自己部署。const promise = new Promise(function (resolve, reject) {// ... some codeif (/* 异步操作成功 */) {// 在异步操作成功时调用,并将异步操作的结果,作为参数value传递出去;resolve(value);} else {// 在异步操作失败时调用,并将异步操作报出的错误,作为参数error/reason传递出去。reject(error);}});
Promise实例的then方法接收两个回调函数作为参数,第一个回调当resolve()时执行,是成功回调,第二个回调reject()时执行,是失败回调。
promise.then(function(value) {// success
}, function(error) {// failure
});
但同时Promise实例的catch方法也可以捕获异常,这时候我有点疑惑,既然then方法已经可以指出失败回调,那catch方法的意义是什么?先查了一下区别
then的失败回调
仅能捕获当前Promise链中前一个Promise的reject状态或抛出的错误
catch方法
能捕获整个Promise链中任意位置的未处理错误(包括then中抛出的异常)
这个解释明确的说明catch在promise的链式调用中更好用,可以将错误处理集中在链式调用的末尾。这时候我又产生了两个疑惑,第一个是链式调用是什么,第二个是如果集中处理了,那catch如何直到捕获的错误是哪一步的错误
然后针对第一个问题,我开始学习promise的链式调用
3:promise的链式调用
let p1 = new Promise((resolve, reject) => {setTimeout(() => resolve("成功!"), 1000);
});p1.then(result => {console.log(result); // 输出:成功!return "下一步"; // 返回一个值,可以被下一个.then()捕获
}).then(nextStep => {console.log(nextStep); // 输出:下一步return new Promise((resolve, reject) => {setTimeout(() => resolve("最终结果"), 500);});
}).then(finalResult => {console.log(finalResult); // 输出:最终结果
});
首先,我已知promise的then方法返回的是一个新的promise,从这段代码可以看出,p1的then方法的成功回调中返回一个字符串—‘下一步’,直接被下一个then方法捕获了。换种说法就是,首先,第一个then函数本身返回了一个新的promise-------------
当你then里面的回调函数返回一个非promise的值的时候,这个新的promise会立即resolve该值,也就是直接执行第二个then方法的第一个回调。
当你then里面的回调函数返回一个promise时候,then返回新的promise会跟随你在回调函数中返回的promise的状态[你也可以看成同一个promise,没有差别,反正都是执行相同的回调]。
略绕,总之这样链式调用最大好处就是整个链条能保持清晰的执行顺序和统一的错误处理
4:链式调用的catch回调
【1】.catch()会捕获第一个错误(无论是reject的错误还是throw 的异常),后续错误会被静默忽略。
【2】如果同时存在then的错误回调和独立的catch方法,遵循就近原则和互斥执行【只执行最近那个】
let promise = new Promise((resolve, reject) => {setTimeout(() => reject("失败"), 1000);});promise.then(result => {return "下一步"; },(error)=>{return 'error' // 只执行这个}).then(nextStep => {console.log(nextStep); return new Promise((resolve, reject) => {setTimeout(() => reject("第二步报错"), 500);});}).then(finalResult => {console.log(finalResult); }).catch(error=>{console.log(error)});
5:promise的其他方法
all()
使用场景:并行请求多个接口后统一处理数据,批量文件上传全部完成后触发通知
// 模拟三个异步API请求
const fetchUser = () => new Promise(resolve => setTimeout(() => resolve({id: 1, name: 'Alice'}), 800));const fetchOrders = () =>new Promise(resolve => setTimeout(() => resolve([101, 102, 103]), 500));const fetchProducts = () =>new Promise(resolve => setTimeout(() => resolve(['Laptop', 'Phone']), 300));// 使用Promise.all并行执行
Promise.all([fetchUser(), fetchOrders(), fetchProducts()]).then(([user, orders, products]) => {console.log('整合数据:', { user, orders, products });}).catch(err => console.error('请求失败:', err));
race()
采用竞速模式,返回第一个敲定状态(无论成功/失败)的Promise结果
使用场景:请求超时控制:将目标请求与setTimeout的reject Promise竞速。【现在axios等工具直接传入timeout参数即可】
// 模拟API请求(2秒返回)
const apiRequest = new Promise(resolve => setTimeout(() => resolve("数据获取成功"), 2000)
);// 设置1秒超时
const timeout = new Promise((_, reject) =>setTimeout(() => reject("请求超时"), 1000)
);// 竞速执行
Promise.race([apiRequest, timeout]).then(res => console.log(res)).catch(err => console.error(err));
finally()
使用场景:隐藏加载动画无论请求成功与否
fetchData().then(res => console.log(res)).catch(err => console.error(err.message)).finally(() => {console.log("无论成功失败,都会执行清理工作");document.getElementById('loading').style.display = 'none';});
6:附加一个执行顺序判断的示例,测试下对promise的了解程度
console.log('script start')
let promise1 = new Promise(function (resolve) {console.log('promise1')resolve()console.log('promise1 end')
}).then(function () {console.log('promise2')
})
setTimeout(function(){console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
五、Async/Await
async 将一个函数标记为异步函数,await 需要在异步函数中使用,标记当前操作是异步操作。async + await 必须配合 promise 使用,同时 async 和 await 必须一起使用。即 await 必须在 async 标记的函数中使用。
以下是一个使用示例:
function getProfile() {return new Promise((resolve, reject) => {// 使用定时器模拟接口请求setTimeout(() => {resolve({code: 200,msg: "用户信息",data: {id: 1,name: "liang"}})}, 3000);});
}
// 以下代码会执行 先输出 123 再执行输出 res
function loadData() {getProfile().then(res => {console.log(res);})console.log(123);
}
// 下面写法会使 getProfile() 先执行
// 等待三秒后执行完再把得到的结果赋值给左边的res,然后再继续往下执行
async function loadData() {const res = await getProfile()console.log(res);console.log(123);
}
我的理解:async创建了一个异步环境,在这个环境中其实是不存在同步代码的。
首先在使用时每个await后面理论上都需要紧跟一个promise,例如:
async function asyncChain () {try {const stage1 = await new Promise(resolve => setTimeout(() => resolve(1), 1000));console.log("第一阶段结果:", stage1);} catch (error) {console.error("Async链错误:", error);}}
如果跟的是一个非 Promise 值时(如字符串、数字等),引擎会通过 Promise.resolve() 将其隐式转换为已解决的 Promise 对象,此时代码会立即继续执行,也就是跟了一个立即resolve那个字符串或者数字的promise。
例如:
async function demo() {const result = await 42; // 等同于 await Promise.resolve(42)console.log(result); // 直接输出42
}
而await后面的代码,比如上方代码块中的console.log(result),实际上相当于最近的前一个await后面紧跟的Promise的then回调里的内容。
我:也就是await会阻塞async函数中在await之后的代码,那是不是相当于await之后的代码其实是当前await后面的这个promise实例的then回调?
deepseek:是的,await 之后的代码可以视为当前 await 后面的 Promise 实例的 then 回调,但两者在实现机制和细节上存在差异。
async function demo() {const result = await somePromise; // 阻塞后续代码console.log(result); // 相当于 somePromise.then(res => console.log(res))
}
六、Generator/yield
这个我写了五年前端代码没有用到过。当然我是菜鸟哈哈。面试遇到就放弃好了。