【JavaScript内功系列】循序渐进理解 Promise 异步编程(一)

前言

现代 JavaScript 开发中,异步编程是一个很重要的话题,Promise 的出现为 JavaScript 异步编程提供了优雅的标准解决方案

本篇文章我们将循序渐进理解 Promise 核心知识

同步和异步的概念理解

我们都知道,JavaScript 是单线程的,如果把线程想象成一个快递员(镇上唯一的快递员),那么

同步模式:你必须按顺序挨家挨户的送货,每送到一户,都要等待收件人签收,在当前收件人完成签收前,你无法前往下一户

基于这样的情况,不难看出同步模式的特点在于:

  • 阻塞式执行:必须要等待收件人签收,才能送下一单
  • 执行顺序是可预测的:某一个快递是否签收?这样的问题可以预测或解答
  • 利用率低:如果某个快递消耗时间长(比如开箱检查),快递员必须等待

异步模式:快递员将包裹统统放在驿站,每个包裹都有一个取件码,收件人凭取件码取件,它们之前互不影响

同样的,异步模式的特点在于:

  • 非阻塞式执行:快递员把快递存放驿站,取件/签收由收件人自行完成
  • 并发:可以处理多个快递的投放状态

记住,同步与异步的本质区别在于:是否会阻塞当前执行流程

JavaScript 中的异步概念,更多指的是单线程 + 任务队列管理异步任务的执行顺序

注意,JavaScript 是单线程的,要避免将异步和多线程混淆,异步是同时处理多个任务,就好比一个人在边吃饭边看电视,而不是两个人吃饭看电视

Promise 之前的回调地狱(Callback Hell)

回调地狱是在异步操作中因多层嵌套的回调函数导致的代码结构问题,它的表现方式就像是俄罗斯套娃,一层一层往下套,特别是在处理多个顺序依赖的异步操作时,也就是下一层的操作,需要基于上一层的数据

js 复制代码
function double(value,successCallback){
    setTimeout(()=>{
        successCallback(value * 2)
    },1000)
}

// 回调地狱
double(10,(firstResult)=>{
  console.log(firstResult);// 20
  double(firstResult,(secondResult)=>{
    console.log(secondResult); // 40
    double(secondResult,(thirdResult)=>{
      console.log(thirdResult); // 80
    })
  })
})

上面这段代码,在一秒后将打印出数字 20,再一秒后打印 40,最后是 80

回调地狱的核心问题在于其代码的可读性、可维护性难以言喻,就如同打了个死结!

Promise 对象

ECMAScript 6 依据社区广泛流行的 Promise/A+ 规范并加以完善支持,在此基础上推出了 Promise 对象,为异步编程模式提供了标准方案

Promise 的基本用法

Promise 对象是一个构造函数,可以通过 new 操作符来创建它的实例,它接收一个执行器函数(executor)作为参数,比如:

js 复制代码
const p1 = new Promise(()=>{})
console.log(p1);

在控制台打印 p1 实例,你会发现存在一个 [[PromiseState]] 属性,这个属性的值范围可以是三种状态:

  • pending(待定) :初始状态,既没有完成,也没有被拒绝
  • fulfilled(已完成):意味着操作成功完成
  • rejected(已拒绝):意味着操作失败

这三种状态中,pending 是最初始的状态,也就是没有做任何影响状态的操作

但要注意的是,一个 Promise 对象的状态一旦发生改变,比如从 pending 转为 fulfilledpending 转为 rejected 后,就已经落定,是不可逆的

并且,Promise 状态不能通过 JS 检测到,也无法进行修改

通过执行器函数(executor)来切换状态

上面说到,Promise 实例化时接收一个执行器函数,而这个执行器函数又接收两个函数作为参数,一般是 resolvereject

在 TypeScript 中,表现为:

ts 复制代码
interface PromiseConstructor {
	//...
	new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
}

resolve 翻译为解决的意思,可以将 Promise 对象的状态从 pending(待定) 转变到 fulfilled(已完成),并将结果(value)放在参数中传递出去

js 复制代码
const p1 = new Promise((resolve,reject)=>{
    resolve('成功!')
})
console.log(p1);

相应的,reject 翻译为拒绝的意思,可以将 promise 状态从pending(待定) 转变到 rejected(已拒绝),并将理由(reason)放在参数中传递出去

js 复制代码
const p1 = new Promise((resolve,reject)=>{
  reject('失败')
})
console.log(p1);

Promise 的实例方法

Promise 的实例上可以访问三种方法,这些方法是在 Primise.prototype 上定义的

Promise.prototype.then

先聊一聊重要的 then() 方法,它为 Promise 对象的 fulfilled(完成) 状态、rejected(拒绝) 状态提供处理程序,接收两个参数:

  • onFulfilled:当状态为 fulfilled 时执行此回调函数,接收一个参数,参数值来于 resolve(value) 中传递的 value
  • onRejected:当状态为 rejected 时执行此回调函数,接收一个参数,参数值来于 reject(reason) 调用时传递的 reason(原因,理由)

onFulfilledonRejected 这两个参数在 then() 中是可选的,只写单个参数也可以

它的 TypeScript 类型表现为:

ts 复制代码
interface Promise<T> {
 	//...
	then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
}

