zoukankan      html  css  js  c++  java
  • 拥抱javascript的promise

    原文地址

    在本文中,我们看到如何通过promises,在javascript中实现异步调用,从而写出更加优雅的代码。这篇文章不是一个完整,有深度的对promises的探索。如果是为了了解更深入,我强烈建议你去看下 Jake Archibald's post on HTML5 Rocks

    在这里我将会使用es6-promise库,这是一个在ECMAScript6中存在的原生promise实现。
    我所有的代码都会在Node.js运行,和在浏览器环境下运行结果一致。当你在代码中看到Promise的时候,这里都是基于polyfill使用的,但是如果你读到了浏览器中广泛实现的单词promises,你将会发现所有运行结果是一样的。

    解决错误

    第一个要处理的主题就是promise的错误处理。这个问题曾经被许多人问过,也阻碍了很多人的理解。让我们看下下面的代码,当我运行如下内容时,你希望打印出什么?

    var someAsyncThing = function() {
    	return new Promise(function(resolve, reject) {
    	    // this will throw, x does not exist
    	    resolve(x + 2);
    	});
    };
    
    someAsyncThing().then(function() {
    	console.log('everything is great');
    });
    

    你可能觉得一个错误会被抛出,因为x并不存在。这种情况只会在上面这段代码在promise外面时才会发生。但是,运行这段代码,控制台不会有任何输出,没有错误被抛出。在promise中,任何错误将被抛出,被当做一个promise 拒绝异常。这意味着我们必须要捕获这种异常:

    someAsyncThing().then(function() {
      console.log('everything is great');
    }).catch(function(error) {
      console.log('oh no', error);
    });
    

    现在运行这个:

    oh no [ReferenceError: x is not defined]
    

    你需要理解promises的链式异常捕获机制。例如:

    var someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        // this will throw, x does not exist
        resolve(x + 2);
      });
    };
    
    var someOtherAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        reject('something went wrong');
      });
    };
    
    someAsyncThing().then(function() {
      return someOtherAsyncThing();
    }).catch(function(error) {
      console.log('oh no', error);
    });
    

    这里我们依旧得到结果:oh no [ReferenceError: x is not defined]。因为someAsyncThing 中使用reject。如果someAsyncThing 使用resolve,我们依旧会在someOtherAsyncThing在reject时返回这个错误:

    var someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        var x = 2;
        resolve(x + 2);
      });
    };
    
    var someOtherAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        reject('something went wrong');
      });
    };
    
    someAsyncThing().then(function() {
      return someOtherAsyncThing();
    }).catch(function(error) {
      console.log('oh no', error);
    });
    

    现在,我们获得的结果是:oh no something went wrong。当一个promises reject时,链式的第一个捕获会被调用。

    另一个重要的点是这里的捕获没有什么特别的地方。它只是一个方法,注册了一个处理函数,当promises reject时触发该处理函数。它不会停止下一步的执行:

    someAsyncThing().then(function() {
      return someOtherAsyncThing();
    }).catch(function(error) {
      console.log('oh no', error);
    }).then(function() {
      console.log('carry on');
    });
    

    以上给出的代码,一旦出现 reject,carry on将会在控制台被打印出。当然,如果在catch中的代码抛出异常,则不会是这样:

    someAsyncThing().then(function() {
      return someOtherAsyncThing();
    }).catch(function(error) {
      console.log('oh no', error);
      // y is not a thing!
      y + 2;
    }).then(function() {
      console.log('carry on');
    });
    

    现在,捕获的调用被执行了,但是carry on却没有,因为catch调用抛出了一个异常。需要注意的是这里没有错误任何记录,也不会打日志,不会有任何显示的异常抛出。如果你在最后添加了另外一个捕获,则捕获函数将会执行,因为当一个回调函数抛出异常时,调用链中的下个捕获才能够被调用。

    Promises链式传递

    这是来自我最近做过的一个添加CSV导出到我们客户端应用程序的功能。在那个例子里,使用了$q框架,这个框架里面包含了AngularJS的应用程序,但是我已经将其替换,以便于我可以用其作为一个例子:

    导出CSV的步骤如下(CSV文件是在浏览器使用FileSaver建立的):

    从构成CSV数据的API处获取数据(可能意味着多个API请求),将数据传递给一个object,在object中对数据做一些编辑,以便为填充到CSV中做好准备。

    填写数据到CSV中。给用户一个消息,确认他们的CSV已经被成功创建按,或者是有一个错误。

    我们不会去看代码工作的细节,但是我想在一定程度上了解我们是如何使用Promises建立一个健壮的解决方案。在这样的一个复杂的操作中,任意环节都可能产生错误(API功能可能会失效,代码解析数据时可能会抛出一个异常,或者CSV可能不会被正确地保存),同时我们发现通过promises,使用then和catch的组合,我们能够非常优雅地解决这个问题。

    正如你将看到的,我们将停止在promises的调用链中增加环节。在我看来,promises的链式写法的确让问题处理方式有了亮点,但是也需要我们去习惯这个写法,他们的工作方式在一开始看来会有点奇怪。
    Jake Archibald 将这个进行了优化:

    当你从“then”回调函数返回值时,是有点小技巧的。如果你返回了一个value,下一个附带返回值的then将会被调用。但是,如果你返回了一些值例如promise,下一个then则等候,同时只有在这个promise成功或者失败时,下一个then才会被调用。

    如果想深入了解promises,我还是必须强烈推荐这篇博客Jake Archibald's post on HTML5 Rocks

    让我们从一个简单的函数开始,这个函数只是返回一些数据。在一个真正的应用程序中,这里将会有一个http 调用。在我们的例子里,50ms后这个promise将会处理一个users数组,这个数组是我们想导出到CSV中的:

    var fetchData = function() {
      return new Promise(function(resolve, reject) {
        setTimeout(function() {
          resolve({
            users: [
              { name: 'Jack', age: 22 },
              { name: 'Tom', age: 21 },
              { name: 'Isaac', age: 21 },
              { name: 'Iain', age: 20 }
            ]
          });
        }, 50);
      });
    }
    

    接下来,这里有一个函数,为CSV准备数据。在这个例子里,这个函数做的就是立即解决给出的数据,但是在一个实际的应用程序中,它将做更多的工作:

    var prepareDataForCsv = function(data) {
      return new Promise(function(resolve, reject) {
        // imagine this did something with the data
        resolve(data);
      });
    };
    

    这里需要强调一点:在这个例子里(在一个实际的app中),prepareDataForCsv做的工作不是异步的。没有必要将其包含在一个promise中。但是当一个函数作为一个大的链式调用中的一部分中时,我发觉将其包含在一个promise中的确是很有好处的,这意味着所有的错误处理能够通过promises完成。另外的,如果你必须通过promises在某个地方进行错误处理,在另外一个地方则同通过try catch来进行处理。

    最终,我们有一个函数将数据写入csv中:

    var writeToCsv = function(data) {
      return new Promise(function(resolve, reject) {
        // write to CSV
        resolve();
      });
    };
    

    现在我们能够将他们放到一起了:

    fetchData().then(function(data) {
      return prepareDataForCsv(data);
    }).then(function(data) {
      return writeToCsv(data);
    }).then(function() {
      console.log('your csv has been saved');
    });
    

    这里非常简明,可读性也很好。能够很清晰地看到什么在运行,以及发生的事情的顺序。我们能够在以后进行整理。如果你有个函数,只是取一个参数,你可以直接传递给then,而不是通过一个回调函数来进行调用。

    fetchData().then(prepareDataForCsv).then(writeToCsv).then(function() {
      console.log('your csv has been saved');
    });
    

    始终要记得构成promises基础的代码很复杂(至少,在一个真正的应用程序中),高层API读起来很顺。一旦你习惯了使用他们,你能够写出一些比较优雅同时容易被理解的代码。

    到目前为止我们没有任何的错误处理函数,但是我们能够添加到代码的另一块区域。

    fetchData().then(prepareDataForCsv).then(writeToCsv).then(function() {
      console.log('your csv has been saved');
    }).catch(function(error) {
      console.log('something went wrong', error);
    });	
    

    正如之前所说的promises的链式以及错误处理方式,这意味着调用链末尾仅有的一个catch函数被当做可以捕捉任何错误抛出的处理函数。这使得错误处理更加直接。

    为了示范这一点,我将会改变prepareDataForCsv函数,让其在promise中reject:

    var prepareDataForCsv = function(data) {
      return new Promise(function(resolve, reject) {
        // imagine this did something with the data
        reject('data invalid');
      });
    };
    

    现在运行代码,会打印出错误。这的确很赞。prepareDataForCsv处于链式调用的中间位置,但是我们不需要做任何额外的工作来处理错误。另外的,catch不仅仅捕获我们通过promise reject触发的错误,同时也会捕获那些我们意想不到的异常。这意味着即使一个真正的预料之外的非主流程代码触发了js异常,用户还是有其希望的异常处理函数进行处理。

    我们发现的另外一种有效的方式是修改函数,通过以数据为入参,而不是返回一个将会解决数据的promise。我们修改prepareDataForCsv如下:

    var prepareDataForCsv = function(dataPromise) {
      return dataPromise().then(function(data) {
        return data;
      });
    };
    

    我们发觉这是一个非常友好的整理代码的模式,并且能够保持代码的通用性。在一个应用程序里大部分工作是异步的时候,传递promises而不是等待异步完成,再进行数据传递及解决会更加简单。

    通过以上的改变,新的代码看起来是这个样子的:

    prepareDataForCsv(fetchData).then(writeToCsv).then(function() {
      console.log('your csv has been saved');
    }).catch(function(error) {
      console.log('something went wrong', error);
    });
    

    这里的好处在于错误处理没有被改变。fetchData能够在一些表单中拒绝,错误将依然在最后的catch中调用。在你的头脑中想象一下,一旦点击,你将会发现,promises确实很不错,可能比错误处理更加的友好。

    Promises 中的递归

    我们需要处理的问题是当我们从API拉取数据时,可能需要多次请求。当你需要获取更多的数据而不是将所有数据放入一次响应中,对我们的API请求进行分页,你就能够发起多次请求。我们的API会告诉你是否会有更多数据需要拉取,在这个部分,我们将会解释怎么在promises的结合中使用递归,以及载入所有数据。

    var count = 0;
    
    var http = function() {
      if(count === 0) {
        count++;
        return Promise.resolve({ more: true, user: { name: 'jack', age: 22 } });
      } else {
        return Promise.resolve({ more: false, user: { name: 'isaac', age: 21 } });
      }
    };
    

    首先,我们有http,将会作为一个伪HTTP请求,请求我们的API。(Promises.resolve将会创建一个promise,立即处理你传递给他的任何值)。第一次我发起一个请求时,它将会返回附带一个user的响应但是同时有更多的标志位置为true,表明将会有更多的数据需要拉取(这不是实际API所返回的响应,但是将会出于文章的目的给出这个响应)。第二次请求时API将会返回一个user,但是更多的标志位是false。因此拉取所有需要的数据,我们需要发起两次API请求。fetchData如下:

    var fetchData = function() {
      var goFetch = function(users) {
        return http().then(function(data) {
          users.push(data.user);
          if(data.more) {
            return goFetch(users);
          } else {
            return users;
          }
        });
      }
    
      return goFetch([]);
    };
    

    fetchData完成了定义和调用其他goFetch的功能。goFetch获取users的一个数组,接着调用http(),对数据进行处理。http()返回的user将被push到users数组中,函数将会查看data.more
    字段。如果为true,则将会继续调用自身,传新的user。如果为false,则不再获取数据,它将会返回users数组。最重要的事情和工作的原理是它在每一阶段都返回了值。fetchData返回了一个goFetch,goFetch返回了自身或者是一个users数组。每一个它所返回的值都允许promise的链式递归调用的建立。

    总结

    promises将会成为处理大量异步操作的一个标准方法。但是我发现在处理复杂顺序操作的时候,一些是同步的,一些是异步的,promises也提供了很多的好处。如果你还没试过,我建议你在下一个项目中进行尝试。

  • 相关阅读:
    Maven
    Maven
    Maven
    Maven
    Maven
    Maven
    Maven
    Python
    Maven
    include和require的区别
  • 原文地址:https://www.cnblogs.com/luckyflower/p/4857019.html
Copyright © 2011-2022 走看看