美文网首页
ES6标准入门 摘要 (Promise)

ES6标准入门 摘要 (Promise)

作者: Upcccz | 来源:发表于2019-10-16 20:51 被阅读0次

    Promise

    含义

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

    所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

    Promise对象有以下两个特点。

    • 对象的状态不受外界影响。

    有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),从pending变为fulfilled和从pengding变为rejected。

    • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

    如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

    Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

    基本用法

    Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

    resolve将状态变为已成功,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。
    reject将状态变为已失败,在异步操作失败时调用,并将异步操作报错的错误作为参数传递出去。

    Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

    Promise 新建后就会立即执行。

    let promise = new Promise(function(resolve, reject) {
      console.log('Promise');
      resolve();
    });
    
    promise.then(function() {
      console.log('resolved.');
    });
    
    console.log('Hi!');
    
    // Promise
    // Hi!
    // resolved
    

    resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。

    const p1 = new Promise(function (resolve, reject) {
      setTimeout(() => reject(new Error('fail')), 3000)
    })
    
    const p2 = new Promise(function (resolve, reject) {
      setTimeout(() => resolve(p1), 1000)
    })
    
    p2
      .then(result => console.log(result))
      .catch(error => console.log(error))
    // Error: fail
    

    p1和p2都是 Promise 的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。

    注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

    注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。

    一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

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

    then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

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

    getJSON("/posts.json").then(function(json) {
      return json.post;
    }).then(function(post) {
      // ... 此时的post 就是 json.post
    },function(err){
    
    });
    

    采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

    Promise.prototype.catch()

    Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

    同时then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

    p.then((val) => console.log('fulfilled:', val))
      .catch((err) => console.log('rejected', err));
    
    // 等同于
    p.then((val) => console.log('fulfilled:', val))
      .then(null, (err) => console.log("rejected:", err));
    
    // 并不等同于
    p.then((val) => console.log('fulfilled:', val), err => console.log("rejected:", err))
    

    Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

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

    一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。理由可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)

    跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

    const someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
      });
    };
    
    someAsyncThing().then(function() {
      console.log('everything is great');
      // 不会执行,因为要执行也执行catch 而catch没有定义。
    });
    
    setTimeout(() => { console.log(123) }, 2000);
    // Uncaught (in promise) ReferenceError: x is not defined
    // 123
    

    会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

    这个脚本放在服务器执行,退出码就是0(即表示执行成功)。不过,Node 有一个unhandledRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。

    process.on('unhandledRejection', function (err, p) {
      // 第一个是错误对象
      // 第二个是报错的 Promise 实例,它可以用来了解发生错误的环境信息。
      throw err;
    });
    

    注意,Node 有计划在未来废除unhandledRejection事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码不为 0。

    const promise = new Promise(function (resolve, reject) {
      resolve('ok');
      setTimeout(function () { throw new Error('test') }, 0)
    });
    promise.then(function (value) { console.log(value) })
    .catch(err => {
      console.log('err', err);
      // 不会执行
    });
    // ok
    // Uncaught Error: test
    

    上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

    一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。即使catch没有被执行,这个后面接着catch后面又接着调用的then方法还是会执行,此时如果这个then方法报错了,就与前面的catch无关了。

    catch方法之中,还能再抛出错误。如果之后没有别的catch方法,就会导致这个错误不会被捕捉。

    Promise.prototype.finally()

    finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

    finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。即无论Promise是执行then操作还是执行catch操作都需要执行的代码,可以放在finally方法中。常用场景:发起请求时,激活loading,而无论你请求成功与否都需要关闭loading。

    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    

    它的实现也很简单。

    Promise.prototype.finally = function (callback) {
      let P = this.constructor;
      return this.then(
        value  => P.resolve(callback()).then(() => value), 
        // 这个then的参数其实就是callback的返回值,但是这里没有使用这个参数,而是返回了上一个Promise的值
        reason => P.resolve(callback()).then(() => { throw reason })
      );
    };
    
    new Promise(function(resolve){
      resolve();
    })
    .then(result => {})
    .catch(error => {})
    .finally(() => {console.log('finally')}) // finally
    .then(val => {
      console.log(val); // undefined
    });
    

    上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。

    Promise.all()

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

    const p = Promise.all([p1, p2, p3]);
    

    Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 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的回调函数。

    // 生成一个Promise对象的数组
    const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
      return getJSON('/post/' + id + ".json");
    });
    
    Promise.all(promises).then(function (posts) {
      // 如果promises数组所有的getJSON异步操作都成功
      // 则posts是一个数组,里面包含6个异步操作成功的结果。
    }).catch(function(reason){
      // 如果promises数组只要有一个getJSON异步操作都失败
      // 就会将错误返回给catch
    });
    

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

    const p1 = new Promise((resolve, reject) => {
      resolve('hello');
    })
    .then(result => result)
    .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了');
    })
    .then(result => result)
    .catch(e => e);
    
    // p2定义的Promise会执行reject,然后触发catch方法,
    // catch方法返回一个新的Promise实例给了p2变量,该实例即此时的p2的状态也会变成resolved 
    
    Promise.all([p1, p2]) 
    .then(result => console.log(result)) // ["hello", Error: 报错了]
    .catch(e => console.log(e)); // 只有p2没有catch方法,才会触发Promise.all的catch
    

    上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

    Promise.race()

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

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

    上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

    Promise.race方法的参数如果不是 Promise 实例,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

    下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。

    const p = Promise.race([
      fetch('/resource-that-may-take-a-while'),
      new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('request timeout')), 5000)
      })
    ]);
    
    p
    .then(console.log)
    .catch(console.error);
    // 如果 5 秒之内fetch方法无法返回结果,则setTimeout就会执行并抛出错误,率先改变状态
    // 变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
    
    Promise.resolve()

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

    // 将 jQuery 生成的deferred对象,转为一个新的 Promise 对象。
    const jsPromise = Promise.resolve($.ajax('/whatever.json'));
    
    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
    

    Promise.resolve方法的参数分成四种情况。

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

    • 参数是一个thenable对象,thenable对象指的是具有then方法的对象,比如下面这个对象。Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

    let thenable = {
      then: function(resolve, reject) {
        resolve(42);
      }
    };
    
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
      console.log(value);  // 42
    });
    
    • 参数不是具有then方法的对象,或根本就不是对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。Promise.resolve方法的参数,会同时传给回调函数。
    const p = Promise.resolve('Hello');
    
    p.then(function (s){
      console.log(s)
    });
    // Hello
    
    • 不带有任何参数

    Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

    所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。

    const p = Promise.resolve();
    
    p.then(function () {
      // ...即then回调中也接受不到参数
    });
    
    Promise.reject()

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

    const p = Promise.reject('出错了');
    
    // 等同于
    
    const p = new Promise((resolve, reject) => reject('出错了'))
    p.then(null, function (s) {
      console.log(s) // 出错了
    });
    

    注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

    const thenable = {
      then(resolve, reject) {
        reject('出错了');
      }
    };
    
    Promise.reject(thenable)
    .catch(e => {
      console.log(e === thenable)
    })
    // true
    // Promise.reject方法的参数是一个thenable对象,执行以后
    // 后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。
    
    Promise.try()

    实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。

    但是有一个缺点,就是如果f是同步函数,那么它会在本轮事件循环的末尾执行。

    const f = () => console.log('now');
    Promise.resolve().then(f);
    console.log('next');
    // next
    // now
    

    那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。

    第一种写法是用async函数来写。

    const f = () => console.log('now');
    (async () => f())()
    .then(...)
    .catch(...);
    console.log('next');
    // now
    // next
    // 因为async函数总是返回一个Promise
    // 如果async函数的执行有返回值,那么这个返回这就会被传递到then方法的参数中
    
    // 同时如果f是同步函数,那么就会async 函数中同步执行
    // 如若是异步,那么在then方法中的代码也能正确处理这种情况
    

    第二种写法是使用new Promise()。

    const f = () => console.log('now');
    (
      () => new Promise(
        resolve => resolve(f()) 
        // Promise 新建后的参数(传入的函数)就会立即执行,是同步队列, resolve(f())就会被立即执行
        // 如果是异步,就会将f()函数调用之后的返回值传递给then函数正确处理
      )
    )();
    console.log('next');
    // now
    // next
    

    鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法替代上面的写法。

    const f = () => console.log('now');
    Promise.try(f);
    console.log('next');
    // now
    // next
    

    下面代码中,database.users.get()返回一个 Promise 对象,如果抛出异步错误,可以用catch方法捕获,就像下面这样写

    
    function getUsername(userId) {
      return database.users.get({id: userId})
      .then(function(user) {
        return user.name;
      });
    }
    
    database.users.get({id: userId})
    .then(...)
    .catch(...)
    

    但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。

    try {
      database.users.get({id: userId})
      .then(...)
      .catch(...)
    } catch (e) {
      // ...
    }
    

    使用这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。

    Promise.try(() => database.users.get({id: userId}))
      .then(...)
      .catch(...)
    

    事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

    相关文章

      网友评论

          本文标题:ES6标准入门 摘要 (Promise)

          本文链接:https://www.haomeiwen.com/subject/mbkhuctx.html