【JS】JavaScript异步操作系列(3)——Promise【1】

说明:
本博客来源于以下博客和《你不知道的JavaScript》中卷,原博客链接为:http://www.cnblogs.com/wangfupeng1988/p/6515855.html

ES6的Promise基本使用示例

1、传统的异步操作

var wait = function () {
    var task = function () {
        console.log('执行完成')
    }
    setTimeout(task, 2000)
}
wait()

2、用Promise进行封装

const wait =  function () {
    // 定义一个 promise 对象
    const promise = new Promise((resolve, reject) => {
        // 将之前的异步操作,包括到这个 new Promise 函数之内
        const task = function () {
            console.log('执行完成')
            resolve()  // callback 中去执行 resolve 或者 reject
        }
        setTimeout(task, 2000)
    })
    // 返回 promise 对象
    return promise
}
  • 将之前的异步操作那几行程序,用new Promise((resolve,reject) => {.....})包装起来,最后return即可
  • 异步操作的内部,在callback中执行resolve()(表明成功了,失败的话执行reject()

继续往下看。

const w = wait()
w.then(() => {
    console.log('ok 1')
}, () => {
    console.log('err 1')
}).then(() => {
    console.log('ok 2')
}, () => {
    console.log('err 2')
})

wait()返回的是一个promise对象,而promise对象有then属性。

注:then接收两个参数(函数),第一个在成功时(触发resolve)执行,第二个在失败时(触发reject)时执行。而且,then还可以进行链式操作。

上面就是ES6的Promise基本使用示例。

Promise在ES6中的具体应用

1、准备工作

因为以下所有的代码都会用到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。

2、参数传递

我们要使用上面封装的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属性的值,代码如下:

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 过来的值
})

之前我们已经知道then可以执行链式操作,如果then有多步骤的操作,那么前面步骤return的值会被当做参数传递给后面步骤的函数,上面代码中的a就接收到了return JSON.parse(data).a的值。

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

3、异常捕获

我们知道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的样子,更易理解

4、串联多个异步操作(链式流)

如果现在有一个需求:先读取data2.json的内容,当成功之后,再去读取data1.json。这样的需求,如果用传统的callback去实现,会变得很麻烦。而且,现在只是两个文件,如果是十几个文件这样做,写出来的代码就没法看了。但是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来对待

5、Promise.all和Promise.race的应用

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

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

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

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

6、Promise.resolve的应用

6.1 new Promise的快捷方式

静态方法Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。
比如:Promise.resolve(42); 可以认为是以下代码的语法糖。

new Promise(function(resolve){
    resolve(42);
});

在这段代码中的 resolve(42); 会让这个promise对象立即进入确定(即resolved)状态,并将 42 传递给后面then里所指定的函数。

方法 Promise.resolve(value); 的返回值也是一个promise对象,所以我们可以像下面那样接着对其返回值进行 .then 调用。

Promise.resolve(42).then(function(value){
    console.log(value);
});

6.2 Thenable

Promise.resolve 方法另一个作用就是将 thenable 对象转换为promise对象。
那么,什么是thenable对象呢?看下面的例子:

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

到底什么样的对象能算是thenable的呢,最简单的例子就是 jQuery.ajax(),它的返回值就是thenable的。

thenable对象转换成Promise对象,就用到了Promise.resolve来转换。

var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象
promise.then(function(value){
   console.log(value);
});

jQuery.ajax()的返回值是一个具有 .then 方法的 jqXHR Object对象,这个对象继承了来自 Deferred Object 的方法和属性。
但是Deferred Object并没有遵循Promises/A+或ES6 Promises标准,所以即使看上去这个对象转换成了一个promise对象,但是会出现缺失部分信息的问题。
这个问题的根源在于jQuery的 Deferred Objectthen 方法机制与promise不同。

注意:如果向Promise.resolve(...)传递一个真正的promise,就只会返回同一个promise。看如下例子:

var p1 = Promise.resolve(42);
var p2 = Promise.resolve(p1);
p1 === p2;  //true

术语:决议、完成及拒绝

我们先来研究下构造器Promise(...)

var p = new Promise(function(x,y){
    //x()用于完成
    //y()用于拒绝
});

这里提供了两个回调(称为X和Y)。第一个通常用于标识Promise已经完成,第二个总是用于标识Promise被拒绝。这个“通常”是什么意思呢?对于这些参数的精确命名又意味着什么呢?

对于第二个参数名很容易决定。因为这就是它真实的(也是唯一的)工作。但是,第一个参数就有一些模糊了。第一个参数标识Promise完成,为什么不用fulfill呢?
我们先来看两个Promise API方法:

var fulfilledPr = Promise.resolve(42);
var rejectedPr = Promise.reject("Oops");

Promise.resolve(...)创建了一个决议为输入值的Promise。这个例子中,42是一个非Promise、非thenable的普通值,所以完成后的promise fulfilledPr 是为值42创建的。Promise.reject("Oops")创建了一个被拒绝的Promise rejectedPr,拒绝理由为“Oops”

现在来解释单词resolve用于表达结果可能是完成也可能是拒绝,既没有歧义,也确实更精确。

//thenable对象
var rejectedTh = {
    then: function(resolved,rejected){
        rejected("Oops");
    }
};

var rejectedPr = Promise.resolve(rejectedTh); 

Promise.resolve(...)会将传入的真正Promise直接返回,对传入的thenable则会展开。如果这个thenable展开得到一个拒绝状态,那么从Promise.resolve(...)返回的Promise实际上就是这同一个拒绝状态。

Promise(...)构造器的第一个参数回调会展开thenable或真正的Promise

var rejectedPr = new Promise(function(resolve,reject){
    //用一个被拒绝的promise完成这个promise
    resolve(Promise.reject("Oops"));
});

rejectedPr.then(
    function fulfilled(){
        //永远不会到这里
    },
    function rejected(err){
        console.log(err);//Oops
    }
);

现在应该很清楚了,Promise(...)构造器的第一个回调参数的恰当称谓是resolve(...)

reject(...)不会像resolve(...)一样进行展开。如果向reject(...)传入一个Promise/thenable值,它会把这个值原封不动的设置为拒绝理由。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值