异步编程:实现 Promise| 豆包MarsCode AI刷题

27 阅读10分钟

前言

在现代 JavaScript 开发中,异步编程是不可或缺的部分。无论是与后端 API 通信、读取文件、处理定时任务,还是执行复杂的用户交互,异步操作无处不在。传统的回调函数(callback)曾经是处理异步任务的主要手段,但随着应用复杂度的增加,回调地狱问题逐渐显现,代码的可读性和维护性变得越来越差。

为了解决这些问题,ES6 引入了 Promise,它的出现不仅简化了代码逻辑,还为异步操作提供了链式调用和错误处理机制,使得开发者能够更加轻松地管理异步流程。

所以,在本篇文章中,我们将从基础到深入,着重介绍 Promise 的概念、用途、方法、以及如何实现一个完整的 Promise ,全面解析 Promise 的原理与实现,帮助我们更好的理解异步编程所面临的问题。

异步编程的挑战与演变

前置知识:JavaScript是一种单线程语言,一次只能执行一条语句或者一行代码,你可以理解为在加载 JS 文件时,JS 引擎会从上到下执行每一行代码。

为什么要引入异步编程?

在浏览器中,我们经常需要与外部系统同步,比如:请求数据、读取文件,如果操作这些任务是同步的,那么我们需要等待服务端响应数据,或者等待文件数据读取完毕,才能执行接下来的任务,最终导致主线程被阻塞,用户看到的页面也是卡顿的、不流畅的。

为了避免这样的情况发生,异步编程应用而生。异步编程的本质就是在遇到一些需要耗时较长的任务时,不需要等待该任务完成,而是执行其他任务,当异步任务完成后,将对应的结果返回给主线程。

这样,用户在浏览页面时,就算某个数据发生异常或者返回的时间较慢,也不会影响其他功能的正常使用,即提升了用户的体验,又优化了 JS 性能,也加快了渲染的速度。

异步编程的都有哪些方式实现?

由以下四种方式实现:

  • 回调函数

  • Promise

  • Generator

  • async/await

重点:

在执行异步操作时,通过事件循环机制来检测异步操作并处理,

原理:

当执行主线程任务时,从上而下执行每一行代码,如果遇到同步代码时,将同步代码推到执行栈中按顺序执行,如果遇到异步代码时,不会立即执行,而是先注册异步任务,然后继续执行主线程,当异步任务执行完成后,会将相应的回调函数放入任务队列中,在执行完所有同步代码后,此时事件循环开始检测任务队列中是否有需要处理的回调函数,如果有,他会从任务队列中取出,然后依次执行。

回调地狱

这个名词大家应该都不陌生,尤其是使用过 callback 来实现异步编程的,如果遇到较为复杂的业务逻辑,就像嵌套了无数层 if 语句,导致异步代码变得极其晦涩难懂,在可读性和可维护性上都让人难以理解。

其实在写这篇文章之前,我没有去深入了解过使用回调函数去实现异步编程,通常在开发项目中,一直使用的都是 async/await,也就是在写文章的过程发现不得不去深入这些有着千丝万缕之间的知识点。

来展示代码:

var sayhello = function (name, callback) {
  setTimeout(function () {
    console.log(name);
    callback();
  }, 1000);
}
sayhello("first", function () {
  sayhello("second", function () {
    sayhello("third", function () {
      console.log("end");
    });
  });
});
//输出: first second third  end

在上面这个例子中,每个任务都要依赖上一个任务完成后才能执行,而后,导致的这种嵌套回调,在可读性上都是非常差的,更不用说它的可维护性。

所以,这也就是为什么后面推出这些异步编程的方式,就是为了更好的实现异步编程。

那么,接下来就让我们来深入 Promise,看看它是如何解决回调地狱,去更好的处理异步编程的。

Promise 前置知识

什么是 Promise?

  • Promise 是 JavaScript 中用于处理异步操作的一种对象,它代表一个在未来某个时间可能会完成或失败的操作。

  • Promise 在 ES6 中引入,它是回调函数的改进,提供了更清晰的链式调用方式,避免了嵌套。

  • Promise 内置了各种方法来处理异步任务,基于它提供的三种状态值来处理。

Promise 的三种状态

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  1. pending:进行中

  2. fulfilled:已成功

  3. rejected:已失败

状态改变:Promise 接收一个函数作为参数,该函数有俩个参数皆为函数如下

  • fulfilled 函数:pending => fulfilled

  • reject 函数:pending => rejected

创建一个 Promise

Promise对象本质上是一个构造函数,所以需要 new 操作符来生成实例去使用。

