js Promise

  1. js Promise
  2. 简介
  3. 基本使用
  4. 链式调用
  5. Promise封装
  6. 错误处理
  7. finally
  8. Promise.all
  9. Promise.race
  10. Promise.allSettled
  11. Promise.any
  12. Promise.resolve & Promise.reject

js Promise

简介

Promisejs里一种异步编程解决方案,比较传统的是回调函数和事件,熟悉nodejs的都知道,在Promise还没出现之前,一切异步编程都是通过回调函数来实现的,包括文件读取,数据库操作等,很容易造成回调地狱,而Promise可以帮我们很好的解决这个问题。

Promise可以看做是一个容器,里面存放着未来某个时间才会结束的异步操作,其内部有三种状态,分别是pending(进行中),fulfilled(已成功)和rejected(已失败)。只有内部异步操作的结果,会影响这三种状态,其他任何操作都无法影响这三种状态。

Promise对象的状态改变,只可能有两种可能:pending -> fulfilled或者pending -> rejected。只要状态改变了,就不会再改变了。

基本使用

Promise是一个构造函数,用来生成Promise实例,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己指定。如果Promise内部的异步操作成功,则调用resolve函数,把Promise内部的状态由pending变为fulfilled,并把结果作为参数抛出去,否则调用reject函数把Promise内部的状态由pending变为rejected,把错误作为参数抛出去。

const fs = require('fs');
// 1、创建promise对象,一经创建,立马执行
new Promise((resolve, reject) => {
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        if (err) {
            reject(err);
        } else {
            // 请求成功时,调用.then()里面自己写的resolve函数
            resolve(data);
        }
    });
})

抛出去的结果我们可以使用Promisethen方法进行接收,Promisethen方法接收两个回调函数作为参数,第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

const fs = require('fs');
// 1、创建promise对象,一经创建,立马执行
new Promise((resolve, reject) => {
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        if (err) {
            reject(err);
        } else {
            resolve(data);
        }
    });
}).then(
    // 下面这个函数式作为上面Promise的resolve使用
    (data) => { // 这里的参数就是上面Promise里resolve传出来的参数
        console.log(data.toString());
    },
    // 这个作为reject使用
    (err) => {
        console.log(err)
    }
)

使用Promise的注意点

  • 调用resolve或reject并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。这是因为立即resolvedPromise是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。

一般来说一旦Promise内部调用resolve或者reject之后,它的任务就完成了,不应该继续在后面执行其他任务,因此我们最好再resolvereject前面加上return

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

链式调用

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

const fs = require('fs');


// then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
// 第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
new Promise((resolve, reject) => {
    fs.readFile('./data/a.txt', (err, data) => {
        if (!err) {
            resolve(data)
        } else {
            reject(err)
        }
    })
})
    .then((data) => {
        console.log(data.toString());
        return '在.then()中返回的结果,会传入下一个then的参数中'
    })
    .then((data) => {
        console.log(data);
        return 1
    })
    .then((data) => {
        console.log(data);
    })
    .catch((err) => {
        console.log('err', err);
    })

输出

aaaaaaaaaaaaaaaaaaaaaa // 文件a.txt的内容
在.then()中返回的结果,会传入下一个then的参数中
1

Promise封装

假如我们要依次地区文件的内容,我们可能会像下面这样编码

const fs = require('fs');

