Promise 在 ES6 中的具体应用

上一节对 ES6 的 Promise 有了一个最简单的介绍,这一节详细说一下 Promise 那些最常见的功能

本节展示的代码参考这里

本节课程概述

  • 准备工作
  • 参数传递
  • 异常捕获
  • 串联多个异步操作
  • Promise.allPromise.race的应用
  • Promise.resolve的应用
  • 其他

准备工作

因为以下所有的代码都会用到Promise,因此干脆在所有介绍之前,先封装一个Promise封装一次,为下面多次应用

const fs = require('fs')
const path = require('path')  // 后面获取文件路径时候会用到
const readFilePromise = function (fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, (err, data) => {
            if (err) {
                reject(err)  // 注意,这里执行 reject 是传递了参数,后面会有地方接收到这个参数
            } else {
                resolve(data.toString())  // 注意,这里执行 resolve 时传递了参数,后面会有地方接收到这个参数
            }
        })
    })
}

以上代码一个一段 nodejs 代码,将读取文件的函数fs.readFile封装为一个Promise。经过上一节的学习,我想大家肯定都能看明白代码的含义,要是看不明白,你就需要回炉重造了!

参数传递

我们要使用上面封装的readFilePromise读取一个 json 文件../data/data2.json,这个文件内容非常简单:{"a":100, "b":200}

先将文件内容打印出来,代码如下。大家需要注意,readFilePromise函数中,执行resolve(data.toString())传递的参数内容,会被下面代码中的data参数所接收到。

const fullFileName = path.resolve(__dirname, '../data/data2.json')
const result = readFilePromise(fullFileName)
result.then(data => {
    console.log(data)
})

再加一个需求,在打印出文件内容之后,我还想看看a属性的值,代码如下。之前我们已经知道then可以执行链式操作,如果then有多步骤的操作,那么前面步骤return的值会被当做参数传递给后面步骤的函数,如下面代码中的a就接收到了return JSON.parse(data).a的值

const fullFileName = path.resolve(__dirname, '../data/data2.json')
const result = readFilePromise(fullFileName)
result.then(data => {
    // 第一步操作
    console.log(data)
    return JSON.parse(data).a  // 这里将 a 属性的值 return
}).then(a => {
    // 第二步操作
    console.log(a)  // 这里可以获取上一步 return 过来的值
})

总结一下,这一段内容提到的“参数传递”其实有两个方面:

  • 执行resolve传递的值,会被第一个then处理时接收到
  • 如果then有链式操作,前面步骤返回的值,会被后面的步骤获取到

异常捕获

我们知道then会接收两个参数(函数),第一个参数会在执行resolve之后触发(还能传递参数),第二个参数会在执行reject之后触发(其实也可以传递参数,和resolve传递参数一样),但是上面的例子中,我们没有用到then的第二个参数。这是为何呢 ———— 因为不建议这么用

对于Promise中的异常处理,我们建议用catch方法,而不是then的第二个参数。请看下面的代码,以及注释。

const fullFileName = path.resolve(__dirname, '../data/data2.json')
const result = readFilePromise(fullFileName)
result.then(data => {
    console.log(data)
    return JSON.parse(data).a
}).then(a => {
    console.log(a)
}).catch(err => {
    console.log(err.stack)  // 这里的 catch 就能捕获 readFilePromise 中触发的 reject ,而且能接收 reject 传递的参数
})

在若干个then串联之后,我们一般会在最后跟一个.catch来捕获异常,而且执行reject时传递的参数也会在catch中获取到。这样做的好处是:

  • 让程序看起来更加简洁,是一个串联的关系,没有分支(如果用then的两个参数,就会出现分支,影响阅读)
  • 看起来更像是try - catch的样子,更易理解

码农进阶题库 - 每天一道面试题 or Js小知识@Js中文网

每天弄懂一道面试题(Javascript小知识)来鞭策自己学习思考,每天进步一点,帮助你提高你的代码编写质量

串联多个异步操作

如果现在有一个需求:先读取data2.json的内容,当成功之后,再去读取data1.json。这样的需求,如果用传统的callback去实现,会变得很麻烦。而且,现在只是两个文件,如果是十几个文件这样做,写出来的代码就没法看了(臭名昭著的callback-hell)。但是用刚刚学到的Promise就可以轻松胜任这项工作

