资讯专栏INFORMATION COLUMN

你真的了解JavaScript的Promise吗?

darkbaby123 / 820人阅读

摘要:说明状态改变的调用是同步于的。如果在构造函数的回调函数中或的回调函数中发生了异常,返回的会自动。避免了发送重复的请求。

什么是Promise

Promise代理了一个可能要在未来才能到达的值[[PromiseValue]]。Promise的一个最重要的特点是,你可以通过then来指定当[[PromiseValue]]到来时(或到来失败时)调用的handler

Promise的4种状态

fulfilled - 成功,[[PromiseValue]]是成功获取到的值

rejected - 失败,[[PromiseValue]]是失败的原因

pending - [[PromiseValue]]还没有到达

settled - [[PromiseValue]]已经有结果(fulfilled或rejected)

创建Promise 方式1:new Promise(executor)
new Promise( /* executor */ function(resolve, reject) { ... } );

传入Promise()的参数叫做executor,它封装了获取[[PromiseValue]]的过程

在初始化Promise的过程中,executor被Promise内部代码执行,并给executor传入2个参数:resolve函数, reject函数。

MDN文档:The executor function is executed immediately by the Promise implementation, passing resolve and reject functions (the executor is called before the Promise constructor even returns the created object).

即使在executor中调用了resolve或reject,也会先执行完当前的executor函数体(不能像return一样直接退出函数体)

var p1 = new Promise(function (res, rej) {
  console.log("before res");
  res("ok!");
  console.log("after res");
});
console.log("after p1 init, p1:", p1);

// before res
// after res  它被输出说明:即使在executor中调用了resolve或reject,也会先执行完当前的executor函数体,而不像return那样立即退出函数
// after p1 init, p1: Promise { "ok!" } 它在"before/after res"以后才被输出说明:Promise在初始化过程中就会同步地调用executor(called synchronously)

executor的resolve或reject执行以后,Promise的状态就立刻改变(change synchronously)

var p1 = new Promise(function (res, rej) {
  setImmediate(function() {
    res("haha");
    console.log("after res", p1); // executor的res函数执行完毕以后,p1状态已经变为fulfilled
  });
});
console.log("after init", p1);  // 由于executor的res或rej函数还未执行,p1处于pending状态

// after init Promise {  }
// after res Promise { "haha" }

方式2:Promise.resolve(value)

Promise.resolve(value);

返回一个fulfilled的Promise,[[PromiseValue]]为value。

Promise.resolve(promise);

直接返回参数promise。

Promise.resolve(thenable);

将thenable转换为Promise,Promise的状态和[[PromiseValue]]跟随thenable。thenable会在后文讨论。

方式3:Promise.reject(reason)

返回一个rejected的Promise。
Promise.reject(reason)就是下面代码的语法糖形式:

new Promise(function(resolve, reject){
    reject(reason);
});

它没有Promise.resolve(something)这么复杂,不管传入什么,它直接将参数reason作为reject原因,返回一个rejected Promise,即使你闲着没事干传一个Promise进去:

var p = Promise.resolve("res");
Promise.reject(p)
    .then((val) => {
        console.log("111", val);
    }, (err) => {
        console.log("222", err === p);
    });
// 222 true
为了方便debug,最好传入Error实例。
Promise的核心方法:then(onFulfilled[, onRejected]) 先说说then模式

then模式:你先把成功和失败时要调用的handler传给then函数等到时机成熟以后(进入settled状态以后)then函数就帮你调用合适的那个handler。存在一个这样的then函数的对象叫做Thenable对象

// 一个简单的Thenable对象
var thenable = {
  then: function (onFulfilled, onRejected) {
    // setTimeout模拟一个需要花2秒的异步过程
    setTimeout(function () {
      var num = Math.random();
      if (num > 0.5) {
        onFulfilled(num);
      } else {
        onRejected(num);
      }
    }, 2000);
  }
}
// 使用方式
thenable.then(
  function (result) {
    console.log("get result:", result);
  },
  function (err) {
    console.log("get error:", err);
  });
then模式类似于我们经常使用的Callback模式。
说回Promise的then方法

Promise的then方法其实就是在普通的then模式的基础上增加了链式调用的功能:then函数返回Promise对象,前一个Promise对象进入settled状态以后才调用下一个then函数的handler。

Promise.prototype.then()涉及2个Promise对象:

调用then方法的Promise对象,这里用p1表示

调用then以后返回的Promise对象,这里用p2表示

p2 = p1.then(onFulfilled, onRejected);

then的作用就是,立即返回一个pending状态的Promise:p2,并在p1进入settled状态以后自动帮你调用handler

