Javascript Promise example

目录

  1. Promise.resolve
  2. Promise.reject
  3. Promise.then
  4. Promise.catch
  5. Promise.all
  6. Promise.race

上一节Javascript Promise Intro已经详细介绍了Promise,这一节内容讲述的是Promise示例。

1. Promise.resolve

一般情况下我们都会使用 new Promise() 来创建promise对象,但是除此之外我们也可以使用其他方法。

在这里,我们将会学习如何使用 Promise.resolve 和 Promise.reject这两个方法。

1.1 new Promise的快捷方式

静态方法Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。

比如 Promise.resolve(42); 可以认为是以下代码的语法糖。

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

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

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

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

Promise.resolve作为 new Promise() 的快捷方式,在进行promise对象的初始化或者编写测试代码的时候都非常方便。

1.2. Thenable

Promise.resolve 方法另一个作用就是将 thenable 对象转换为promise对象。

ES6 Promises里提到了Thenable这个概念,简单来说它就是一个非常类似promise的东西。

就像我们有时称具有 .length 方法的非数组对象为Array like一样,thenable指的是一个具有 .then 方法的对象。

这种将thenable对象转换为promise对象的机制要求thenable对象所拥有的 then 方法应该和Promise所拥有的 then 方法具有同样的功能和处理过程,在将thenable对象转换为promise对象的时候,还会巧妙的利用thenable对象原来具有的 then 方法。

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

因为jQuery.ajax() 的返回值是 jqXHR Object 对象,这个对象具有 .then 方法。

$.ajax('/json/comment.json');// => 拥有 `.then` 方法的对象

这个thenable的对象可以使用 Promise.resolve 来转换为一个promise对象。

变成了promise对象的话,就能直接使用 then 或者 catch 等这些在 ES6 Promises里定义的方法了。

将thenable对象转换promise对象

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

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

Promise.resolve 只使用了共通的方法 then ,提供了在不同的类库之间进行promise对象互相转换的功能。

这种转换为thenable的功能在之前是通过使用 Promise.cast 来完成的,从它的名字我们也不难想象它的功能是什么。

除了在编写使用Promise的类库等软件时需要对Thenable有所了解之外,通常作为end-user使用的时候,我们可能不会用到此功能。

我们会在后面第4章的Promise.resolve和Thenable中进行详细的说明,介绍一下结合使用了Thenable和Promise.resolve的具体例子。

简单总结一下 Promise.resolve 方法的话,可以认为它的作用就是将传递给它的参数填充(Fulfilled)到promise对象后并返回这个promise对象。

此外,Promise的很多处理内部也是使用了 Promise.resolve 算法将值转换为promise对象后再进行处理的。

2. Promise.reject

Promise.reject(error)是和 Promise.resolve(value) 类似的静态方法,是 new Promise() 方法的快捷方式。

比如 Promise.reject(new Error("出错了")) 就是下面代码的语法糖形式。

new Promise(function(resolve,reject){
    reject(new Error("出错了"));
});

这段代码的功能是调用该promise对象通过then指定的 onRejected 函数,并将错误(Error)对象传递给这个 onRejected 函数。

Promise.reject(new Error("BOOM!")).catch(function(error){
    console.error(error);
});

它和Promise.resolve(value) 的不同之处在于promise内调用的函数是reject而不是resolve,这在编写测试代码或者进行debug时,说不定会用得上。

3. Promise.then

在前面的章节里我们对Promise基本的实例方法 then 和 catch 的使用方法进行了说明。

这其中,我想大家已经认识了 .then().catch() 这种链式方法的写法了,其实在Promise里可以将任意个方法连在一起作为一个方法链(method chain)。

promise可以写成方法链的形式

aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
    console.log(error);
});

如果把在 then 中注册的每个回调函数称为task的话,那么我们就可以通过Promise方法链方式来编写能以taskA → task B 这种流程进行处理的逻辑了。

Promise方法链这种叫法有点长(其实是在日语里有点长,中文还可以 –译者注),因此后面我们会简化为 promise chain 这种叫法。

Promise之所以适合编写异步处理较多的应用,promise chain可以算得上是其中的一个原因吧。

3.1. promise chain

在第一章 promise chain 里我们看到了一个很简单的 then → catch 的例子,如果我们将方法链的长度变得更长的话,那在每个promise对象中注册的onFulfilled和onRejected将会怎样执行呢?

promise chain - 即方法链越短越好。 在这个例子里我们是为了方便说明才选择了较长的方法链。

我们先来看看下面这样的promise chain。

promise-then-catch-flow.js
function taskA() {
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);

上面代码中的promise chain的执行流程,如果用一张图来描述一下的话,像下面的图那样。
promise chain

在 上述代码 中,我们没有为 then 方法指定第二个参数(onRejected),也可以像下面这样来理解。

then
注册onFulfilled时的回调函数

catch
注册onRejected时的回调函数

再看一下 上面的流程图 的话,我们会发现 Task A 和 Task B 都有指向 onRejected 的线出来。