理论了解后,来看看下面这个例子:

js 复制代码
const p1 = new Promise((resolve,reject)=>{
    if(/**条件 */){
        resolve('成功值') // 触发 then 的 onFulfilled
    }else{
        reject(new Error('失败')) // 触发 then 的 onRejected
    }
})

p1.then(
  (res)=>{
    console.log(res); // 成功值
},
  (err)=>{
    console.log(err); // Error 对象
}
)

then() 方法会返回一个新的 Promise 实例 ,这意味着 then() 可以形成链式调用,也就是在 then() 之后再接 then()

并且,上一个 then() 可以通过 return 将值传递给下一个 then() ,但请注意,这里有一些规则:

  • 如果没有返回,则下一个 then() 的参数是 undefined
  • 如果返回的是一个普通值,那么将立即触发后续的 then(),并将值传递
  • 如果返回的是一个 Promise 实例,那么等到此 Promise 状态为 fulfilled(完成)rejected(拒绝)时将值/理由传递

看下面的例子:

js 复制代码
const p1 = new Promise((resolve,reject)=>{
  resolve('成功值') // 触发 then 的 onFulfilled
})
.then(res=>{
  console.log('首次then接收:', res) // 首次then接收: 成功值
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve('链式') // 异步改变状态为fulfilled(1秒后)
    },1000)
  })
})
.then(res=>{
   // 前序Promise解决后才执行(等待1秒)
   console.log('异步then接收:', res) // 异步then接收: 链式
  return '第二个then的值' // 同步
})
.then(res=>{
  console.log(res); // 第二个then的值
})

Promise.prototype.catch

catch() 方法用于执行 Promise 状态为 rejected(拒绝) 的回调函数

它的 TypeScript 类型表现为:

ts 复制代码
interface Promise<T> {
    //...
	catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

在上文介绍的 then() 方法中,我们说它接收 onFulfilledonRejected 两个参数

但在一般的实践中,我们通常使用 catch() 来充当 onRejected 处理程序的角色,因为 catch() 方法实际上是 then() 第二个参数 onRejected 程序的语法糖

并且,catch()then() 一样,返回一个 Promise 对象

js 复制代码
const p1 = new Promise((resolve,reject)=>{
  reject(new Error('失败值'))
})

p1
.then(res=>{
  // 当状态为fulfilled时
  console.log(res);
})
.catch(err=>{
  // 当状态为rejected时
  console.log(err);
})

Promise.prototype.finally

finally() 方法的特点是不管 Promise 状态如何变化,都会执行的操作

它接收一个参数:

  • onFinally:在状态为 fulfilled(成功)rejected(拒绝) 时都会触发的回调函数,但要注意,这个回调函数不接收任何参数值

TypeScript类型表现为:

ts 复制代码
interface Promise<T> {
    //...
    finally(onfinally?: (() => void) | undefined | null): Promise<T>;
}

和前面的两个方法一样,finally() 方法也返回一个 Promise

Promise 解决回调地狱

还记得在回调地狱章节的示例代码吗?现在,有了 Promise 后,我们有了更棒的解决方案:

js 复制代码
const asyncDouble = (value,successCallback)=>{
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve(value * 2)
    },1000)
  })
}

asyncDouble(10)
  .then(res => { console.log(res); return asyncDouble(res) }) // 20
  .then(res => { console.log(res); return asyncDouble(res) }) // 40
  .then(res => { console.log(res); return asyncDouble(res) }) // 80

总结

总结一下本文的内容,在开头我们通过快递员的例子讲解了同步和异步的概念:

  • 同步:挨家挨户的送货,在未签收前无法前往下一家
  • 异步:快递存放驿站,用户可以互不影响,多任务的签收

然后,我们举例说明了在没有 Promise 之前,异步函数多层嵌套带来的可阅读性、可维护性差问题(回调地狱)

正式介绍 Promise 对象时,我们介绍了其基本语法和三个状态,并可以通过执行器函数切换状态,还介绍了 Promise.prototype 原型对象带给实例的三个方法 then()catch()finally()

参考资料

JavaScript 内功系列

解析 JavaScript 核心技术,提升编程内功

本文已收录至《JavaScript 内功系列》,全文地址:我的 GitHub 博客 | 掘金专栏

对你有帮助的话,欢迎 Star

交流讨论

对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正

相关推荐
剪刀石头布啊5 分钟前
var、let、const与闭包、垃圾回收
前端·javascript
剪刀石头布啊7 分钟前
js常见的单例
前端·javascript
剪刀石头布啊7 分钟前
数据口径
前端·后端·程序员
剪刀石头布啊11 分钟前
http状态码大全
前端·后端·程序员
剪刀石头布啊13 分钟前
iframe通信、跨标签通信的常见方案
前端·javascript·html
宇之广曜22 分钟前
搭建 Mock 服务,实现前端自调
前端·mock
yuko093124 分钟前
【手机验证码】+86垂直居中的有趣问题
前端
用户15129054522028 分钟前
Springboot中前端向后端传递数据的几种方式
前端
阿星做前端28 分钟前
如何构建一个自己的 Node.js 模块解析器:node:module 钩子详解
前端·javascript·node.js
用户15129054522032 分钟前
Web Worker:让前端飞起来的隐形引擎
前端