• 【译】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,完成链式调用

  • 相关阅读:
    位运算学习
    C语言从文件中读取数字
    百度网盘视频加速代码
    算法思想
    递归解决全排列问题
    android studio出现offline情况
    二叉树的遍历(递归与非递归)
    (一)为什么要UML
    Logstash详解之——input模块
    解决maven项目update project会更改jdk版本问题
  • 原文地址:https://www.cnblogs.com/lessfish/p/promise-api.html
Copyright © 2020-2023  润新知