代码示例:

const promise = new Promise(function (resolve, reject) {
    if (/* 异步操作成功 */) {
        resolve(value);
    } else {
        reject(error);
    }
});

这是一个最基本的 Promise 示例。

  • 当异步操作成功时,就会调用该函数的第一个参数,也就是将状态从 "进行中" 变为 "成功" 的这个函数,并将结果作为参数传递出去。

  • 当异步操作失败时,就会调用该函数的第二个参数,也就是将状态从 "进行中" 变为 "失败" 的这个函数,并将报出的错误作为参数传递出去。

链式调用与错误捕获

then 和 catch 的使用

then

概念:then 是定义在原型对象上 Promise.prototype 的方法。

作用:为 Promise 实例添加状态改变时的回调函数。

参数

  • resolved:resolved状态下的回调函数

  • rejected:rejected状态下的回调函数

代码示例


let promise = new Promise(function(resolved,rejected){
    resolved('成功了')
})
promise.then(function (value) {
    console.log(value)
}, function (error) {
    console.log(error)
})

上述代码在创建示例后,执行 resolved 函数,将状态从 '进行中' 变为 '已成功',并将结果作为参数传递出去,而promise实例 调用 then 方法,执行第一个回调函数收该参数,并将参数打印到控制台,并且,当第一个参数执行完成后,会返回一个新的 Promise 对象,如果新的 Promise 对象状态变为 rejected,就会执行第二个回调函数。

如果执行的函数为 rejected 函数,那么直接会调用then 方法,执行第二个回调函数,并且将失败结果作为参数打印到控制台。

使用 promise.then 链式调用时,代码的结构更像是同步的线性代码,这样也解决了 callback 上存在的回调地狱问题。

catch

概念:catch 是定义在原型对象上 Promise.prototype 的方法。

作用:用于指定发生错误时的回调函数

参数

  • 状态改变成失败的回调函数,或者promise内部发生错误提供的回调函数。

代码示例

let promise = new Promise(function (resolved, rejected) {
    return rejected("状态更新为失败")
})
p2.then((data) => {
    return data
}).catch((err) => {
    return err
})

上述代码中,创建 promise 对象,然后执行 rejected 函数,由于我们使用了 catch 方法,当promise状态变为失败的情况下,会执行 catch 中定义的回调函数,然后将失败的结果作为参数传递给 catch 方法回调函数作为参数,最后打印出:状态更新为失败。

解析完这段代码之后,我们回忆一下,当状态发生错误时,我们可以使用在上面讲过的 then 方法中第二个参数来执行失败时的操作,那直接用 then 方法不就可以了吗?

其实,catch 方法是 .then(null, rejection) 的别名,也就是只采用 then 方法的第二个回调函数,当状态为失败时执行。

来对比一下区别

let promise = new Promise(function(resolved,rejected){
    resolved('成功了')
})

// 只使用 then
promise.then(function (value) {
    console.log(value)
}, function (error) {
    console.log(error)
})

// 使用catch
p2.then((data) => {
    return data
}).catch((err) => {
    return err
})

从代码结构上观察,使用 catch 方法更接近同步代码,代码可读性也会更好一些。

而且 catch 方法也可以捕获前面 then 方法中执行的错误。

所以,我们通常会使用 catch 方法用来处理错误情况,而且 catch 也会返回一个 新的 promise 对象,后面可以接着调用 then 方法。

finally 的作用

指定不管最后的 Promise 状态如何,都会执行的操作。

let promise=new Promise(...)
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

这里的示例代表:无论 promise 最后是成功还是失败,执行完 thencatch 回调函数后,最终都会执行 finally 回调函数。

注意:finally 方法的回调函数不传递任何参数,所以这也就导致不知道前面 promise 的状态是成功还是失败。

finally 是 then 方法的特例

promise
.finally(() => { ... });
// 等同于
promise
.then(
    result => {return result},
    error => { throw error}
);

手写一个 Promise

在了解完上面的异步编程和 Promise 知识后,我们现在来手写一个 Promise。

基本的 Promise 实现

先来总结一下 Promise 的特征

  1. Promise 对象是构造函数,所以需要 new 创建 promise 对象。

  2. Promise 对象的参数为函数,该函数有俩个参数:resolvedrejected

  3. 这俩个参数为回调函数:

  • resolved 函数:状态从 pending 变为 fulfilled

  • rejected 函数:状态从 pending 变为 rejected

  • Promise 对象状态一旦发生改变,就不会再变。

现在去实现 promise 就变得非常清晰了:

代码示例