这些线的意思是在 Task A 或 Task B 的处理中,在下面的情况下就会调用 onRejected 方法。

  • 发生异常的时候
  • 返回了一个Rejected状态的promise对象

在上面我们讲到,Promise中的处理习惯上都会采用 try-catch 的风格,当发生异常的时候,会被 catch 捕获并被由在此函数注册的回调函数进行错误处理。

另一种异常处理策略是通过 返回一个Rejected状态的promise对象 来实现的,这种方法不通过使用 throw 就能在promise chain中对 onRejected 进行调用。

关于这种方法由于和本小节关系不大就不在这里详述了,大家可以参考一下第4章 使用reject而不是throw 中的内容。

此外在promise chain中,由于在 onRejected 和 Final Task 后面没有 catch 处理了,因此在这两个Task中如果出现异常的话将不会被捕获,这点需要注意一下。

下面我们再来看一个具体的关于 Task A → onRejected 的例子。

Task A产生异常的例子 ask A 处理中发生异常的话,会按照TaskA → onRejected → FinalTask 这个流程来进行处理。
Task 将上面流程写成代码的话如下所示。

function taskA() {
    console.log("Task A");
    throw new Error("throw Error @ Task A")
}
function taskB() {
    console.log("Task B");// 不会被调用
}
function onRejected(error) {
    console.log(error);// => "throw Error @ Task A"
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);

执行这段代码我们会发现 Task B 是不会被调用的。

在本例中我们在taskA中使用了 throw 方法故意制造了一个异常。但在实际中想主动进行onRejected调用的时候,应该返回一个Rejected状态的promise对象。

3.2. promise chain 中如何传递参数

前面例子中的Task都是相互独立的,只是被简单调用而已。

这时候如果 Task A 想给 Task B 传递一个参数该怎么办呢?

答案非常简单,那就是在 Task A 中 return 的返回值,会在 Task B 执行时传给它。

我们还是先来看一个具体例子吧。

function doubleUp(value) {
    return value * 2;
}
function increment(value) {
    return value + 1;
}
function output(value) {
    console.log(value);// => (1 + 1) * 2
}

var promise = Promise.resolve(1);
promise
    .then(increment)
    .then(doubleUp)
    .then(output)
    .catch(function(error){
        // promise chain中出现异常的时候会被调用
        console.error(error);
    });

这段代码的入口函数是 Promise.resolve(1); ,整体的promise chain执行流程如下所示。

  1. Promise.resolve(1); 传递 1 给 increment 函数
  2. 函数 increment 对接收的参数进行 +1 操作并返回(通过return)
  3. 这时参数变为2,并再次传给 doubleUp 函数
  4. 最后在函数 output 中打印结果

promise-then-passing-value

每个方法中 return 的值不仅只局限于字符串或者数值类型,也可以是对象或者promise对象等复杂类型。

return的值会由 Promise.resolve(return的返回值); 进行相应的包装处理,因此不管回调函数中会返回一个什么样的值,最终 then 的结果都是返回一个新创建的promise对象。

也就是说, Promise.then() 不仅仅是注册一个回调函数那么简单,它还会将回调函数的返回值进行变换,创建并返回一个promise对象。

4. Promise.catch

在 前面的Promise#then 的章节里,我们已经简单地使用了 Promise.catch 方法。

这里我们再说一遍,实际上 Promise#catch 只是 promise.then(undefined, onRejected); 方法的一个别名而已。 也就是说,这个方法用来注册当promise对象状态变为Rejected时的回调函数。

4.1. IE8的问题

ie8
上面的这张图,是下面这段代码在使用 polyfill 的情况下在个浏览器上执行的结果。

polyfill是一个支持在不具备某一功能的浏览器上使用该功能的Library。

Promise.catch的运行结果

var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
    console.error(error);
});

如果我们在各种浏览器中执行这段代码,那么在IE8及以下版本则会出现 identifier not found 的语法错误。

这是怎么回事呢? 实际上这和 catch 是ECMAScript的 保留字 (Reserved Word)有关。

在ECMAScript 3中保留字是不能作为对象的属性名使用的。 而IE8及以下版本都是基于ECMAScript 3实现的,因此不能将 catch 作为属性来使用,也就不能编写类似 promise.catch() 的代码,因此就出现了 identifier not found 这种语法错误了。

而现在的浏览器都是基于ECMAScript 5的,而在ECMAScript 5中保留字都属于 IdentifierName ,也可以作为属性名使用了。

在ECMAScript5中保留字也不能作为 Identifier 即变量名或方法名使用。 如果我们定义了一个名为 for 的变量的话,那么就不能和循环语句的 for 区分了。 而作为属性名的话,我们还是很容易区分 object.for 和 for 的,仔细想想我们就应该能接受将保留字作为属性名来使用了。

当然,我们也可以想办法回避这个ECMAScript 3保留字带来的问题。