如果进入fulfilled状态(成功),自动调用onFulfilled

如果进入rejected状态(失败),自动调用onRejected

在调用完handler以后,会根据handler的返回值触发p2的状态改变

如果handler返回一个普通值val,p2状态立即(synchronously)变化:pending-->fulfilled,且p2的[[PromiseValue]]为val。

如果handler中throw一个错误err,p2状态立即(synchronously)变化:pending-->rejected,且p2的[[PromiseValue]]为err。

如果handler返回一个settled的Promise对象temp,p2状态立即(synchronously)变化:pending-->与temp相同的状态,且p2的[[PromiseValue]]与temp的[[PromiseValue]]相同。

如果handler返回一个pending的Promise对象temp,p2的状态不立即改变,而是等到temp进入settled状态以后,p2的状态再(异步地)改变:pending-->与temp相同的状态,且p2的[[PromiseValue]]与temp的[[PromiseValue]]相同。

在这里我们只用关注p2是如何改变的,在后文我会解释p2是什么时候改变的(同步还是异步)以及p2的handler是什么时候调用的。

举个例子:

Promise.resolve("result").then(onFulfilled1, onRejected1).then(onFulfilled2, onRejected2);

这等价于:

var p1 = Promise.resolve("result");
var p2 = p1.then(onFulfilled1, onRejected1);
var p3 = p2.then(onFulfilled2, onRejected2);

因为p1是fulfilled状态,所以p1的成功handler——onFulfilled1被调用。

如果onFulfilled1返回普通值(不是Promise),那么p2的状态变化:pending-->fulfilled,且p2的[[PromiseValue]]为onFulfilled1的返回值。接下来p2的成功handler——onFulfilled2被调用,且传入onFulfilled2的参数为p2的[[PromiseValue]]

如果onFulfilled1返回的是Promise,那么p2的状态和[[PromiseValue]]都跟随这个被返回的Promise。onFulfilled2将在p2 fulfilled以后被调用,onRejected2将在p2 rejected以后被调用,传入的参数都是p2的[[PromiseValue]]

依此类推,p3的状态和[[PromiseValue]]都取决于onFulfilled2/onRejected2的返回值。

handler何时被调用

handler的调用异步于then的调用。

A异步于B的意思:A与B在JavaScript消息队列中属于不同的消息。当前消息的调用栈完全退出以后,Event loop再处理下一个消息。Event loop处理完B消息以后可能要再处理0个或多个消息才能处理到A。

如果p1是通过new Promise(executor)的方式得到,那么除了满足第一条以外,p1 handler的调用还异步于executor中resolve()reject()的调用。

如果Promise是由then返回的:

var p1 = Promise.resolve("haha");
var p2 = p1.then(p1_handler);
p2.then(p2_handler)

那么除了满足第一条以外,如我在之前讨论then的时候所说:

如果p1_handler产生同步的结果(包括返回普通值、抛出异常、返回settled Promise),则p2状态立即变化,且p2_handler立即调用。这两者都是同步于p1_handler进行的。

如果p1_handler产生异步的结果(返回pending Promise,temp),则p2状态随着temp自动改变,且p2_handler在p2状态改变以后自动调用。这两者不仅异步于p1_handler进行,而且异步于temp的状态改变。

接下来我们一个一个地讨论。

1. handler的调用异步于then的调用

在调用then为p1指定handler以后,并不会立即触发handler的调用,而是向JavaScript消息队列中增加一个消息,然后继续执行then之后的代码。等到then所在的执行栈完全弹出,Event loop再处理下一个消息。处理完若干个消息以后,Event loop处理到handler的消息。处理这个消息的时候,先检查p1是否为settled,如果是,则调用对应的handler。

即使p1在调用then时就是settled状态,handler的调用也是异步的。p1在调用then时是pending状态的话就更不用说了。

var p1 = Promise.resolve("haha");
// p1在调用then时就是settled状态
p1.then((val) => {
  // 在nextTick以后,p1 handler才调用
  console.log("in p1 handler, p1:", p1);
});
console.log("after then called, p1:", p1);
process.nextTick(() => {
  console.log("nextTick, p1:", p1);
});

// after then called, p1: Promise { "haha" }
// nextTick, p1: Promise { "haha" }
// in p1 handler, p1: Promise { "haha" }

这也是为什么then返回的p2(在刚被返回的时候)必定处于pending状态。因为p1 handler的调用异步于p1的产生(也就异步于p2的产生)。p2需要等待p1 handler异步调用并返回结果才能改变状态,因此在handler被调用以前,p2都是pending状态:

