从回调地狱到异步之王:小白初次感受JS Promise的魅力!

748 阅读8分钟

一、Promise的引入

首先,我们先来看个例子。

function xq() { 
    return new Promise((resolve, reject) =>{
        setTimeout(() => {
            console.log('张三去相亲!');
            resolve('相亲成功!')
        },2000)
    })
}
function marry() {
    return new Promise((resolve, reject) =>{
        setTimeout(() => {
            console.log('张三结婚了!');
            resolve()
        },1000)
    })
}
function baby() {
    setTimeout(() => {
        console.log('小张出生了!');
    },500)
}
xq()
marry()
baby()

666.jpg 完犊子了!我代码里明明是先调用相亲函数,再调用结婚函数,最后才是生娃的,怎么输出顺序乱套了呢?

我们再来看一个例子。

console.log('1');
console.log('2');
setTimeout(() => {
    console.log('3')
},1000)
console.log('4');
console.log('5');

111.jpg 输出结果是12453,为啥不是按照顺序12345这样来输出呢?我代码里明明是按这个顺序写的呀,有什么办法能解决这个问题呢?诶,试一下回调函数吧!

setTimeout(function () {
    console.log('1');
    setTimeout(function () {
        console.log('2');
        setTimeout(function () {
            console.log('3');
            setTimeout(function () {
                console.log('4');
                setTimeout(function () {
                    console.log('5');
                })
            })
        })
    })
})

222.jpg 输出结果是12345,好了,这下输出顺序的问题倒是解决了,但是也成俄罗斯套娃了,如果我要输出更多东西呢?你不觉得这代码多少有点不优雅吗?

传统的回调函数方式可能会导致代码嵌套层次深、难以维护和理解,像这样回调函数里面嵌套回调函数,我们称为“回调地狱”(Callback Hell)。本文将介绍现代JavaScript中的一种异步编程技术Promise,尝试摆脱回调地狱,写出更加优雅和可维护的异步代码。

二、什么是 Promise?

Promise 是 JavaScript 提供的一种用于处理异步操作的对象,比传统的解决方案——回调函数和事件——更合理和更强大。它可以代表一个异步操作的最终完成或失败,并可以将其结果传递给相应的处理函数。

Promise 有三个状态:

  • Pending(进行中): 初始状态,表示异步操作正在进行中,尚未完成。
  • Fulfilled(已完成): 表示异步操作已经成功完成,并且有一个返回值。
  • Rejected(已失败):表示异步操作已经失败,可能由于某种错误。

一旦 Promise 进入了 Fulfilled 或 Rejected 状态,就被称为已 resolved(已定型),状态将不再改变,会一直保持这个结果。可以通过 .then() 或 .catch() 方法来处理 Promise 的结果。

三、Promise的一些常用API

1.Promise.resolve()和Promise.prototype.then()

首先我们来看一段代码,如下所示:

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
        },2000)
    })
}
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
        },1000)
    })
}
a().then(() => {
    b();
})

11.jpg 执行结果是输出aaa,但是我们的本意是想使得b函数在a函数之后被执行,可是这里并没有执行b函数,为什么呢?我们接着再看一段代码:

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
            let res = '我是a函数里的resolve信息';
            resolve(res);
        },2000)
    })
}
function b(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
            console.log(data);
        },1000)
    })
}
a().then((res) => {
    console.log(res);
    b(res);
})

22.jpg then方法可以为Promise实例添加状态改变时的回调函数,定义在原型对象Promise.prototype上。如果有resolve函数的调用,then里面的回调函数就会被触发,否则不会。所以上面的例子中之所以没有执行b函数,是因为没有调用resolve函数。

如果a函数里有信息想拿给b函数去使用,可以通过resolve函数传递参数,这样就能很方便的把一个函数内部的变量传递给另外一个函数使用。

2.Promise.reject()和Promise.prototype.catch()

我们继续来看一段代码,如下所示:

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
            reject('我是a函数里的reject信息')
        },2000)
    })
}
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
        },1000)
    })
}
a().then((res) => {
    console.log(res);
    b();
})

33.jpg 好家伙,不仅b函数没有执行,还报错了!这是为什么呢?

很多时候我们在接口请求时要用到Promise对象,接口请求可能成功也可能失败,成功就用resolve处理结果,失败就用reject处理结果。因为这里调用的是reject函数,在catch里执行的,而不是在then里执行的,catch这个方法就是专门用来捕获错误。resolve对应thenreject对应catch

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
            reject('我是a函数里的reject信息')
        },2000)
    })
}
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
        },1000)
    })
}
a()
    .then((res) => {
        console.log('then: ', res);
        b();
    })
    .catch((err) => {
        console.log('catch: ', err);
    })

44.jpg

3.Promise.prototype.finally()

finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作,不管此时是thencatch中的哪一个执行,finally一定会执行。主要用于在处理完Promise结果后执行一些清理操作,例如关闭文件或释放资源。下面的代码中省略了之前相同的部分,节省篇幅。

a()
    .then((res) => {
        console.log('then: ', res);
        b();
    })
    .catch((err) => {
        console.log('catch: ', err);
    })
    .finally(() => {
        console.log('finally里的操作执行啦!');
    })

