ECMAScript6 之 Promise™

1. 什么是 Promise

1.1. 定义

  • Promise 对象用于异步计算
  • 一个 Promise 表示一个现在、将来或永不可能用到的值

1.2. 优缺点

1.2.1. 优点

  • 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
  • Promise 对象提供统一的接口,使得控制异步操作更加容易

1.2.2. 缺点

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
  • 当处于 Pending 状态时,无法得知目前进展到哪一个阶段

1.3. 使用方法

Promise 构造函数包含一个参数和一个带有 resolve(解析)和 reject(拒绝)两个参数的回调。在回调中执行一些操作(例如异步),如果一切都正常,则调用 resolve,否则调用 reject。

  • Promise 状态发生变化,就会触发 .then() 里的响应函数处理后续步骤
  • Promise 状态最多只能改变一次,一经改变,不能再变
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    new Promise(
    // 执行器 executor
    function (resolve, reject) {
    // 一段耗时很长的异步操作

    resolve(); // 数据处理完成

    reject(); // 数据处理出错
    }
    ).then(function A() {
    // 处理成功 下一步操作
    }, function B() {
    // 处理失败 下一步操作
    })

promise

1.4. Promise 的 3 种状态

状态 含义 描述
pending 待定 初始状态
fulfilled 实现 操作成功
rejected 被否决 操作失败

1.5. 最简单的实例

1
2
3
4
5
6
7
8
9
// 先输出 here we go,等待 2s 后输出 hello world
console.log('here we go');
new Promise(resolve => {
setTimeout(() => {
resolve('hello');
});
}).then(value => {
console.log(value + ' world');
});

1.6. 使用 Promise 实现 Ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ajax(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}

1.7. 浏览器兼容性

Promise 浏览器兼容性

2. 异步的问题

2.1. 异步操作的常见语法

  1. 事件的侦听与响应

    1
    2
    3
    4
    5
    6
    7
    function start() {
    // 响应事件,进行对应的操作
    }
    // JavaScript
    document.getElementById('start').addEventListener('click', start, false);
    // jQuery
    $("#start").on('click', start);
  2. 回调

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // ajax
    $.ajax({
    url: 'https://baidu.com',
    success: function (res) {
    // 回调函数
    }
    });
    // 页面加载完毕之后回调
    $(function () {
    // 回调函数
    });

2.2. 异步回调的问题

  • 嵌套层次很深,难以维护
    回调地狱
    1
    2
    3
    4
    5
    6
    7
    8
    9
    a(function (arg1) {
    b(arg1,function (arg2) {
    c(arg2,function (arg3) {
    d(arg3,function () {
    // 回调地狱
    })
    })
    })
    });
  1. 无法正常的使用 return 和 throw
    不能捕获异步函数的异常,因为异步函数是在不同的栈里面运行,没办法正常的使用 try catch 处理异步函数中的错误。
  2. 无法正常检索堆栈信息
    每次回调都是在系统层面的一个新的堆栈
  3. 多个回调之间难以建立联系
    一个回调一旦启动,我们再也没有办法对它操作

3. 使用 Promise

3.1. 两步执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 隔 2s 后输出 hello world,再隔 2s 后输出 hello promise
console.log('here we go');
new Promise(resolve => {
setTimeout(() => {
resolve('hello');
},2000);
}).then(value => {
console.log(value + ' world');
return new Promise(resolve => {
setTimeout(()=>{
resolve('hello promise');
},2000);
});
}).then(value => {
console.log(value);
})

3.2. .then()

  • .then() 接收两个函数作为参数,分别代表 fulfilledrejected
  • .then() 返回一个新的 Promise 实例,所以它可以链式调用
  • 当前面的 Promise 状态改变时,.then() 根据其最终状态,选择特定的状态响应函数执行
  • 状态响应函数可以返回新的 Promise,或者其它值;如果返回其它值,则会立刻执行下一级 .then()
  • 如果返回新的 Promise,那么下一级 .then() 会在新 Promise 状态改变之后执行

3.3. 错误处理

Promise 会自动捕获内部异常,并交给 rejected 响应函数处理。强烈建议在所有队列最后都加上 .catch(),以避免漏掉错误处理造成意想不到的问题。
处理错误的两种方法

  • rejected(‘错误信息’) .then(null, message => {});
  • throw new Error(‘错误信息’) .catch(message => {});

推荐使用 catch 捕获异常,更加清晰好读,并且可以捕获前端的错误。

1
2
3
4
5
6
7
8
9
10
console.log('here we go');
var promise = new Promise((resolve,reject) => {
reject('错误'); // throw new Error('错误');
});
promise.then(value => {
console.log(value);
return value;
}).catch(value => {
console.log("catch:" + value);
});

4. Promise 常用函数

4.1. Promise.all()

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

1
Promise.all([p1, p2, p3]);

上面代码中,Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 对象的实例。(Promise.all 方法的参数不一定是数组,但是必须具有 iterator 接口,且返回的每个成员都是 Promise 实例。)
p 的状态由 p1、p2、p3 决定,分成两种情况:

  1. 只有 p1、p2、p3 的状态都变成 fulfilled,p的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数
  2. 只要 p1、p2、p3 之中有一个被 rejected,p的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
console.log('here we go');
Promise.all([1, 2, 3])
.then(all => {
console.log("1: " + all);
let promise1 = new Promise(resolve => {
resolve('hello');
});
let promise2 = new Promise(resolve => {
resolve('world');
});
return Promise.all([promise1, promise2]);
})
.then(all => {
console.log("2: " + all);
let promise3 = new Promise((resolve, reject) => {
reject('错误-promise3');
});
let promise4 = new Promise((resolve, reject) => {
reject('错误-promise4');
});
return Promise.all([promise3, promise4]);
}).then(all => {
console.log(all);
})
.catch(err => {
console.log('catch: ' + err);
});

4.2. Promise.race()

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

1
var p = Promise.race([p1, p2, p3]);

上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的返回值。
如果 Promise.all 方法和 Promise.race 方法的参数,不是 Promise 实例,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。

4.3. Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve() 方法就起到这个作用。

1
var jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代码将 jQuery 生成 deferred 对象,转为一个新的 ES6 的 Promise 对象。
如果 Promise.resolve() 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为 fulfilled。

1
2
3
4
5
var p = Promise.resolve('Hello');

p.then(function (s) {
console.log(s); // Hello
});

上面代码生成一个新的 Promise 对象的实例 p,它的状态为 fulfilled,所以回调函数会立即执行,Promise.resolve() 方法的参数就是回调函数的参数。
如果 Promise.resolve() 方法的参数是一个 Promise 对象的实例,则会被原封不动地返回。

4.4. Promise.reject()

Promise.reject() 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。Promise.reject() 方法的参数会被传递给实例的回调函数。

1
2
3
4
5
var p = Promise.reject('出错了');

p.then(null, function (s) {
console.log(s); // 出错了
});

上面代码生成一个 Promise 对象的实例 p,状态为 rejected,回调函数会立即执行。

5. 兼容性说明

如果需要在 IE 中使用 Promise,方法有两种: