Javascript Promise Intro

目录

  1. 前言
  2. Promise介绍
  3. 编写Promise代码
  4. 浏览器支持和 polyfill
  5. 参考

1. 前言

一直以来,JavaScript处理异步都是以callback的方式,在前端开发领域callback机制几乎深入人心。在设计API的时候,不管是浏览器厂商还是SDK开发商亦或是各种类库的作者,基本上都已经遵循着callback的套路。

在callback的模型里边,我们假设需要执行一个异步队列,代码看起来可能像这样:

loadImg('a.jpg', function() {
    loadImg('b.jpg', function() {
        loadImg('c.jpg', function() {
            console.log('all done!');
        });
    });
});

这也就是我们常说的回调金字塔,当异步的任务很多的时候,维护大量的callback将是一场灾难。

或者你会用监听事件和回调来一起解决该问题,但是回调函数和事件并不总是最佳方法,如果关系到异步成功/失败,理想的情况是您希望:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // 如果加载完成则调用执行此语句
}).orIfFailedCallThis(function() {
  // 如果加载失败则执行这里的语句
});

// 或者
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // 当所有的都加载完成则执行此语句
}).orIfSomeFailedCallThis(function() {
  // 当其中一个或者更多失败时候执行此语句
});

幸运的是,这种理想的情况使用 Promise 就能轻松完成所执行的任务。 例如 HTML 图像元素有一个返回 promise 的“ready”方法,我们可以执行:

img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

Promise.all([img1.ready(), img2.ready()]).then(function() {
  // img1img2全部加载完成后的异步执行
}, function() {
  // 一个或者更多加载失败情况都会捕获得到
});

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

2.Promise介绍

首先让我们来了解一下到底什么是Promise。

Promise对象用于一个异步操作的最终完成(或失败)及其结果值的表示。通俗的解释:Promise是一个对象,用来解决异步操作的问题,让异步调用写起来更优雅、美观、便于阅读。从ES6语法上说,Promise对象是一个构造函数,用来生成Promise实例。 Promise并不是从JavaScript中发起的概念。Promise的概念是由CommonJS工作组在Promises/A规范中提出来的。ES6将其写进语言标准,统一了用法,原生提供了promise对象。

下面是使用了Promise进行异步处理的一个例子

var promise = new Promise((resolve, reject) => {
    // 异步处理结束后、调用resolve  reject
});
promise.then(function(result){
    // 获取文件内容成功时的处理
}).catch(function(error){
    // 获取文件内容失败时的处理
});

我们可以向这个预设了抽象化异步处理的promise对象, 注册这个promise对象执行成功时和失败时相应的回调函数。 这和回调函数方式相比有哪些不同之处呢? 在使用promise进行一步处理的时候,我们必须按照接口规定的方法编写处理代码。 也就是说,除promise对象规定的方法(这里的 then 或 catch)以外的方法都是不可以使用的, 而不会像回调函数方式那样可以自己自由的定义回调函数的参数,而必须严格遵守固定、统一的编程方式来编写代码。

这样,基于Promise的统一接口的做法, 就可以形成基于接口的各种各样的异步处理模式。所以,promise的功能是可以将复杂的异步处理轻松地进行模式化, 这也可以说得上是使用promise的理由之一。

2.1 ES6 Promises API

在 ES6 Promises 标准中定义的常用API有下面几种类型。

1. Promise.then

//语法
promise.then(onFulfilled, onRejected);

示例:Promise.then

var promise = new Promise(function(resolve, reject) {
    resolve("传递给then的值");
});
promise.then(function(value) {
    console.log(value);
}, function(error) {
    console.error(error);
});

这段代码创建一个promise对象,定义了处理onFulfilled和onRejected的函数(handler),然后返回这个promise对象。这个promise对象会在变为resolve或者reject的时候分别调用相应注册的回调函数。 (1)当handler返回一个正常值的时候,这个值会传递给promise对象的onFulfilled方法。 (2)定义的handler中产生异常的时候,这个值则会传递给promise对象的onRejected方法。

2. Promise.catch

//语法
promise.catch(onRejected);

等价于promise.then(undefined, onRejected) 的语法糖。 示例:Promise.catch

var promise = new Promise(function(resolve, reject) {
    resolve("要传递给then的值");
});
promise.then(function(value) {
    console.log(value);
}).catch(function(error) {
    console.log(error);
});

3. Promise.resolve

//语法
Promise.resolve(promise);
Promise.resolve(thenable);
Promise.resolve(object);

示例:Promise.resolve

var taskName = "task1";
asyncTask(taskName).then(function(value) {
    console.log(value);
}).catch(function(error) {
    console.error(error);
});
function asyncTask(name) {
    return Promise.resolve(name).then(function(value) {
        return "Done! " + value;
    })
}

