JS系列之 Promise 及链式调用顺序

5,196 阅读12分钟

1. Promise 是什么

还记得第一次听到 JS 里有个这个东西,本能的中式英语翻译了一下,许诺?
我好好的搬个代码,我许什么诺?花里胡哨的名字
现在回头看看,它状态不可更改的特性也蛮符合这个名字,难道... 莫非这就是程序员的浪漫!!
(狗头)(我不信,阿里 pX 什么事的,果然男人有钱会变坏。我不会,我缺的就是 money)

好了,说回正题,让我们相信爱情的美好,看看这个 Promise 到底是什么

Promise 是异步编程的一种解决方案,比传统的回调函数和事件相比更加的合理,强大

1.1 Promise 产生的原因,解决的痛点

在实际项目中,如果遇到这样一个情况:
我们需要根据第一个网络请求的结果,再去执行第二个网络请求,拿着第二个请求的结果再去执行第三个请求...

不使用 Promise 的代码大概是下面这样子:

请求1 (function (请求结果1) {
  请求2 (function (请求结果2) {
    请求3 (function (请求结果3) {
       ...
    })
  })
})

这样看其实还好,没有很恐怖!
但是如果业务需求再复杂一些,这个请求就要一直叠加下去。
而且更糟糕的是,实际应用中,每一个请求都会对请求数据进行处理,这样代码就会变得十分难看臃肿,而且基本上这段代码无法复用。
这就是大名鼎鼎的 回调地狱

JS 实现异步是通过回调函数实现的。就是把任务的第二段单独写在一个函数里,等到第一段有了结果需要执行第二段时,直接调用这个回调函数。
Promise 就是为了解决 回调地狱 问题提出的,它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套改变成链式调用。

21世纪了,打代码一定要有所追求,一定要优雅。所以很多大佬就想办法解决这个问题,要用一个更加优雅的代码组织方式解决异步嵌套的问题。想到了类似下面这种同步的写法,于是 Promise 规范就诞生了。

let 请求结果1 = 请求1();
let 请求结果2 = 请求2(请求结果1);
let 请求结果3 = 请求3(请求结果2);
...
// 还可以复用某一个请求
let 请求结果4 = 请求3(请求结果1);
let 请求结果5 = 请求2(请求结果3);

当然 Promise 也有它的不足,Promise 最大的问题是会代码冗余,一大堆then,原来的语义不清晰,这个之后我们再说。

那下面我们来看看什么是 Promise 规范

1.2 Promise 规范

Promise 构造函数:

Promise 对象是一个构造函数,用来生成 Promise 实例

// 创建一个 promise 实例
const promise = new Promise(function (resolve, reject) {
  // ... some code 
  if (/* 异步操作成功 */) {
    resolve(value)
  } else {
    reject(error)
  }
})

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject.。它们又是两个函数。
resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

promise 常规写法:

new Promise (请求1)
  .then(请求2(请求结果1))
  .then(请求3(请求结果2))
  .then(请求4(请求结果3))
  .then(请求5(请求结果4))
  .catch(...// 处理异常)

比较一下这种写法和上面的回调式的写法。我们不难发现,Promise 的写法更为直观,并且能够在外层捕获异步函数的异常信息。

Promise 常用的方法有哪些?

类方法:

1. Promise.resolve

Promise 对象必须 reslove 一个值才可以被之后的 then 接收。而 then 中的函数要 return 一个结果或者一个新的 Promise 对象 ( then 本身就会返回一个新 promise,如果没有 return 数据,下一个 then 接收的值就是 undefined ),才可以让之后的 then 回调接收

let p = new Promise((reslove) => {
  reslove(2)
  // return 2 无法传递给下面的then
})
p.then(v => v).then(v => console.log(v)) // 2
2. Promise.reject
3. Promise.race

多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败

4. Promise.all

将多个 Promise 实例,包装成一个新的 Promise 实例。 const p = Promise.all([p1, p2, p3])
多个 Promise 任务同时执行。如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。
当这个数组里的所有 promise 对象全部变为 resolve 或者有一个 rejected 状态出现的时候,它才会去调用 .then 方法,它们是并发执行的。

实现 Promise.all
先看下 promise.all 的用法

  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象。
  2. 如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
  3. 如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调
  4. 只要有一个失败,状态就变为 rejected,返回值将直接传递给回调。
  5. all() 的返回值也是新的 Promise 对象。
function promiseAll(promises) {
	return new Promise(function (resolve, reject) {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }

    let promiseLength = promises.length;
    let resolveCounter = 0;
    let resolveValues = new Array(promiseLength);

    for (let i = 0; i < promiseLength; i++) {
      Promise.resolve(promises[i]).then(function (value) {
        resolveCounter++;
        resolveValues[i] = value;

        if (resolveCounter == promiseLength) {
          return resolve(resolveValues);
        }
      }, function (err) {
        return reject(err);
      })
    }
  })
}