const fullFileName2 = path.resolve(__dirname, '../data/data2.json')
const result2 = readFilePromise(fullFileName2)
const fullFileName1 = path.resolve(__dirname, '../data/data1.json')
const result1 = readFilePromise(fullFileName1)

result2.then(data => {
    console.log('data2.json', data)
    return result1  // 此处只需返回读取 data1.json 的 Promise 即可
}).then(data => {
    console.log('data1.json', data) // data 即可接收到 data1.json 的内容
})

上文“参数传递”提到过,如果then有链式操作,前面步骤返回的值,会被后面的步骤获取到。但是,如果前面步骤返回值是一个Promise的话,情况就不一样了 ———— 如果前面返回的是Promise对象,后面的then将会被当做这个返回的Promise的第一个then来对待 ———— 如果你这句话看不懂,你需要将“参数传递”的示例代码和这里的示例代码联合起来对比着看,然后体会这句话的意思。

Promise.allPromise.race的应用

我还得继续提出更加奇葩的需求,以演示Promise的各个常用功能。如下需求:

读取两个文件data1.jsondata2.json,现在我需要一起读取这两个文件,等待它们全部都被读取完,再做下一步的操作。此时需要用到Promise.all

// Promise.all 接收一个包含多个 promise 对象的数组
Promise.all([result1, result2]).then(datas => {
    // 接收到的 datas 是一个数组,依次包含了多个 promise 返回的内容
    console.log(datas[0])
    console.log(datas[1])
})

读取两个文件data1.jsondata2.json,现在我需要一起读取这两个文件,但是只要有一个已经读取了,就可以进行下一步的操作。此时需要用到Promise.race

// Promise.race 接收一个包含多个 promise 对象的数组
Promise.race([result1, result2]).then(data => {
    // data 即最先执行完成的 promise 的返回值
    console.log(data)
})

Promise.resolve的应用

从 jquery 引出,到此即将介绍完 ES6 的Promise,现在我们再回归到 jquery 。

大家都是到 jquery v1.5 之后$.ajax()返回的是一个deferred对象,而这个deferred对象和我们现在正在学习的Promise对象已经很接近了,但是还不一样。那么 ———— deferred对象能否转换成 ES6 的Promise对象来使用??

答案是能!需要使用Promise.resolve来实现这一功能,请看以下代码:

// 在浏览器环境下运行,而非 node 环境
cosnt jsPromise = Promise.resolve($.ajax('/whatever.json'))
jsPromise.then(data => {
    // ...
})

注意:这里的Promise.resolve和文章最初readFilePromise函数内部的resolve函数可千万不要混了,完全是两码事儿。JS 基础好的同学一看就明白,而这里看不明白的同学,要特别注意。

实际上,并不是Promise.resolve对 jquery 的deferred对象做了特殊处理,而是Promise.resolve能够将thenable对象转换为Promise对象。什么是thenable对象?———— 看个例子

// 定义一个 thenable 对象
const thenable = {
    // 所谓 thenable 对象,就是具有 then 属性,而且属性值是如下格式函数的对象
    then: (resolve, reject) => {
        resolve(200)
    }
}

// thenable 对象可以转换为 Promise 对象
const promise = Promise.resolve(thenable)
promise.then(data => {
    // ...
})

上面的代码就将一个thenalbe对象转换为一个Promise对象,只不过这里没有异步操作,所有的都会同步执行,但是不会报错的。

其实,在我们的日常开发中,这种将thenable转换为Promise的需求并不多。真正需要的是,将一些异步操作函数(如fs.readFile)转换为Promise(就像文章一开始readFilePromise做的那样)。这块,我们后面会在介绍Q.js库时,告诉大家一个简单的方法。

其他

以上都是一些日常开发中非常常用的功能,其他详细的介绍,请参考阮一峰老师的 ES6 教程 Promise 篇

最后,本节我们只是介绍了Promise的一些应用,通俗易懂拿来就用的东西,但是没有提升到理论和标准的高度。有人可能会不屑 ———— 我会用就行了,要那么空谈的理论干嘛?———— 你只会使用却上升不到理论高度,永远都是个搬砖的,搬一块砖挣一毛钱,不搬就不挣钱! 在我看来,所有的知识应该都需要上升到理论高度,将实际应用和标准对接,知道真正的出处,才能走的长远。

下一节我们介绍 Promise/A+ 规范

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「IT平头哥联盟」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

results matching ""

    No results matching ""