面向小白编程:Promise 的浅入深出

127 阅读12分钟

前言

随着技术的发展和代码的复杂性增加,传统的回调地狱模式已经不能满足我们对代码结构清晰和可维护性的需求。Promise 的出现为我们带来了一种全新的编程模式,使得异步代码更加可读、可管理,同时提供了强大的错误处理机制。

在这篇文章中,我们将深入探讨 Promise 的基本概念、使用方法以及一些特性,帮助你更好地理解和运用 Promise,提升 JavaScript 异步编程的技能。

异步

在传统的同步编程中,每个操作都是按照顺序一个接一个地执行的。当遇到一个耗时的操作时,整个程序就会被阻塞,直到这个操作完成。而异步编程中,程序的执行不会等待某个操作完成,而是继续执行下一步操作。当程序遇到一个需要耗时完成的操作时,会先将其挂载起来,继续执行后面的代码。常见的例子有文件读取、网络请求、定时器等,setTimeout就是一个常见异步。

例如:

我们模拟一个场景,假如我们在饭店吃饭,刚点完菜,需要等待一会儿才会上菜,在饭前我们可以刷刷抖音。

function a() {
    setTimeout(function() {
        console.log('我的菜来了');
    },1000)
}
function b(){
    console.log('我在刷抖音');
}
a()
b()

我在刷抖音

我的菜来了

如果我们不用setTimeout,打印顺序应该是按照谁先调用谁先打印,那么应该是先上菜刷抖音,这明显与我们的需求相悖。而setTimeout就可以得到我们想要的结果,假设上菜需要 1 秒钟。这时候运行代码我们直接看到了'我在刷抖音',1秒后再看到'我的菜来了'。这就是一个异步事件。

具体来说,在函数 a 中,通过 setTimeout 创建了一个定时器,该定时器在 1000 毫秒后会执行一个回调函数,即打印 '我的菜来了'。然而,整个程序不会等待这个定时器计时结束,而是会立即执行后续的代码,包括调用函数 b

因此,执行顺序如下:

  1. 定义函数 a
  2. 调用函数 a,设置定时器,并继续执行后续代码,不等待定时器完成。
  3. 定义函数 b
  4. 在调用函数 a 后立即调用函数 b,并记录 '我在刷抖音'
  5. 定时器计时结束,执行定时器回调函数,记录 '我的菜来了'

这种行为展示了异步的特性,即代码的执行不是按照书写顺序线性进行的,而是通过事件循环等机制,使得某些操作可以在后台进行,而不阻塞后续代码的执行。这对于处理需要等待的操作是非常有用的,可以提高程序的性能和响应性。


回调

回调函数是处理异步任务的方法之一。

那如果我们现在就想先打印刷抖音再打印上菜怎么办?

b加个更长的定时器?这种方法虽然可以实现要求,但是多少有点没道理,那要是在异步任务非常多的时候,可想任务量多么庞大,而且不易维护。

这时候回调函数就可以解决我们的问题,我们将b的调用放到a函数体内,先打印上菜在执行b,这样就能达到先打印刷抖音再打印上菜。代码如下:

function a() {
    setTimeout(function() {
        console.log('我的菜来了');
        b()
    },1000)
}
function b(){
    console.log('我还在刷抖音');
}
a()

我的菜来了

我还在刷抖音

这种方式可以帮助我们处理异步操作,确保在特定条件满足时执行相应的代码逻辑。

回调地狱

回调地狱是指在异步编程中,当有多个连续的异步操作需要处理时,使用回调函数嵌套的方式导致代码变得复杂、难以理解和维护的情况。

举个栗子:

function a(callback) {
    setTimeout(function() {
        console.log('第一道菜来了');
        callback();
    }, 1000);
}

function b(callback) {
    setTimeout(function() {
        console.log('第二道菜来了');
        callback();
    }, 1000);
}

function c(callback) {
    setTimeout(function() {
        console.log('第三道菜来了');
        callback();
    }, 1000);
}

a(function() {
    b(function() {
        c(function() {
            console.log('全部菜都上齐了');
        });
    });
});

第一道菜来了

第二道菜来了

第三道菜来了

全部菜都上齐了

这个例子中,每个函数都接受一个回调函数作为参数,形成了多层嵌套的结构,这就是回调地狱。当我们在做大项目的时候,这点回调才算哪到哪,使得代码难以阅读和维护。但是在 ES6 之前,程序员都只能这样写代码的,直到 promise 的出现。