class myPromise {
    constructor(fn) {
        this.status = 'pending'
        this.value = undefined
        this.reason = undefined
        const resolved = (value) => {
            if (this.status == 'pending') {
                this.status = 'fulfilled'
                this.value = value
            }
        }
        const rejected = (reason) => {
            if (this.status == 'pending') {
                this.status = 'rejected'
                this.reason = reason
            }
        }
        try {
            fn(resolved, rejected)
        } catch (err) {
            rejected(err)
        }
    }
}
let promise = new myPromise(function (resolved, rejected) {
    resolved('ok')
})

这里只是一个基本 myPromise ,Promise原型定义的方法都没有实现,接下来我们来实现常用的方法、

拓展 Promise API

then 方法

首先,then 是定义在原型对象上 Promise.prototype 的方法。

我们都知道调用 then 方法需要传递俩个参数:

  1. 第一个参数是状态变为成功时调用的回调函数。

  2. 第二个参数是状态变为失败时调用的回调函数。

所以我们需要确定参数是否传递正确:

then(onFulfilled,onRejected){
    const onFulfilled=typeof onFulfilled ==='function'?onFulfilled:value=>value
    const onRejected=typeof onRejected==='function'?onRejected:err=>{throw err}
}

我们此时需要获取 promise 状态,如果成功的话,调用第一个回调函数,反之,调用第二个回调函数

then(onFulfilled,onRejected){
    const onFulfilled=typeof onFulfilled ==='function'?onFulfilled:value=>value
    const onRejected=typeof onRejected==='function'?onRejected:err=>{throw err}
    // 判断 promise 的状态做不同处理
    if(this.status==='fulfilled'){
        
    }
    if(this.status==='rejected'){
            
    }
}

无论是 then 方法还是 catch 方法,都会返回一个新的 Promise 对象

then(onFulfilled,onRejected){
    onFulfilled=typeof onFulfilled ==='function'?onFulfilled:value=>value
    onRejected=typeof onRejected==='function'?onRejected:err=>{throw err}
    // 判断 promise 的状态做不同处理
    if (this.status === 'fulfilled') {
        return new myPromise((resolved, rejected) => {
            try {
                let result = onFulfilled(this.value)
                resolved(result)
            } catch (err) {
                rejected(err)
            }
        })
    }
    if (this.status === 'rejected') {
        return new myPromise((resolved, rejected) => {
            try {
                let result = onRejected(this.reason)
                rejected(result)
            } catch (err) {
                rejected(err)
            }
        })
    }
}

现在一个完整的 then 方法就已经完成了。

catch 方法

catch 方法用于捕获状态为失败的错误,然后执行相应的回调函数。如果运行中发生错误也会被 catch 捕获。

其实 catch 方法的实现与 then 方法第二个参数实现无异,上文中也提到过,catch 方法是 .then(null, rejection) 的别名。

代码示例

catch(onRejected){
    if (this.status === 'rejected') {
        return new myPromise((resolved, rejected) => {
            try {
                let result = onRejected(this.reason)
                rejected(result)
            } catch (err) {
                rejected(err)
            }
        })
    }
}

现在,我们实现了完整的 then 方法和 catch方法。

完整代码附上:

class myPromise {
    constructor(fn) {
        this.status = 'pending'
        this.value = undefined
        this.reason = undefined
        const resolved = (value) => {
            if (this.status == 'pending') {
                this.status = 'fulfilled'
                this.value = value
            }
        }
        const rejected = (reason) => {
            if (this.status == 'pending') {
                this.status = 'rejected'
                this.reason = reason
            }
        }
        try {
            fn(resolved, rejected)
        } catch (err) {
            rejected(err)
        }
    }
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
        if (this.status === 'fulfilled') {
            return new myPromise((resolved, rejected) => {
                try {
                    let result = onFulfilled(this.value)
                    resolved(result)
                } catch (err) {
                    rejected(err)
                }
            })
        }
        if (this.status === 'rejected') {
            return new myPromise((resolved, rejected) => {
                try {
                    let result = onRejected(this.reason)
                    rejected(result)
                } catch (err) {
                    rejected(err)
                }
            })
        }
    }
    catch(onRejected) {
        if (this.status === 'rejected') {
            return new myPromise((resolved, rejected) => {
                try {
                    let result = onRejected(this.reason)
                    rejected(result)
                } catch (err) {
                    rejected(err)
                }
            })
        }
    }
}
let promise = new myPromise(function (resolved, rejected) {
    resolved('ok')
})
promise.then(function (value) {
    return value
}).catch(function (err) {
    return err
})