这里参考了大佬的文章 实现 promise.all

如何实现即使有失败状态,依然返回所有数据? 对每个传入 all 的 Promise 对象都加上 .then

Promise.all([
	Promise.resolve(1).then((res) => ({ status: 'suc', res }), (err) => ({ status: 'err', err })), 
	Promise.reject(2).then((res) => ({ status: 'suc', res }), (err) => ({ status: 'err', err })), 
	Promise.resolve(3).then((res) => ({ status: 'suc', res }), (err) => ({ status: 'err', err }))
]).then(res => console.log(res), err => console.log(err))
// [{status: "suc", res: 1},{status: "err", err: 2},{status: "ok", res: 3}]

// 函数封装
function handlePromise(promises) {
    return promises.map(promise =>
      promise.then(res => ({ status: 'suc', res }), err => ({ status: 'err', err }))
    )
  }
Promise.all(handlePromise([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)]))
	.then(res => console.log(res),err=>console.log(err))

这里就不一一展开每个类方法的具体使用方法了,这些可以看阮一峰大神的 ES6 学习。请点击这里

实例方法:

  1. Promise.prototype.then

作用是为 Promise 实例添加状态改变时的回调函数。
第一个参数是reslove状态的回调函数
第二个参数(可选)是rejected状态的回调函数

then 方法返回的是一个新的 Promise 实例,因此可以采用链式写法

then 这两个参数的返回值可以是一下三种情况中的一种

  • return 一个同步的值,或者undefined(没有返回一个有效值时,默认返回 undefined);
    返回一个 resolved 状态的 Promise 对象,值为 同步的值undefined
  • return另一个 Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回
  • throw一个同步异常,then方法将返回一个rejected状态的Promise,值是该异常
.then(() => {
  ...
  return 2;
  return Promise.resolve(2); // 与上面一样
})
  1. Promise.prototype.catch

.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数

1.3 Promise 优缺点

优点

  1. 统一异步 API。

  2. 解决了回调地狱的问题,将异步操作以同步的流程表达出来。

  3. 链式调用是 Promise 的一大优点,事件处理不可以链式处理。

  4. 带来了较好的错误处理方式。

缺点

  1. 无法取消 Promise,一旦新建就会立即执行。并且调用 resolvereject 并不会终结 Promise 的参数函数的执行。

    new Promise((resolve, reject) => {
      resolve(1);
      console.log(2);
    }).then(r => {
      console.log(r);
    });
    // 2 1 console.log(2)还是会执行 最好是 return resolve(1)
    
  2. 不设置回调,Promise 内部抛出的错误不会反应到外部。

  3. 当处于 Pending 状态时,无法得知进展,是刚开始还是快结束。

  4. Promise 真正执行回调时,定义 Promise 的代码部分实际已经运行完了,所以 Promise 的报错堆栈上下文不友好。

1.4 Promise 的实现

简单实现几个重要属性