new Promise((resolve, reject) => {
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        if (err) {
            reject(err);
        } else {
            resolve(data);
        }
    });
}).then((data) => {
    console.log(data.toString());
    return new Promise((resolve, reject) => {
        fs.readFile(__dirname + '/data/b.txt', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    })
}).then((data) => {
    console.log(data.toString());
    return new Promise((resolve, reject) => {
        fs.readFile(__dirname + '/data/c.txt', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    })
}).then((data) => {
    console.log(data.toString());
});

这样写没问题,但是充斥了大量的重复代码,因此我们可以对读取文件的Promise进行一层封装,改造成以下代码

const fs = require('fs');

function readFiles(...args) {
    return new Promise((resolve, reject) => {
        fs.readFile(...args, (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        })
    })
}

readFiles(__dirname + '/data/a.txt', 'utf8')
    .then((data) => {
        console.log(data);
        return readFiles(__dirname + '/data/b.txt', 'utf8');
    })
    .then((data) => {
        console.log(data);
        return readFiles(__dirname + '/data/c.txt', 'utf8');
    })
    .then((data) => {
        console.log(data);
        return readFiles(__dirname + '/data/a.txt', 'utf8');
    })
    .then((data)=>{
        console.log(data);
    });

我们封装了一个readFiles函数,返回一个读取文件的Promise对象,在链式调用中,then函数返回的也是一个Promise对象,因此可以一层一层往下调用

错误处理

Promise的错误处理有两种方式,一种是在then函数的第二个回调函数参数中进行处理,另一种是使用Promise.catch进行错误捕获。

例如:

let p = new Promise(...)
p.then(null, reject => console.log(reject))

或者

let p = new Promise(...)
p.then(resolve => console.log(resolve))
 .catch(err => console.log(err))

Promise.catch具有”冒泡”性质,意思是无论then链式调用了多少层,Promise.catch都能捕获到其中任意一层发生的错误

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

因此我们推荐总是使用Promise.catch进行异常捕获

finally

不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的Promise状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于Promise的执行结果。

const fs = require('fs');

const P1 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        resolve(data)
    }) 
});

const P2 = new Promise((resolve) => { 
    throw new Error('读取文件b.txt出错了')
});

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.all([P1, P2, P3])
    .then(res => {
        console.log(res);
        res.map(file => console.log(file.toString()))
    })
    .catch(err => console.log(err))
    .finally(() => console.log('文件读取完毕'))

Promise.all

Promise.all可以把多个Promise实例,组装成一个Promise实例,此时只有当所有的Promise的状态都变为fulfilled时,Promise.all实例的状态才会变为fulfilled,只要其中有一个实例状态变为了rejected,最终Promise.all实例的状态就会变成rejected

Promise.all我们常用来并发的执行异步操作,比如我们想要同时读取三个文件,使用Promise链式调用会逐一读取,会造成一定的性能损失。

Promise.all接收一个Promise的数组(不一定是数组,只要有iterator接口就行)作为参数,返回包装后的Promise实例,实例的then方法接收的结果是所有Promise实例结果的数组。无论异步操作哪个先执行完毕,结果数组的顺序和Promise.all传入的Promise实例数组的顺序相同

再次强调一下,Promise.all入参不一定是数组,只要有iterator接口就行

const fs = require('fs');

const P1 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        resolve(data)
    }) 
});

const P2 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/b.txt', (err, data) => {
        resolve(data)
    }) 
});

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.all([P1, P2, P3]).then(res => {
    console.log(res);
    res.map(file => console.log(file.toString()))
})

输出

[
  <Buffer 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61>,
  <Buffer 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62>,
  <Buffer 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63>
]
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccc

注意,如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

const fs = require('fs');

const P1 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        resolve(data)
    }) 
});

const P2 = new Promise((resolve) => { 
    throw new Error('读取文件b.txt出错了')
}).catch(err => err);

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.all([P1, P2, P3])
    .then(res => {
        console.log(res);
        res.map(file => console.log(file.toString()))
    })
    .catch(err => console.log(err))

结果

aaaaaaaaaaaaaaaaaaaaaa
Error: 读取文件b.txt出错了
cccccccccccccccccccccc

如果P2没有自己的catch方法,错误则会被Promise.all捕获

const fs = require('fs');

const P1 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        resolve(data)
    }) 
});

const P2 = new Promise((resolve) => { 
    throw new Error('读取文件b.txt出错了')
});

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.all([P1, P2, P3])
    .then(res => {
        console.log(res);
        res.map(file => console.log(file.toString()))
    })
    .catch(err => console.log(err))

结果报错

Promise.race

Promise.race,顾名思义,race是竞赛的意思,Promise.race里包装的Promise实例,只要有一个实例的状态率先改变了,那么Promise.race的状态就随之改变,那个率先改变状态的返回值,将作为Promise.race``then函数的入参,无论那个实力是成功或者失败,Promise.race的状态都会随之改变

const fs = require('fs');

const P1 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/a.txt', (err, data) => {
        resolve(data)
    }) 
});

const P2 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/b.txt', (err, data) => {
        resolve(data)
    }) 
});

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.race([P1, P2, P3]).then(res => {
    console.log(res.toString())
}).catch(err => console.log(err));

