拥抱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也提供了很多的好处。如果你还没试过,我建议你在下一个项目中进行尝试。

posted @ 2015-10-06 13:20  testForever  阅读(188)  评论(0编辑  收藏  举报