根据接收到的参数不同,返回不同的promise对象。 (1)接收到promise对象参数的时候:返回的还是接收到的promise对象 (2)接收到thenable类型的对象的时候:返回一个新的promise对象,这个对象具有一个 then 方法 (3)接收的参数为其他类型的时候(包括JavaScript对或null等):返回一个将该对象作为值的新promise对象

4. Promise.reject

//语法
Promise.reject(object)

示例:Promise.reject

var r = Promise.reject(new Error("error"));
console.log(r === Promise.reject(r));       // false

var r = Promise.resolve("test");
console.log(r === Promise.resolve(r));      // true

和 Promise.resolve不同的是,即使Promise.reject接收到的参数是一个promise对象,该函数也还是会返回一个全新的promise对象。

5. Promise.all

//语法
Promise.all(promiseArray)

示例

var p1 = Promise.resolve(1),
    p2 = Promise.resolve(2),
    p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function(results) {
    console.log(results);       // [1, 2, 3]
});

生成并返回一个新的promise对象。 参数传递promise数组中所有的promise对象都变为resolve的时候,该方法才会返回, 新创建的promise则会使用这些promise的值。 如果参数中的任何一个promise为reject的话,则整个Promise.all调用会立即终止,并返回一个reject的新的promise对象。

6. Promise.race

//语法
Promise.all(promiseArray)

示例

var p1 = Promise.resolve(1),
    p2 = Promise.resolve(2),
    p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function(results) {
    console.log(results);       // 1
});

生成并返回一个新的promise对象。 参数 promise 数组中的任何一个promise对象如果变为resolve或者reject的话, 该函数就会返回,并使用这个promise对象的值进行resolve或者reject。

2.2 Promise工作流

我们先来看一看下面的示例代码。

function asyncFunction() {
    //new Promise构造器之后,会返回一个promise对象
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve('Async Hello world');
        }, 16);
    });
}
//promise对象用设置 `.then` 调用返回值时的回调函数。
asyncFunction().then(function (value) {
    console.log(value);    // => 'Async Hello world'
}).catch(function (error) {
    console.log(error);
});

asyncFunction 这个函数会返回promise对象, 对于这个promise对象,我们调用它的 then 方法来设置resolve后的回调函数, catch 方法来设置发生错误时的回调函数。

该promise对象会在setTimeout之后的16ms时被resolve, 这时 then 的回调函数会被调用,并输出 'Async Hello world'

在这种情况下 catch 的回调函数并不会被执行(因为promise返回了resolve), 不过如果运行环境没有提供 setTimeout 函数的话,那么上面代码在执行中就会产生异常,在 catch 中设置的回调函数就会被执行。

当然,像promise.then(onFulfilled, onRejected) 的方法声明一样, 如果不使用 catch 方法只使用 then 方法的话,如下所示的代码也能完成相同的工作。

asyncFunction().then(function (value) {
    console.log(value);
}, function (error) {
    console.log(error);
});

2.3 Promise的状态

我们已经大概了解了Promise的处理流程,接下来让我们来稍微整理一下Promise的状态。

new Promise 实例化的promise对象有以下三个状态。

“has-resolution” - Fulfilled resolve(成功)时。此时会调用 onFulfilled

“has-rejection” - Rejected
reject(失败)时。此时会调用 onRejected

“unresolved” - Pending
既不是resolve也不是reject的状态。也就是promise对象刚被创建后的初始化状态等

Promise状态

Figure 1. promise states

在 ECMAScript Language Specification ECMA-262 6th Edition – DRAFT 中 [[PromiseStatus]] 都是在内部定义的状态。 由于没有公开的访问 [[PromiseStatus]] 的用户API,所以暂时还没有查询其内部状态的方法。

到此在本文中我们已经介绍了promise所有的三种状态。

promise对象的状态,从Pending转换为Fulfilled或Rejected之后, 这个promise对象的状态就不会再发生任何变化。也就是说,Promise与Event等不同,在.then 后执行的函数可以肯定地说只会被调用一次。另外,Fulfilled和Rejected这两个中的任一状态都可以表示为Settled(不变的)。

Settled resolve(成功) 或 reject(失败)。

从Pending和Settled的对称关系来看,Promise状态的种类/迁移是非常简单易懂的。

当promise的对象状态发生变化时,用.then 来定义只会被调用一次的函数。

JavaScript Promises - Thinking Sync in an Async World // Speaker Deck 这个ppt中有关于Promise状态迁移的非常容易理解的说明。

3. 编写Promise代码

这里我们来介绍一下如何编写Promise代码。

3.1 创建promise对象

  1. new Promise(fn) 返回一个promise对象
  2. 在fn 中指定异步等处理
    • 处理结果正常的话,调用resolve(处理结果值)
    • 处理结果错误的话,调用reject(Error对象)

按这个流程我们来实际编写下promise代码吧。

