你的Promise VS 我的Promise,好像不一样。--JS基础篇(十二)

259 阅读9分钟

写在前面

深入JS腹地,体会Event Loop的精妙设计--JS基础篇(十一) - 掘金 (juejin.cn)

本期我们来看一个ES6中引入的重量级对象,Promise。在此之前,建议先理解事件循环机制(Event Loop)。⬆️⬆️⬆️

我们从以下三个方面来聊聊:

文末直达

正文

一、异步编程的发展

  • 早期的同步执行: JS最初主要用于简单的页面交互和动态效果,执行靠单线程。这让代码只能顺序执行,页面有时会失去响应。

  • XMLHttpReequest的引入 XMLHttpReequest的引入,使得JS可以通过AJAX(异步的Javascript和XML)技术进行异步通信。JS进入了一个全新的时代。

  • 回调函数: JS广泛采用回调函数来处理异步任务的结果,回调函数在异步任务完成后执行,返回数据或者报错等。

  • Promise的出现: 在ES6中,JS官方引入Promise对象作为处理异步任务的一种新方式。这极大的避免了回调函数带来的回调地狱等问题。

  • async/await的出现: 在ES8中,async/await语法诞生,进一步简化了异步任务的编写方式,使得异步代码更像同步代码,有更好的可读性和维护性。

以上异步编程机制和语法让JS在执行耗时操作时不会阻塞主线程,能够确保在处理网络请求时不影响用户与页面的交互,这对提高用户体验、程序性能具有重要意义。

二、传统异步编程带来的挑战

  • 回调地狱(Callback Hell)

异步操作通常使用回调函数来处理,多个异步操作嵌套过深会导致代码结构复杂,难以维护。

// 回调地狱

function getUser(userId, callback) {
    setTimeout(() => {
        const user = { id: userId, name: 'John' };
        callback(user);
    }, 1000);
}

function getUserPosts(user, callback) {
    setTimeout(() => {
        const posts = [
            { userId: user.id, postId: 1, content: 'Post 1' },
            { userId: user.id, postId: 2, content: 'Post 2' }
        ];
        callback(posts);
    }, 1500);
}

function getComments(post, callback) {
    setTimeout(() => {
        const comments = [
            { postId: post.postId, commentId: 1, text: 'Comment 1' },
            { postId: post.postId, commentId: 2, text: 'Comment 2' }
        ];
        callback(comments);
    }, 2000);
}

在这个示例中,getUser,getUserposts,getComments都是模拟的一个异步任务,它们用于获取用户信息,用户发的帖子,用户帖子的评论。如果执行结果通过回调函数来传递,那么这种嵌套行为让代码结构变得复杂,不易扩展。就像这样:

// 使用回调函数处理多个异步操作
getUser(1, function(user) {
    console.log('User:', user);
    getUserPosts(user, function(posts) {
        console.log('Posts:', posts);
        const firstPost = posts[0];
        getComments(firstPost, function(comments) {
            console.log('Comments for post 1:', comments);
        });
    });
});

多个异步操作依次执行,并且需要依赖前一次异步操作的结果时,若使用传统的回调函数处理,会导致异步代码呈现深层次的调用,进而形成回调地狱。那么有解决办法吗?当然有,继续看看传统异步编程的局限性。

  • 错误处理的复杂化

在异步编程中,异步任务的回调函数的执行时机会受到多种因素的影响(网络延迟、资源可用性等)。这种不确定性可能导致错误发生时,当前代码的执行上下文已经无法约束错误所在区域,导致错误的处理变得更加困难。

  • 并发(竞态)

多个异步操作同时进行(在浏览器或Node.js环境),也就是多线程并发的状态下,多个异步任务中的共享数据可能在不同的时间点被读取或者修改,会导致各异步任务的数据不同步,导致结果的正确性受到影响。

🌰:假设有两个异步操作 A 和 B 都要读取和修改同一个变量。如果操作 A 读取变量后,操作 B 立即也读取并修改了该变量,那么操作 A 的修改可能会被操作 B 覆盖,导致最终的数据状态与预期不符。

三、Promise

Promise是ES6中引入的一种处理异步操作的对象,他实际上包含了异步操作的结果以及状态。当我们使用Promise处理异步任务时,只需要向构造函数Promise()中传入一个执行器,这个执行器函数里面应该包含了整个异步任务的逻辑

1️⃣Promise的状态

Promise是一尚未完成但是最终会完成的内置对象,它可以处于以下三个状态:

  • pending(待定):初始状态,既不是成功也不是失败。
  • fulfilled(已成功):表示异步操作完成且成功。
  • rejected(已失败):表示异步操作完成但失败了,通常是因为出现错误。

以上三个状态构成了Promise这个对象的完整生命周期。

2️⃣Promise构造函数的方法

new Promise(executor);

如上,Promise的构造函数接收一个执行器(executor function),创建对象时,该函数在其内部立即执行,即使Promise对象还未创建完成。执行器函数里包含2个参数resolvereject,这两个参数是构造函数原型里的方法,用于改变Promise的状态:

示例:

//箭头函数为执行器
const promise = new Promise((resolve, reject) => {
//模拟异步操作逻辑
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('Success!');
        } else {
            reject('Failed!');
        }
    }, 1000);
});

  • 状态1 resolve(value):当异步操作成功时调用该函数,将Promise的状态由pending改为fulfilled,并且将value作为参数传递给.then()方法的回调函数。

  • 状态2 reject(reason):当异步操作成功时调用此函数,将Promise的状态由pending改为rejected,并将reason作为参数传递给.catch()或者.then()的回调函数。