function myPromise(constructor) {
  let self = this;
  self.status = 'pending';
  self.value = undefined;
  self.reason = undefined;

  function resolve(value) {
    if (self.status === 'pending') {
      self.value = value;
      self.status = 'resolved';
    }
  }

  function reject(reason) {
    if (self.status === 'pending') {
      self.reason = reason;
      self.status = 'rejected';
    }
  }

  // 捕获构造异常
  try {
    constructor(resolve, reject)
  } catch (e) {
    reject(e)
  }
}

myPromise.prototype.then = function (onFullfilled, onRejected) {
  let self = this;
  switch (self.status) {
    case 'resolved':
      onFullfilled(self.value);
      break;
    case 'rejected':
      onRejected(self.reason);
      break;
    default:
  }
}

这里只是简单模仿下 Promise , 完整的实现机制很复杂,需要钻进去。网上文章很多

2. promise 应用

2.1 Promise 特性

  • 立即执行性
  • 内部状态机,三种状态
  • 状态的不可逆性
  • 链式调用
  • then 回调异步性,微任务,then 回调是交替执行的
  • Promise.resolve() 返回值
  • resolve 拆箱, reject 不拆箱

通过 8 题看下 Promise 特性 Click Here

2.2 async / await

详细内容看这里 async 函数

与 promise 区别

  1. 定义上
  • Promise 是对象,用于表示一个异步操作最终完成或失败,及其结果值

  • async function 是声明语句,定义一个返回 AsyncFunction 对象的异步函数,它会通过一个隐式的 Promise 返回其结果。

  • await 是表达式,用于暂停当前异步函数的执行,等待 await 那个Promise 对象执行完成。

  1. 中断机制上
  • Promise 内部是状态机,一旦运行无法中止

  • async function 使用 await 中断程序

与 promise 关系

async / await 目的是简化使用多个 promise 时的同步行为,并对一组 Promises 执行某些操作。

Promises 类似结构化回调,async / await 更像结合了 Generators 和 Promise

使用场景

一般在异步操作时使用,搭配使用

  1. Promise 提供的工具函数对应的场景,如 Promise.all 并执行一组 Promises

  2. async / await 避免了繁杂的 Promise 链式调用,并且更加语义化。

举个栗子:假如有这么个需求:有三个独立的请求,请求成功后改变数据,我们需要再请求成功后拿到最新数据,然后再去执行另一个函数。