55.jpg

4.Promise.all()

Promise.all() 方法接收一个包含多个 Promise 对象的数组,并返回一个新的 Promise 对象,该对象在数组中的所有 Promise 对象都变为已解决状态时才会变为已解决状态,结果值是一个包含所有 Promise 对象结果值的数组,顺序与输入数组一致。

a().all()

66.jpg 报错,输出a().all() is not a function,我们不能直接a().all(),实例对象是不能直接调用all方法的,因为all方法没有挂在原型上,没被隐式继承,实例对象是访问不到的。正确写法如下:

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
            resolve('我是a函数里的resolve')
        },1000)
    })
}
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
            resolve('我是b函数里的resolve')
        },2000)
    })
}
function c() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('ccc');
        },500)
    })
}
Promise.all([a(), b()]).then(c)

77.jpg c依赖ab的执行结果,只有等ab都执行完后才执行。

5.Promise.race()

Promise.race()方法接收一个包含多个 Promise 对象的数组,并返回一个新的 Promise 对象,该对象在数组中的任何一个 Promise 对象变为已解决或已拒绝状态时就会变为相应的状态,并使用第一个解决或拒绝的 Promise 对象的结果或原因作为结果。

Promise.race([a(), b()]).then(c)

88.jpg race有比赛、竞争的意思,当发送接口请求时,有可能是a先拿到数据结果,也有可能b先拿到数据结果,这个跟HTTP、TCP、UDP有关,反正c不知道谁先谁后,所以就调用race方法,有结果执行出来的第一时间就拿到该结果。这里a需要花1秒,b需要2秒,a更快,所以c跟在a之后执行。

6.Promise.any()

Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个Promise变成rejected状态而结束,必须等到所有参数Promise变成rejected状态才会结束。

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
            reject('我是a函数里的resolve')
        },1000)
    })
}
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
            reject('我是b函数里的resolve')
        },2000)
    })
}
function c() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('ccc');
        },500)
    })
}
Promise.any([a(), b()]).then(c)

10.jpg 此时,a函数和b函数都是reject,都执行失败,所以then里的c函数就不执行,程序报错,c函数依赖前面函数的执行结果,但凡a函数和b函数中有一个执行成功,c函数就会执行成功。any就是任何的意思嘛,有一个就行。

四、Promise的实际应用

现在让我们通过一个实际的例子来展示 Promise 的用法。假设我们有一个简单的异步操作,从服务器获取用户信息,并在获取成功后进行一些处理。下面是使用 Promise 的示例代码:

// 模拟从服务器获取用户信息的异步操作
function getUserInfoFromServer(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟异步请求成功
      const userInfo = {
        id: userId,
        name: "zhangsan",
        age: 20
      };
      resolve(userInfo); // 将结果传递给成功处理函数
    }, 1000);
  });
}

// 使用 Promise 处理异步操作的结果
getUserInfoFromServer(666)
  .then(userInfo => {
    console.log("用户信息:", userInfo);
    // 进行其他处理
  })
  .catch(error => {
    console.error("获取用户信息失败:", error);
  });

在上面的示例中,我们通过 getUserInfoFromServer()函数模拟了一个异步操作,并使用 Promise 处理了成功的结果。如果异步操作成功,then()方法的回调函数将被调用,并传递用户信息作为参数;如果异步操作失败,catch()方法的回调函数将被调用,并传递错误信息作为参数。

下面是一个使用Promise.all处理动态数量的Promise的例子:

// 动态生成 Promise 数组
function generatePromises(num) {
  const promises = [];
  for (let i = 1; i <= num; i++) {
    promises.push(
      new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
          if (i % 2 === 0) {
            resolve(`Promise ${i} 已完成`);
          } else {
            reject(`Promise ${i} 已失败`);
          }
        }, Math.random() * 1000);
      })
    );
  }
  return promises;
}

const num = 5; // 动态生成 5 个 Promise 对象
const promises = generatePromises(num);

// 使用 Promise.all 处理动态数量的 Promise
Promise.all(promises)
  .then(results => {
    console.log(results); // 所有 Promise 都成功解决时的结果数组
  })
  .catch(error => {
    console.error(error); // 第一个被拒绝的 Promise 的错误值
  });

在上面的代码示例中,generatePromises函数用于动态生成一定数量的Promise对象,每个Promise对象都可能成功解决或被拒绝。然后,将这些生成的Promise对象作为参数传递给Promise.all方法,Promise.all方法会返回一个新的Promise,在所有输入的Promise对象都成功解决时才会被解决,如果任何一个输入的Promise对象被拒绝,则返回的Promise也会被拒绝,并将第一个被拒绝的Promise的错误值作为拒绝原因。

五、结语

Promise 是 JavaScript 中处理异步操作的一种强大工具,它可以使异步编程更加优雅和简洁。在实际的开发中,可以进一步深入学习 Promise,并结合实际项目中的异步操作来灵活运用,合理地运用 Promise,从而写出更加优秀的 JavaScript 代码,有效地提高代码的可读性、可维护性和健壮性。

感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!如有问题欢迎大家指正,祝大家生活愉快!