var p1 = Promise.resolve("haha");
console.log("p1", p1);
var p2 = p1.then((val) => {
  console.log("in p1 handler");
  return "xixi";
});
console.log("p2", p2);
setImmediate(() => {
  console.log("setImmediate p2", p2);
});

// p1 Promise { "haha" }
// p2 Promise {  }
// in p1 handler
// setImmediate p2 Promise { "xixi" }
2. handler的调用异步于executor中resolve()reject()的调用

如果p1是通过new Promise(executor)的方式得到,那么除了满足第一条以外,p1 handler的调用还异步于executor中resolve()reject()的调用。
也就是说resolve()reject()的调用并不会立即触发handler的调用,而是向JavaScript消息队列中增加一个消息,等待Event loop处理到这个消息。处理这个消息的时候会调用handler。
例子(用node.js运行):

var global_val = "old value";

var p1 = new Promise((res) => {
  setTimeout(function () {
    res("haha");
    console.log(p1); // p1的状态立刻改变
    console.log("immediately", global_val); // 但是此时p1的handler还没有调用
    process.nextTick(function () {
      console.log("nextTick", global_val);  // 此时p1的handler还是没有调用
    });
    setImmediate(function() {
      console.log("setImmediate", global_val);  // 此时p1的handler已经调用
    });
  }, 1000);
});
var p2 = p1.then(() => {
  // p1的handler
  console.log("in p1 handler");
  global_val = "new value";
});

// Promise { "haha" }
// immediately old value
// nextTick old value
// in p1 handler
// setImmediate new value
为什么handler要异步于executor的resolve()reject()调用

因为在executor的resolve()reject()的调用以后可能还有其他代码要同步执行(当前handler还没有结束)。前一个handler都还没有执行完,自然不应该开始下一个handler的执行。(handler的执行不应该嵌套,而应该串行)

比如在上面global_val的例子中,传入setTimeout的函数就是一个handler,调用res("haha")的时候这个handler还有很多代码要执行。那么下一个handler(p1的handler)不应该打断这些代码的执行。

3. 如果Promise是由then返回的

如果Promise是由then返回的:

var p1 = Promise.resolve("haha");
var p2 = p1.then(p1_handler);
p2.then(p2_handler)

那么除了满足第一条以外,如我在之前讨论then的时候所说:

如果p1_handler产生同步的结果(包括返回普通值、抛出异常、返回settled Promise),则p2状态立即变化,且p2_handler立即调用。这两者都是同步于p1_handler进行的。

如果p1_handler产生异步的结果(返回pending Promise,temp),则p2状态随着temp自动改变,且p2_handler在p2状态改变以后自动调用。这两者不仅异步于p1_handler进行,而且异步于temp的状态改变。

这里给出一个测试代码供大家自行验证,注释中有说明,并且可以通过注释/解注释来修改p1 handler的返回结果。

// 如果p1_handler产生同步的结果(包括返回普通值、抛出异常、返回settled Promise),则p2状态立即变化,且p2_handler立即调用。这两者都是同步于p1_handler进行的。
// 如果p1_handler产生异步的结果(返回pending Promise,temp),则p2状态随着temp自动改变,且p2_handler在p2状态改变以后自动调用。这两者不仅异步于p1_handler进行,而且异步于temp的状态改变。

var p1 = Promise.resolve("haha");

var p2 = p1.then((val) => {
    console.log("in p1 handler. p1:", p1, "p2", p2, "p3:", p3);
    process.nextTick(function () {
        // 当p1 handler产生同步的结果时,p2 handler在nextTick之前就被调用,且p2在nextTick时已经settled。说明p2状态改变、p2 handler的调用是同步于p1 handler的。
        // 当p1 handler产生异步的结果时,p2 handler在nextTick之后才被调用,且p2在nextTick时依然pending。说明p2状态改变、p2 handler的调用是异步于p1 handler的。
        console.log("p1 handler nextTick. p1:", p1, "p2", p2, "p3:", p3);
    });

    // throw "err!";
    // return Promise.resolve("heihei");
    // return Promise.reject("heihei");
    // return new Promise(res => {     // temp Promise
    //     setTimeout(function () {
    //         res("heihei");
    //         process.nextTick(function () {
    //             // p2在nextTick时依然是pending,说明p2的状态改变异步于temp的状态改变
    //             console.log("resolve nextTick. p1:", p1, "p2", p2, "p3:", p3);
    //         });
    //     }, 1000);
    // });
    return "heihei";
});

var p3 = p2.then(val => {
    console.log("in p2 success handler. p1:", p1, "p2", p2, "p3:", p3);
    return "xixi";
}, err => {
    console.log("in p2 error handler. p1:", p1, "p2", p2, "p3:", p3);
    return "hoho";
});

