zoukankan      html  css  js  c++  java
  • 【译】JavaScript Promise API

    原文地址:JavaScript Promise API

    在 JavaScript 中,同步的代码更容易书写和 debug,但是有时候出于性能考虑,我们会写一些异步的代码(代替同步代码)。思考这样一个场景,同时触发几个异步请求,当所有请求到位时我们需要触发一个回调,怎么做?Promise 让一切变的简单,越来越多的原生 API 基于 Promise 去实现。那么,什么是 Promise?Promise API 如何使用?

    基于 Promise 的 原生 API

    Promise 主要是为了解决异步的回调地狱。我们熟悉的 XMLHttpRequest API 可以异步使用,但是它没有基于 Promise API。一些原生的 API 已经使用了 Promise,比如:

    • Battery API
    • fetch API(下一代 XHR)
    • ServiceWorker API

    对于 Promise 的测试其实非常简单,使用 SetTimeout 就能当做一个异步的事件来测试。

    Promise 基本用法

    Promise 本质其实是一个构造函数,其接受一个函数作为参数,而这个函数内部一般会写一些异步事件处理的代码,比如 SetTimeout 或者 XMLHttpRequest。异步事件我们一般都会有一个 "失败" 的处理机制,我们还可以给这个作为参数的函数传入两个参数 resolve 和 reject,分别表示异步事件 "成功" 和 "失败" 时的回调函数。

    let p = new Promise((resolve, reject) => {
      // Do an async task
      setTimeout(() => {
        // good condition
        if (Math.random() > 0.5) {
          resolve('The number is bigger than 0.5');
        } else {
          reject('The number is smaller than 0.5');
        }
      }, 1000);
    });
    
    p.then(data => {
      // do something with the result
      console.log(data);
    }, data => {
      // do something with the result
      console.log(data);
    });
    

    对于什么时候调用 resolve(可以粗略理解为异步操作成功)或者 reject(可以粗略理解为异步操作失败)作为异步事件的回调函数,完全取决于开发者。

    以下的代码我们将 XMLHttpRequest 基于 Promise 去实现:

    // From Jake Archibald's Promises and Back:
    // http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promisifying-xmlhttprequest
    
    function get(url) {
      // Return a new promise.
      return new Promise(function(resolve, reject) {
        // Do the usual XHR stuff
        var req = new XMLHttpRequest();
        req.open('GET', url);
    
        req.onload = function() {
          // This is called even on 404 etc
          // so check the status
          if (req.status == 200) {
            // Resolve the promise with the response text
            resolve(req.response);
          }
          else {
            // Otherwise reject with the status text
            // which will hopefully be a meaningful error
            reject(Error(req.statusText));
          }
        };
    
        // Handle network errors
        req.onerror = function() {
          reject(Error("Network Error"));
        };
    
        // Make the request
        req.send();
      });
    }
    
    // Use it!
    get('story.json').then(function(response) {
      console.log("Success!", response);
    }, function(error) {
      console.error("Failed!", error);
    });
    

    Promise 构造函数调用 new 操作符,传入一些异步事件的代码,会生成一个 Promise 实例对象,但是有的时候,我们需要生成一个 Promise 的实例对象,但是并不需要执行一些异步代码,我们可以用 Promise.resolve() 和 Promise().reject() 来做这件事情。

    var userCache = {};
    
    function getUserDetail(username) {
      // In both cases, cached or not, a promise will be returned
    
      if (userCache[username]) {
        // Return a promise without the "new" keyword
        return Promise.resolve(userCache[username]);
      }
    
      // Use the fetch API to get the information
      // fetch returns a promise
      return fetch('users/' + username + '.json')
        .then(function(result) {
          userCache[username] = result;
          return result;
        })
        .catch(function() {
          throw new Error('Could not find user: ' + username);
        });
    }
    

    getUserDetail() 会始终返回一个 Promise 实例,所以 then 等方法可以在该函数返回值(即 Promise 实例)中使用。

    then

    所有 Promise 实例均拥有 then 方法,then 方法上可以定义两个回调函数,第一个回调函数能接收到实例化 Promise 时通过 resolve 方法传递过来的参数(必须),而第二个回调函数对应 reject 方法(可选)。

    new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { resolve(10); }, 1000);
    })
    .then(function(result) {
      console.log(result);
    });
    
    // From the console:
    // 10
    

    当这个 Promise 实例内部调用 resolve 方法时(Pending -> Resolved),then 中的第一个参数代表的回调函数被触发。

    then 也能被链式调用。

    new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { resolve(10); }, 3000);
    })
    .then(function(num) { console.log('first then: ', num); return num * 2; })
    .then(function(num) { console.log('second then: ', num); return num * 2; })
    .then(function(num) { console.log('last then: ', num);});
    
    // From the console:
    // first then:  10
    // second then:  20
    // last then:  40
    

    在 then 方法中,可以直接 return 数据而不是 Promise 对象,在后面的 then 中就可以接收到数据了。

    catch

    catch 方法有两个作用。

    第一个作用可以代替 then 方法的第二个参数。

    let p = new Promise((resolve, reject) => {
      // Do an async task
      setTimeout(() => {
        // good condition
        if (Math.random() > 0.5) {
          resolve('The number is bigger than 0.5');
        } else {
          reject('The number is smaller than 0.5');
        }
      }, 1000);
    });
    
    p.then(data => {
      // do something with the result
      console.log(data);
    }).catch(data => {
      // replace the second argument of then function
      console.log(data);
    });
    

    第二个作用有点像 try-catch,尝试捕获错误。在执行 resolve 的回调(也就是 then 中的第一个参数)时,如果抛出异常了(代码出错),那么会进入这个 catch 方法中。

    let p = new Promise((resolve, reject) => {
      // Do an async task
      setTimeout(() => {
        // good condition
        if (Math.random() > 0) {
          resolve('The number is bigger than 0');
        }
      }, 1000);
    });
    
    p.then(data => {
      // do something with the result
      console.log(data);
      throw new Error();
    }).catch(error => {
      console.log('There is an error.');
    });
    
    // The number is bigger than 0
    // There is an error.
    

    Promise.all

    回到本文开头说的应用场景,如果多个异步事件同时请求,我们需要在所有事件完成后触发回调,Promise.all 方法可以满足。该方法接收一个 Promise 实例组成的数组作为参数,当该数组中的所有 Promise 实例的状态变为 resolved 时,触发回调方法,回调方法的参数是由所有 Promise 实例的 resolve 函数的参数组成的数组。

    Promise.all([promise1, promise2]).then(function(results) {
      // Both promises resolved
    })
    .catch(function(error) {
      // One or more promises was rejected
    });
    

    Promise.all 可以和 fetch 结合使用,因为 fetch 方法始终返回 Promise 实例。

    var request1 = fetch('/users.json');
    var request2 = fetch('/articles.json');
    
    Promise.all([request1, request2]).then(function(results) {
      // Both promises done!
    });
    

    有任意的 Promise 实例抛出异常就会进入 catch 方法,但是需要注意的是,异步事件并不会停止执行

    var req1 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() {
        resolve('First!');
        console.log('req1 ends!');
      }, 4000);
    });
    var req2 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { reject('Second!'); }, 3000);
    });
    Promise.all([req1, req2]).then(function(results) {
      console.log('Then: ', results);
    }).catch(function(err) {
      console.log('Catch: ', err);
    });
    
    // From the console:
    // Catch: Second!
    // req1 ends!
    

    2018/09/10 add: 以上代码,有一个 Promise 抛出异常,Promise.all 里就得不到结果了,如果还是需要得到结果,可以稍作修改(req2 加了个 catch,这样就不会进入 Promise.all 的 catch 里。然后异常的都会返回 undefined):

    var req1 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() {
        resolve('First!');
        console.log('req1 ends!');
      }, 4000);
    });
    
    var req2 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { reject('Second!'); }, 3000);
    }).catch(err => {console.log('err ' +  err)}) // 这里加个 catch
    
    Promise.all([req1, req2]).then(function(results) {
      console.log('Then: ', results);
    }).catch(function(err) {
      console.log('Catch: ', err);
    });
    
    // From the console:
    // err Second!
    // req1 ends!
    // Then:  [ 'First!', undefined ]
    

    Promise.race

    Promise.race 接收参数和 Promise.all 类似,但是有任何实例状态变为 resolved 或者 rejected 时,就会调用 then 或者 catch 中的回调。很显然,它的回调的参数是一个值,并不是一个数组。

    var req1 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { resolve('First!'); }, 8000);
    });
    var req2 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { reject('Second!'); }, 3000);
    });
    Promise.race([req1, req2]).then(function(one) {
      console.log('Then: ', one);
    }).catch(function(one, two) {
      console.log('Catch: ', one);
    });
    
    // From the console:
    // Then: Second!
    

    对于 Promise.race 方法,需要注意的是,虽然有一个异步事件有了结果,便会执行 then 或者 catch 中的回调,但是其他的异步事件其实还会继续执行。

    var req1 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() {
        resolve('First!');
        console.log('req1 end!');
      }, 8000);
    });
    var req2 = new Promise(function(resolve, reject) {
      // A mock async action using setTimeout
      setTimeout(function() { reject('Second!'); }, 3000);
    });
    Promise.race([req1, req2]).then(function(one) {
      console.log('Then: ', one);
    }).catch(function(one, two) {
      console.log('Catch: ', one);
    });
    
    // From the console:
    // Then: Second!
    // req1 end!
    

    有个简单的应用,有个文件的请求我们有多个地址,很显然请求到了任意一个即可。

    熟悉 Promise

    我们有必要掌握 Promise,Promise 可以有效防止回调地狱,优化异步交互,使得异步代码更加直观。而且越来越多的原生 API 基于 Promise 去实现,我们有必要知其然,知其所以然。

    译者补充

    Promise 的主要用法就是将各个异步操作封装成好多 Promise,而一个 Promise 只处理一个异步逻辑。最后将各个 Promise 用链式调用写法串联,在这样处理下,如果异步逻辑之间前后关系很重的话,你也不需要层层嵌套,只需要把每个异步逻辑封装成 Promise 链式调用就可以了。

    promise 模式在任何时刻都处于以下三种状态之一:未完成(Pending)、已完成(Resolved)和拒绝(Rejected)。以 CommonJS Promise/A 标准为例,promise 对象上的 then 方法负责添加针对已完成和拒绝状态下的处理函数。then 方法会返回另一个 promise 对象,以便于形成 promise 管道,这种返回 promise 对象的方式能够支持开发人员把异步操作串联起来,如 then(resolvedHandler, rejectedHandler); 。resolvedHandler 回调函数在 promise 对象进入完成状态时会触发,并传递结果;rejectedHandler 函数会在拒绝状态下调用。

    (2017.02.27)今天看到一道很有意思的关于 Promise 的题目,来自 http://www.cnblogs.com/libin-1/p/6443693.html,稍作修改:

    setTimeout(() => {
      console.log(1);
    }, 0);
    
    new Promise((resolve) => {
      console.log(2);
      resolve();
      console.log(3);
    }).then(() => {
      console.log(4);
    });
    
    console.log(5);
    

    答案是 2 3 5 4 1


    2019.03.09:

    Promise 只能用 .catch 或者 .then 中的第二个函数去捕获错误,是无法用 try..catch 捕获到错误的;async/await 只能用 try..catch 去捕获错误。推荐在 Promise 最后加上 .catch,如果 .catch 和 .then 第二个参数同时存在,会就近捕获错误,.catch 可以放置好几个,而不是只能放在最后,.catch 返回的也是一个 Promise,所以能链式

    Promise 可以被当作队列去执行:

    let p = Promise.resolve();
    
    [1, 2].forEach(item => {
      p.then(() => {
        return new Promise(resolve => {
          setTimeout(() => {
            console.log(item)
            resolve()
          }, item * 1000)
        })
      })
    })
    

    以上代码 1s 后输出 1,2s 后输出 2,是并行的,并不是队列。

    稍作修改:

    let p = Promise.resolve();
    
    [1, 2].forEach(item => {
      p = p.then(() => {
        return new Promise(resolve => {
          setTimeout(() => {
            console.log(item)
            resolve()
          }, item * 1000)
        })
      })
    })
    

    这样就能达成我们的目的,1s 后输出 1,(1+2)s 后输出 2,原理是将返回的 Promise 继续赋值给 p,完成链式调用

  • 相关阅读:
    Mac php使用gd库出错 Call to undefined function imagettftext()
    centos 使用 locate
    Mac HomeBrew 安装 mysql
    zsh 命令提示符 PROMPT
    新的开始
    Java 面试题分析
    Java NIO Show All Files
    正确使用 Volatile 变量
    面试题整理 2017
    有10阶梯, 每次走1,2 or 3 阶,有多少种方式???
  • 原文地址:https://www.cnblogs.com/lessfish/p/promise-api.html
Copyright © 2011-2022 走看看