我们的任务是用Promise来通过异步处理方式来获取XMLHttpRequest(XHR)的数据。

创建XHR的promise对象
首先,创建一个用Promise把XHR处理包装起来的名为 getURL 的函数。

function getURL(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();
    });
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){
    console.error(error);
});

getURL 只有在通过XHR取得结果状态为200时才会调用 resolve - 也就是只有数据取得成功时,而其他情况(取得失败)时则会调用 reject 方法resolve(req.responseText) 在response的内容中加入了参数。 resolve方法的参数并没有特别的规则,基本上把要传给回调函数参数放进去就可以了。 ( then 方法可以接收到这个参数值) 熟悉Node.js的人,经常会在写回调函数时将 callback(error, response) 的第一个参数设为error对象,而在Promise中resolve/reject则担当了这个职责(处理正常和异常的情况),所以 在resolve方法中只传一个response参数是没有问题的。

接下来我们来看一下reject函数。 XHR中 onerror 事件被触发的时候就是发生错误时,所以理所当然调用reject。 这里我们重点来看一下传给reject的值。发生错误时要像这样 reject(new Error(req.statusText)); ,创建一个Error对象后再将具体的值传进去。 传给reject 的参数也没有什么特殊的限制,一般只要是Error对象(或者继承自Error对象)就可以。 传给reject 的参数,其中一般是包含了reject原因的Error对象。 本次因为状态值不等于200而被reject,所以reject 中放入的是statusText。 (这个参数的值可以被 then 方法的第二个参数或者 catch 方法中使用)

3.2 编写promise对象处理方法

让我们在实际中使用一下刚才创建的返回promise对象的函数

getURL("http://example.com/"); // => 返回promise对象

如Promises介绍中做的简单介绍一样,promise对象拥有几个实例方法, 我们使用这些实例方法来为promise对象创建依赖于promise的具体状态、并且只会被执行一次的回调函数。

为promise对象添加处理方法主要有以下两种

  • promise对象被 resolve 时的处理(onFulfilled)
  • promise对象被 reject 时的处理(onRejected)

Promise对象处理

首先,我们来尝试一下为 getURL 通信成功并取到值时添加的处理函数。 此时所谓的 通信成功 , 指的就是在被resolve后, promise对象变为FulFilled状态 。 被resolve后的处理,可以在.then 方法中传入想要调用的函数。

var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){ //为了方便理解我们把函数命名为 `onFulfilled`
    console.log(value);
});

getURL函数 中的 resolve(req.responseText); 会将promise对象变为resolve(Fulfilled)状态, 同时使用其值调用 onFulfilled 函数。

不过目前我们还没有对其中可能发生的错误做任何处理, 接下来,我们就来为 getURL 函数添加发生错误时的异常处理。

此时 发生错误 , 指的也就是reject后 promise对象变为Rejected状态 。

被reject后的处理,可以在.then 的第二个参数 或者是在 .catch 方法中设置想要调用的函数。

把下面reject时的处理加入到刚才的代码,如下所示。

var URL = "http://httpbin.org/status/500"; //服务端返回的状态码为500
getURL(URL).then(function onFulfilled(value){ //为了方便理解函数被命名为 `onRejected`
    console.log(value);
}).catch(function onRejected(error){ 
    console.error(error);
});

在getURL 的处理中发生任何异常,或者被明确reject的情况下, 该异常原因(Error对象)会作为 .catch 方法的参数被调用。

其实 .catch只是 promise.then(undefined, onRejected) 的别名而已, 如下代码也可以完成同样的功能。

getURL(URL).then(onFulfilled, onRejected); //	onFulfilled, onRejected 是和刚才相同的函数

一般说来,使用.catch来将resolve和reject处理分开来写是比较推荐的做法, 这两者的区别会在then和catch的区别中再做详细介绍。

总结

在本章我们简单介绍了以下内容:

  • new Promise 方法创建promise对象
  • .then.catch 添加promise对象的处理函数

到此为止我们已经学习了Promise的基本写法。 其他很多处理都是由此基本语法延伸的,也使用了Promise提供的一些静态方法来实现。

实际上即使使用回调方式的写法也能完成上面同样的工作,而使用Promise方式的话有什么优点么?在本小节中我们没有讲到两者的对比及Promise的优点。在接下来的章节中,我们将会对Promise优点之一,即错误处理机制进行介绍,以及和传统的回调方式的对比。

4. 浏览器支持和 polyfill

现在,promise 已在各浏览器中实现。

在 Chrome 32、Opera 19、Firefox 29、Safari 8 和 Microsoft Edge 中,promise 默认启用。

如要使没有完全实现 promise 的浏览器符合规范,或向其他浏览器和 Node.js 中添加 promise,请查看 polyfill(gzip 压缩大小为 2k)。

5. 参考

Missra

Full-time front-end Engineer

近期日志