点标记法(dot notation) 要求对象的属性必须是有效的标识符(在ECMAScript 3中则不能使用保留字),

但是使用 中括号标记法(bracket notation)的话,则可以将非合法标识符作为对象的属性名使用。

也就是说,上面的代码如果像下面这样重写的话,就能在IE8及以下版本的浏览器中运行了(当然还需要polyfill)。

解决Promise.catch标识符冲突问题

var promise = Promise.reject(new Error("message"));
promise["catch"](function (error) {
    console.error(error);
});

或者我们不单纯的使用 catch ,而是使用 then 也是可以避免这个问题的。

使用Promise#then代替Promise#catch

var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
    console.error(error);
});

由于 catch 标识符可能会导致问题出现,因此一些类库(Library)也采用了 caught 作为函数名,而函数要完成的工作是一样的。

而且很多压缩工具自带了将 promise.catch 转换为 promise["catch"] 的功能, 所以可能不经意之间也能帮我们解决这个问题。

如果各位读者需要支持IE8及以下版本的浏览器的话,那么一定要将这个 catch 问题牢记在心中。

5. Promise.all

Promise.all 接收一个 promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then 方法。

前面我们看到的批量获得若干XHR的请求结果的例子,使用 Promise.all 的话代码会非常简单。

之前例子中的 getURL 返回了一个promise对象,它封装了XHR通信的实现。 向 Promise.all 传递一个由封装了XHR通信的promise对象数组的话,则只有在全部的XHR通信完成之后(变为FulFilled或Rejected状态)之后,才会调用 .then 方法。

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 request = {
        comment: function getComment() {
            return getURL('./json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('./json/people.json').then(JSON.parse);
        }
    };
function main() {
    return Promise.all([request.comment(), request.people()]);
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.log(error);
});

这个例子的执行方法和 前面的例子 一样。 不过Promise.all 在以下几点和之前的例子有所不同。

  • main中的处理流程显得非常清晰
  • Promise.all 接收 promise对象组成的数组作为参数
Promise.all([request.comment(), request.people()]);

如果像下面那样使用一个计时器来计算一下程序执行时间的话,那么就可以非常清楚的知道传递给 Promise.all 的promise数组是同时开始执行的。

// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
var startDate = Date.now();
// 所有promise变为resolve后程序退出
Promise.all([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (values) {
    console.log(Date.now() - startDate + 'ms');
    // 128ms
    console.log(values);    // [1,32,64,128]
});

timerPromisefy 会每隔一定时间(通过参数指定)之后,返回一个promise对象,状态为FulFilled,其状态值为传给 timerPromisefy 的参数。

而传给 Promise.all 的则是由上述promise组成的数组。

var promises = [
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
];

这时候,每隔1, 32, 64, 128 ms都会有一个promise发生 resolve 行为。

也就是说,这个promise对象数组中所有promise都变为resolve状态的话,至少需要128ms。实际我们计算一下Promise.all 的执行时间的话,它确实是消耗了128ms的时间。

从上述结果可以看出,传递给 Promise.all 的promise并不是一个个的顺序执行的,而是同时开始、并行执行的。

如果这些promise全部串行处理的话,那么需要 等待1ms → 等待32ms → 等待64ms → 等待128ms ,全部执行完毕需要225ms的时间。

6. Promise.race

接着我们来看看和 Promise.all 类似的对多个promise对象进行处理的 Promise.race 方法。

它的使用方法和Promise.all一样,接收一个promise对象数组为参数。

Promise.all 在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理, 与之相对的是 Promise.race 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。

Promise.all时的例子一样,我们来看一个带计时器的 Promise.race 的使用例子。

// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
// 任何一个promise变为resolvereject 的话程序就停止运行
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (value) {
    console.log(value);    // => 1
});

上面的代码创建了4个promise对象,这些promise对象会分别在1ms,32ms,64ms和128ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后, .then 注册的回调函数就会被调用,这时候确定状态的promise对象会调用 resolve(1) 因此传递给 value 的值也是1,控制台上会打印出1来。

下面我们再来看看在第一个promise对象变为确定(FulFilled)状态后,它之后的promise对象是否还在继续运行。

var winnerPromise = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is winner');
            resolve('this is winner');
        }, 4);
    });
var loserPromise = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is loser');
            resolve('this is loser');
        }, 1000);
    });
// 第一个promise变为resolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
    console.log(value);    // => 'this is winner'
});

我们在前面代码的基础上增加了 console.log 用来输出调试信息。

执行上面代码的话,我们会看到 winnter和loser promise对象的 setTimeout 方法都会执行完毕, console.log 也会分别输出它们的信息。

也就是说, Promise.race 在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行。

在 ES6 Promises 规范中,也没有取消(中断)promise对象执行的概念,我们必须要确保promise最终进入resolve or reject状态之一。也就是说Promise并不适用于 状态 可能会固定不变的处理。也有一些类库提供了对promise进行取消的操作。

参考

Missra

Full-time front-end Engineer

近期日志