promise

- promise 的出现

经过上面对异步与回调的分析,我们用回调在处理异步时,回调地狱问题让大家感到困扰。为了解决这些问题,ES6 版本引入了 Promise 对象,优点可以总结如下:

  1. 解决异步编程困扰: Promise 主要用于处理异步计算,有效避免了回调地狱等问题,使得异步代码更易读、易维护。
  2. 顺序化异步操作: Promise 可以将异步操作队列化,按照期望的顺序执行。通过链式调用 then 方法,开发者能够更清晰地表达代码逻辑,提高了代码的可读性。
  3. 对象间传递和操作: Promise 可以在对象之间传递,使得异步操作更容易在不同模块之间协同工作。这种特性有助于处理异步操作的队列,提供更好的代码组织结构。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

- promise 的状态

Promise 对象具有三种状态:

  1. Pending(进行中): Promise 对象初始状态。它仍在进行中,可能在未来会变成其他状态。
  2. Fulfilled(已成功): 表示异步操作成功完成。当 Promise 进入此状态时,会调用 resolve 回调函数,并传递操作结果给 then 方法。
  3. Rejected(已失败): 表示异步操作失败。当 Promise 进入此状态时,会调用 reject 回调函数,并传递错误信息给 catch 方法。

Promise 的状态一旦改变,就会永远保持在这个状态,不会再发生变化。一旦进入 FulfilledRejected 状态,就称为settled(已定型)。在 settled 状态之后,Promise 就不再处于进行中(Pending)状态。

下面我们创造一个Promise实例

const promise = new Promise((resolve, reject) => {
    // 异步操作成功时调用 resolve
    // 异步操作失败时调用 reject
    setTimeout(()=>{ //做一些异步操作
        if (/* 异步操作成功 */) {
            resolve('成功的结果');
        } else {
            reject('失败的原因');
        }
    },1000)
});

promise.then(
    result => {
        // 处理成功的情况
        console.log('成功:', result);
    },
    error => {
        // 处理失败的情况
        console.log('失败:', error);
    }
);

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolverejectresolvereject 两个形参接收的是函数,接受的函数由引擎提供,不用自己部署。

  • resolve: 用于将 Promise 对象的状态从 Pending 变为 Fulfilled(成功)。通常在异步操作成功的地方调用,将异步操作的结果作为参数传递给 resolve,即上述代码的'成功的结果'
  • reject: 用于将 Promise 对象的状态从 Pending 变为 Rejected(失败)。通常在异步操作失败的地方调用,将错误信息作为参数传递给 reject,即上述代码的'失败的原因'

- then 方法

then 方法是 Promise 对象的一个关键方法,用于处理异步操作的结果。它接受两个参数:一个是成功时的回调函数,另一个是失败时的回调函数。并且最后会返回一个新的 Promise 实例化对象。

  • 第一个参数是成功时的回调函数,它接收 resolve 函数传递的结果作为参数,对应上述代码'result'
  • 第二个参数是失败时的回调函数,它接收 reject 函数传递的错误信息作为参数,对应上述代码'error'

Promise 对象的状态为 Fulfilled(成功)执行第一个回调, Promise 对象的状态为 Rejected(失败)执行第二个回调。

promise.then(
    function(result) {
        // 处理异步操作成功的情况
        console.log('成功:', result);
    },
    function(error) {
        // 处理异步操作失败的情况
        console.error('失败:', error);
    }
);

- catch 方法

catch 方法是 Promise 对象的一个方法,用于捕获 Promise 链中任何一个 Promiserejected(失败)时的错误。catch 方法接收一个回调函数,该回调函数会处理被 rejectedPromise 的错误信息。

下面是一个简单的例子:

function readFileAsync() {
    return new Promise(function(resolve, reject) {
        // 模拟异步读取文件
        setTimeout(function() {
            if (Math.random() > 0.5) { // 模拟成功或失败
                const data = '文件内容';
                resolve(data); // 成功时调用 resolve,并传递数据
            } else {
                reject(new Error('读取文件失败')); // 失败时调用 reject,并传递错误信息
            }
        }, 1000);
    });
}

// 调用异步函数
readFileAsync()
    .then(function(data) {
        console.log('文件内容:', data);
    })
    .catch(function(error) {
        console.error('发生错误:', error);
    });