Promise.race额错误处理与Promise.all有着相同的特性,如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,就不会触发Promise.race()catch方法。

Promise.allSettled

只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束,和前面的方法不同的是,Promise.allSettled的返回值,不是结果数组,而是一个{status: 'fulfilled' | 'rejected', [reason|value]: VALUE}对象的数组,如果statusfulfilled,表示该Promise实例成功,那么第二个属性是value,值是异步操作的结果,如果statusrejected,表示该Promise实例有异常,那么第二个属性是reason,值是异常信息。

const fs = require('fs');

const P1 = new Promise((resolve) => { 
    throw new Error('错了')
});

const P2 = new Promise((resolve) => { 
    throw new Error('错了')
});

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.allSettled([P1, P2, P3]).then(res => {
    console.log(res)
}).catch(err => console.log(err));

结果

[
  {
    status: 'rejected',
    reason: Error: 错了
        at C:\Users\lyucan\Desktop\pro\newRepo\Repositories\myrepo\nodejs\Promise
        ...
  },
  {
    status: 'rejected',
    reason: Error: 错了
        at C:\Users\lyucan\Desktop\pro\newRepo\Repositories\myrepo\nodejs\Promise
        ...
  },
  {
    status: 'fulfilled',
    value: <Buffer 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63>
  }
]

往往我们需要自己手动筛选出成功的结果或失败的结果,根据需要自行处理

Promise.any

只要参数实例有任何一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

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

const fs = require('fs');
const Promise = require("bluebird");

const P1 = new Promise((resolve) => { 
    throw new Error('错了')
});

const P2 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/b.txt', (err, data) => {
        resolve(data)
    }) 
});

const P3 = new Promise((resolve) => { 
    fs.readFile(__dirname + '/data/c.txt', (err, data) => {
        resolve(data)
    }) 
});

Promise.any([P1, P2, P3]).then(res => {
    console.log(res.toString())
}).catch(err => console.log(err));

结果

bbbbbbbbbbbbbbbbbbbbbb

如果c.txt文件优先读取完毕,那么结果就是c.txt文件的内容

需要注意的是,Promise.any还只是草案里的特性,不能直接使用,我们可以引用社区的Promisebluebird来使用

Promise.resolve & Promise.reject

Promise.resolve可以直接把一个对象转化为Promise对象。

传入不同的对象,Promise.resolve有不同的行为

  1. 传入Promise实例
    如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  2. 参数是一个thenable对象
    thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
    then: (resolve, reject) => {
        resolve(42);
    }
};

Promise.resolve()方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then()方法。

let thenable = {
    then: (resolve, reject) => {
        resolve(42);
    }
};
  
let p1 = Promise.resolve(thenable); // 此时p1的状态已经变成fulfilled了
p1.then(val => console.log(val));   // 42
let thenable = {
    then: (resolve, reject) => {
        reject(42);
    }
};
  
let p1 = Promise.resolve(thenable); // 此时p1的状态已经变成rejected了
p1.catch(err => console.log(err));
let thenable = {
    then: (resolve, reject) => {
        throw new Error('出错了')
    }
};
  
let p1 = Promise.resolve(thenable); // 此时p1的状态已经变成rejected了
p1.catch(err => console.log(err));  // Error: 出错了
  1. 参数不是具有then()方法的对象,或根本就不是对象
let p1 = Promise.resolve('hello world');
p1.then(res => console.log(res)); // hello world

以上代码等价于

let p1 = new Promise(resolve => resolve('hello world'));
p1.then(res => console.log(res));
  1. 不带有任何参数
let p = Promise.resolve();
p.then(res => console.log(res)); // undefined

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected

let p = Promise.reject('出错了');
p.catch(err => console.log(err)); // 出错了

以上代码等同于

let p = new Promise((resolve, reject) => reject('出错了'));
p.catch(err => console.log(err)); // 出错了

最后提一下,在社区有很多库实现了Promise,都遵循Primise/A+规范,有人测试社区的多种实现中,bluebird.js的性能比官方的ES6要高三倍,而且很多还在草案中的特性,bluebird.js都已经实现了,如果想要使用或者学习新的Promise特性,不妨试试bluebird.js


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com