function 1 () { return ...new Promise }
function 2 () { return ...new Promise }
function 3 () { return ...new Promise }
funciton 4 () { ... // 在1,2,3之后处理 }

// 在此触发上述函数
async function click () {
  await 1();
  await 2();
  await 3();
  4()
}

2.3 Promise 与事件循环

Promise 内部的代码是立即执行的。但是 Promise.then 是微任务,复杂的链式调用非常考验对事件循环的分析。还要注意 return 影响的范围。更多分析看下面的题目

1. Promise 链式调用的执行顺序

我们通过一道妈妈做饭的题目来看 Promise 的链式调用 按照书写习惯,从上到下 log 妈妈做饭的过程

new Promise((resolve, reject) => {
  console.log('妈妈要做饭');
  resolve();
}).then(() => {
  console.log('要买菜');
  new Promise((resolve, reject) => {
    console.log('去菜市场');
    resolve();
  }).then(() => {
    console.log('买食物');
  }).then(() => {
    console.log('回家');
  })
}).then(() => {
  console.log('做菜');
})

先思考下这题输出什么?
来,上菜!

妈妈要做饭
要买菜
去菜市场
买食物
做菜
回家

what!!!难道妈妈不爱我了,在外面做了饭也不给我吃了是吗?
不可能呀!!!


第一次看到这个题目就错了,然后快速看了一遍答案,感觉自己会了,感觉自己又学到了新知识,又变强了!!

后来又看到了这题,果不其然,又错了。

这里就牵扯到了更多 JS 不为人知的小秘密了,恍然大悟,不了解这些秘密,那我永远不理解这道题。

详细分析可以看这篇文章 深度揭秘 Promise 微任务注册和执行过程

文章的第一题扩展一下

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
.then(() => {
  console.log("外部第一个then");
  return new Promise((resolve, reject) => {
    console.log("内部promise");
    resolve();
  })
  .then(() => {
    console.log("内部第一个then");
  })
  .then(() => {
    console.log("内部第二个then");
  })
  .then(() => {
    console.log("内部第三个then");
  })
  .then(() => {
    console.log("内部第四个then");
  })
})
.then(() => {
  console.log("外部第二个then");
});

输出结果: 偷懒... 按照书写顺序打印出来

原因:在第一个 thenreturn 了一个 new Promise ,最重要的是,内部的四个 then 是一起被返回的。外层代码下一个 then 必须等到 return 代码执行完成,Promise 状态变更后, 后面的 then 才能执行。return 未完成,第一个 then 就未完成,外部第二个 then 就要等着。

在此题就是要等到 内部第四个 then 执行完成。

证明:给 new Promise 和每个 then 添加返回值,看外部第二个 then 接受谁的返回值就可判断。 会发现,接收的是 内部第四个 then 的返回值,其他的都是 undefined。感兴趣的同学可以自己打开注释测试下。

new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
.then(() => {
  console.log("外部第一个then");
  return new Promise((resolve, reject) => {
    console.log("内部promise");
    resolve('ss');
  })
  .then(() => {
    console.log("内部第一个then");
    // return 1;
  })
  .then(() => {
    console.log("内部第二个then");
    // return 2;
  })
  .then(() => {
    console.log("内部第三个then");
    // return 3;
  })
  .then(() => {
    console.log("内部第四个then");
    return 4;
  })
})
.then((r) => {
  console.log(r);
});

2. Promise 链式调用的交替执行

  1. 一个 Promise
new Promise((resolve, reject) => {
  console.log("外部promise");
  resolve();
})
.then(() => {
  console.log("外部第一个then");
  new Promise((resolve, reject) => {
    console.log("内部promise");
    resolve();
  })
  .then(() => {
    console.log("内部第一个then");
  })
  .then(() => {
    console.log("内部第二个then");
  })
  .then(() => {
    console.log("内部第三个then");
  })
  .then(() => {
    console.log("内部第四个then");
  })
})
.then(() => {
  new Promise((resolve, reject) => {
    resolve();
  })
  console.log("外部第二个then");
});

输出

外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
内部第三个then
内部第四个then
  1. 多个 Promise
new Promise(resolve => {
  resolve()
}).then(() => {
  return new Promise(r => {
    console.log('1-1');
    r()
  }).then(() => {
    console.log('1-1 p1')
  })
}).then(() => {
  console.log('1-2')
})

new Promise(resolve => {
  resolve(2);
}).then(() => {
  console.log('2-1');
}).then(() => {
  console.log('2-2');
}).then(() => {
  console.log('2-3');
})

输出

1-1
2-1
1-1 p1
2-2
2-3
1-2

从上面两个案例可以看出 Promise.then 是交替执行的。

有个问题,因为前面的 return Promise,为什么 1-2 最后才输出?

3. return Promise.resolved 相当于占几个微任务

new Promise(resolve => {
  resolve()
}).then(() => {
  return new Promise(r => {
    console.log('promise');
    r(5)
  })
}).then(r => {
  console.log(r)
})

new Promise(resolve => {
  resolve(2);
}).then(() => {
  console.log('1');
}).then(() => {
  console.log('2');
}).then(() => {
  console.log('3');
}).then(() => {
  console.log('4');
})

// promise 1 2 3 5 4

由之前的学习可知,没有 return 的话,r 的输出在 2 之前。因为 return 延后了两个微任务才输出。

具体分析还是这个文章最后有介绍 深度揭秘 Promise 微任务注册和执行过程
很感谢这篇文章,就不抄写了。引流吧

2.4 Promise 其他题型

之后更新...

参考文章