在这个例子中,catch 方法捕获了 readFileAsync 返回的 Promise 对象在 'rejected' 状态时的错误。如果异步操作失败,会调用 catch 方法中的回调函数,输出错误信息。如果异步操作成功,不会触发 catch,而是会继续执行后面的 then 中的回调。

使用 catch 方法有助于集中处理Promise 链中任何地方发生的错误,避免了在每个 then 方法中都写一遍错误处理逻辑。这提高了代码的可读性和可维护性。


- promise 的简单应用

通过简单了解了promise 的状态、两个参数、then方法和、catch方法,我们就能进行简单的应用了。

就用之前回调地狱的那个例子吧:

function a() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('第一道菜来了');
            resolve();
        }, 1000);
    });
}

function b() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('第二道菜来了');
            resolve();
        }, 1000);
    });
}
function c() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('第三道菜来了');
            resolve();
        }, 1000);
    });
}
a()
  .then(b)
  .then(c)
  .then(()=>{
      console.log('全部菜都上齐了');
  })
  .catch( error => {
      console.log(error);
  });

使用Promise可以将每个异步操作封装成一个Promise对象,并通过链式调用then的方式处理它们的执行顺序,并且通过catch捕获错误。这样可以使代码更加清晰可读。相比于回调,好的不要太多,即不存在回调地狱的问题,又方便代码维护。


下面我们拓展两个Promise常用的方法。

- Promise.all 方法

Promise.allPromise 对象的一个静态方法,用于将多个 Promise 实例包装成一个新的 Promise 实例。这个新的 Promise 实例在所包含的所有 Promise 都成功时才会成功,只要有一个 Promise 失败就会失败。Promise.all 方法接受一个包含 Promise 对象的可迭代对象(通常是数组)作为参数。

下面是一个简单的例子:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第一道菜上了');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第二道菜上了');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第三道菜上了');
    }, 1500);
});

const allPromise = Promise.all([promise1, promise2, promise3]);//调用Promise.all方法

allPromise
    .then(results => {
        console.log('菜上齐了:', results);
        // 输出: 菜上齐了: [ '第一道菜上了', '第二道菜上了', '第三道菜上了' ]
    })
    .catch(error => {
        console.error('至少有一道菜没上:', error);
    });

在这个例子中,Promise.all([promise1, promise2, promise3]) 包装了三个 Promise,只有当这三个 Promise 都成功时,allPromise 才会成功。结果值是一个数组,包含了每个 Promise 成功时的结果值。

需要注意的是,如果其中任何一个 Promise 失败,allPromise 就会立即失败,并返回第一个失败的 Promise 的错误信息。这种机制保证了当一组相关联的异步操作都成功时,才会执行后续的操作。


- Promise.race 方法

Promise.race Promise 对象的另一个静态方法,它接受一个包含 Promise 对象的可迭代对象(通常是数组)作为参数。与 Promise.all 不同的是,Promise.race 在所包含的 Promise 对象中只要有一个对象完成(无论是成功还是失败),race 返回的 Promise 就会立即完成,且结果值或失败原因来自于第一个完成的 Promise

下面还是做菜的例子,演示了 Promise.race 的使用:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第一道菜');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第二道菜');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第三道菜');
    }, 1500);
});

const racePromise = Promise.race([promise1, promise2, promise3]);

racePromise
    .then(result => {
        console.log('第一个成功的菜:', result);
        // 输出: 第一个成功的菜: 第一道菜
    })
    .catch(error => {
        console.error('第一个失败的菜:', error);
    });

在这个例子中,Promise.race([promise1, promise2, promise3]) 包装了三个 PromiseracePromise 会在其中任何一个 Promise 完成时立即完成。结果值或失败原因来自于第一个完成的 Promise。

Promise.race 主要用于竞速多个异步操作,获取最先完成的结果。这对于设置异步操作的超时机制、获取多个异步结果中最快的那个等场景非常有用。

最后

通过本文的介绍,我们深入学习了 Promise 的基本概念、使用方法以及相关的高级特性。Promise 的出现极大地改善了异步编程的体验,使得我们能够以更清晰、更优雅的方式处理异步操作。希望这篇文章能够帮助你更深入地理解 Promise,提高异步编程的水平。让我们一起迎接 JavaScript 异步编程的未来,创造更出色的代码!

我的Gitee:    CodeSpace (gitee.com)

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!