3️⃣Promise的实例方法

Promise对象中有几个实例方法用于处理异步操作成功或失败后的工作。

1.then(onFulfilled,onRejected)

Promise.prototype.then()方法允许我们在异步操作结束后分别执行不同的函数。他接受两个参数:

  • onFulfilled:当Promise状态变为fulfilled(已成功)时调用的函数。这个函数接收resolve函数传递的值作为参数。

  • onRejected:当Promise状态变为rejected(已失败)时调用的函数。这个函数接收reject函数传递的错误作为参数。

这两个参数都是可选参数,省略的那个在.then()方法中将不被调用。

const promise = new Promise((resolve, reject) => {                             resolve('Success'); 
}); 

promise.then( 
       result => console.log(result), // 输出  "Success" 
       error => console.error(error) 
 );

2.catch(onRejected)

Promise.prototype.catch()是.then()方法的一个简写形式,只用于Promise对象中异步任务失败的情况。它等价于调用.then(null,onRejected)

const promise = new Promise((resolve, reject) => {
    reject(new Error('Failure'));
});

promise.catch(error => console.error(error)); // 输出 "Error:Failure"

3..finally(onFinally)

Promise.prototype.finally() 方法允许你指定一个回调函数,无论 Promise 的最终状态是成功还是失败,这个函数都会被执行。这在需要进行一些清理工作,如关闭文件、释放资源等场景下非常有用。

这里的onFulfilled、onRejected、onFinally都是函数,同时也作为Promise在异步操作结束后的行为代表

来看一下流程图:

image.png

流程:

  • pending:promise开始处于pending状态。
  • Operation(async):执行异步操作,如网络请求、定时器等。
  • Success:如果操作成功,则Promise状态变为resolved
  • Failure:如果操作失败,则Promise状态变为rejected
  • Promise(resolved):如果Promise状态为resolved,调用then方法。
  • Promise(rejected):如果Promise状态为rejected,调用catch方法。

4️⃣Promise的链式调用

我们着重看一下.then()这个方法,这个方法主要用于链接异步操作,由Promise对象调用。来看一个例子:

function first() {
    //三种状态 pending准备 resolved触发 rejected
    return new Promise((resolve,reject) => {//status:pending
        setTimeout(() => {
          console.log("我的掘金1级了");
          resolve();//改变状态
        }, 2000);
    })
}


function third(){
    console.log("我的掘金3级了");
}

function second() {
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            console.log("我的掘金2级了");
            resolve();
        }, 1000);
    })
}

要如何调用才能正确打印结果呢。我们来试一试:

first().then(() => {
    second();
}).then(()=>{
    third();
});
//掘金1级
//掘金3级
//掘金2级

这样调用正确吗?

有点小错误。我们联系到Promise的执行和返回机制想想。

在Pomise链中,每个.then()方法都会返回一个新的Promise对象,新的Promise对象的状态由前一个Promise对象的执行结果决定。当我们连续调用.then()方法,将一系列异步操作串联起来形成了一个链式结构。

上面的错误关键在于:
second()函数没有正确返回,导致第二个.then没有找到second()的Promise,反而顺着Promsie链找到了前面的Promise,且状态为resolved,所以第二个.then()里的third()在打印完第一句后直接执行了。

这样修改后就好了:

first().then(() => {
    return second();
}).then(()=>{
    third();
});

我们再详细梳理一遍:

  • first()
    • 返回第一个Promise,在2秒后resolved
  • 第一个.then()
    • 当first()的Promise状态为resolved时,回调函数被执行。
    • 在这个回调函数中,继续调用second(),并且返回它的Promise。
    • 由于second()的Promise,Promise链会等待second()的Promise状态改变。
  • 第二个.then()
    • second()的状态改变为resolved后,回调函数被执行。
    • 回调函数在second()执行完后调用third()

.then()达到了将异步代码捋成同步代码的效果。*

🤔思考:前面这份代码如何使用Promise对象来处理呢。

4️⃣async/await语法糖

async/await语法糖是ES8引入的,是一种建立在Promise之上的更为直观的异步编程方式。

之前的写法可以改为这样:

//部分
function delay(ms) { 
    return new Promise(resolve => setTimeout(resolve, ms)); 
} 

async function first() { 
    await delay(2000); 
    console.log("我的掘金1级了"); 
}

调用时只需要await一下,执行顺序就可以严格规定好。

sync function execute() { 
    await first(); 
    await second(); 
    third(); 
}

这样,有着同步代码的直观,这样就不需要嵌套或者链式调用.then和.catch了。

另外,对于async/await语法糖,可以使用try/catch来捕获错误:

async function execute() { 
    try { 
        await first(); 
        await second(); 
        await third(); 
    } catch (error) { 
        console.error("出错了:", error); 
        throw(error);
    } 
 }

这样,所有异步任务的错误都能被捕获到。

总结

以上就是所有内容。不过,关于Promise的用法还有很多很多,大家可以去Promise - JavaScript | MDN (mozilla.org)看看👀。如果你觉得本文对你有帮助,还请点个赞,这将是我持续创作的动力。