process.nextTick(function () {
    // 它先于"in p1 handler"输出可以说明handler是异步于then调用的
    console.log("after then called. p1:", p1, "p2", p2, "p3:", p3);
});
异常处理

Promise rejected 后,将跳至带有拒绝回调的下一个 then()(或具有相同功能的 catch())。如果有then(func1, func2),则 func1 或 func2 中的一个将被调用,而不可能二者均被调用。但如果是 then(func1).catch(func2),则有可能两者均被调用(func1 rejected时)。

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don"t worry about it");
}).then(function() {
  console.log("All done!");
})

蓝线表示 fulfilled 的 promise 路径,红路表示 rejected 的 promise 路径。

如果在promise构造函数的回调函数中then的回调函数中发生了 JavaScript 异常(throw Errow),返回的promise会自动reject。

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain"t JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})
最佳实践:缓存Promise——重复利用异步操作的结果
var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON("story.json");

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

假设我们要分别获取story的各个章节(用getChapter函数),每个章节所在的URL存储在story.jsonchapterUrls数组中。
我们不需要每次要获取章节的时候都先获取一次story.json在拿到章节所在URL,更好的做法是:调用一次getJSON("story.json")就将这个Promise缓存到storyPromise中,将来需要请求story.json的时候只需要重复利用这个fulfilledstoryPromise。避免了发送重复的HTTP请求。

Promise一旦settled,[[PromiseValue]]不会再改变,我们可以将它看作一个定值,多次使用。
因为Promise对象无法被外部改变(无论是意外地还是恶意地),我们可以安全地将这个对象交给其他库使用,而不用担心库会修改到Promise的结果。
最佳实践:一个 Promise fulfilled 以后再执行下一个 Promise

除了手动写then回调来依次执行Promise以外,对于一个数组的任务,我们可以利用array.reduce的循环来创建then回调

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // 上一个Promise fulfilled以后才执行下一个getJSON
  // 从而保证每一个章节是顺序请求的,从而在页面中是顺序显示的
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())
参考资料

https://developers.google.com...

https://developer.mozilla.org...

http://liubin.org/promises-book/

Node 定时器详解讲述了process.nextTick()和Promise回调函数(microtask)的执行顺序。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88449.html

相关文章

  • JavasScript重难点知识

    摘要:忍者级别的函数操作对于什么是匿名函数,这里就不做过多介绍了。我们需要知道的是,对于而言,匿名函数是一个很重要且具有逻辑性的特性。通常,匿名函数的使用情况是创建一个供以后使用的函数。 JS 中的递归 递归, 递归基础, 斐波那契数列, 使用递归方式深拷贝, 自定义事件添加 这一次,彻底弄懂 JavaScript 执行机制 本文的目的就是要保证你彻底弄懂javascript的执行机制,如果...

    forsigner 评论0 收藏0
  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • 会用 Babel ?

    摘要:安装然后在的配置文件加入入口文件引入这样就可以啦,还是可以减少很多代码量的。是参数,等同于执行正常。这个包很简单,就是引用了和,然后生产环境把它们编译到目录下,做了映射,供使用。 引入 这个问题是对自己的发问,但我相信会有很多跟我一样的同学。对于 babel 的使用,近半年来一直停留在与 webpack 结合使用,以及在浏览器开发环境下。导致很多 babel 的包,我都不清楚他们是干嘛...

    mochixuan 评论0 收藏0
  • 2019前端工程师自检清单与思考

    摘要:前端工程师自检清单对于,掌握其语法和特性是最基本的,但是这些只是应用能力,最终仍旧考量仍然是计算机体系的理论知识,所以数据结构,算法,软件工程,设计模式等基础知识对前端工程师同样重要,这些知识的理解程度,可以决定你在前端工程师这条路上能走多 2019前端工程师自检清单 对于JavaScript,掌握其语法和特性是最基本的,但是这些只是应用能力,最终仍旧考量仍然是计算机体系的理论知识,所...

    Honwhy 评论0 收藏0
  • [译] 深入理解 Promise 五部曲:3. 可靠性问题

    摘要:简单的说,即将到来的标准指出是一个,所以作为一个,必须可以被子类化。保护还是子类化这是个问题我真的希望我能创建一个忠实的给及以下。 原文地址:http://blog.getify.com/promis... 如果你需要赶上我们关于Promise的进度,可以看看这个系列前两篇文章深入理解Promise五部曲--1.异步问题和深入理解Promise五部曲--2.控制权转移问题。 Promi...

    XboxYan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<