怎么用promise异步编程模式实现异步控制

热门推荐:
  Promise 是异步编程的一种解决方案。简单来说 Promise 就是一个容器,里面保存着某个未来才会结束的异步操作的结果。
  从语法上说,Promise 是一个对象,通过它可以获取异步操作的消息。
  在谈论 Promise 之前我们先看一段代码:
  在日常开发中,我们经常会处理一些异步操作,例如:发送 Ajax 请求,setTimeout 等。处理这些异步操作我们一般会传递一个回调函数,然而当我们要进行一连串的异步顺序操作的时候,回调代码就会自动往右边进行偏离,就像金字塔那样。相信你已经遇到过,这就是我们常常说的“金字塔回调”。
  首先,从视觉看,金字塔回调会有多层的嵌套回调函数,结尾会有大量的花括号和圆括号,例如上述代码。并且,这样写出来的代码不优雅,阅读起来也比较费力,我们也无法在内部使用 throw new Error() 并在外部进行捕获异常。
  Promise 的出现就是为了主要解决这两个主要问题:它可以让我们以同步的方式编写异步代码,同时我们也可以优雅的捕获错误和异常。
  Promise 最初起源于社区,现在已经被写入了 ES6 的规范中,并且主流浏览器都已经开始支持了,所以本文就介绍在代码中如何使用 Promise。
  1、 创建Promise实例
  Promise 是一个构造函数,使用时我们需要先使用 new 创建一个 Promise 实例。
  在上面代码中我们可以看到:
  1)构造函数接受一个函数作为参数,该函数有两个参数resolve和reject, 它们是两个函数。
  2)resolve 函数与 reject 函数的作用:
  resolve函数:将Promise对象的状态从 “未完成”变为 “成功”(Pending -& Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
  reject 函数:将Promise对象的状态从 “未完成” 变为 “失败”(Pending -& Rejected),在异步操作失败时调用,并将异步操作报出的错误作为参数传递出去。
  3)Promise 实例生成以后,可以用then方法 指定 Resolved 状态和 Rejected 状态的回调函数。
  其中 resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例,表示异步操作的结果可能是一个值,也有可能是另一个异步操作。
  这时 Promise 实例通过 then 指定的回调会等待另一个 Promise 实例状态发生变化后才会进行调用。
  上述结果是:promise2 等待 2s 之后输出了 promise 中的值。
  这时 promise 的状态会传递给 promise2, promise 的状态决定了 promise2 的状态。
  2、 Promise实例的状态变化
  Promise对象有三种状态
  ?Pending (进行中)
  ?Resolved (已完成)
  ?Rejected (已失败)
  Promise的状态变化有上图两条路径:
  resolve 方法会使 Promise 对象由 Pending 状态变为 Resolved 状态reject 方法或异常会使得 Promise 对象由 pending 状态变为 Rejected 状态
  对象状态一旦改变,任何时候都能得到这个结果。即状态一旦进入 Resolved 状态或者 Rejected 状态, Promise 对象便不再出现状态变化,同时我们再添加回调会立即得到结果。这点跟事件不一样,事件是发生后再绑定监听,就监听不到了。
  3、 then方法与错误处理
  Promise 实例生成以后,可以用 then 为实例添加状态改变时的回调函数。
  getData 函数返回一个 promise 实例,使用 then 为它指定一个 Resolved 状态的回调函数, 异步请求中传给 resolve 的值,将作为回调函数中的参数。当异步请求成功之后,回调函数变会执行输出对应的值。
  假设异步请求失败了怎么办? then 其实还可以指定第二个可选的参数,即Rejected 状态的回调函数。
  在上述例子中,异步请求成功后,第一个回调函数会执行,如果失败了,第二个回调函数便会执行。
  其实我们还可以使用 catch 指定错误时的回调,catch 调用其实等同于使用then(undefined, function) 。
  链式调用
  我们已经知道了,使用 then 可以为 promise 实例添加状态改变的回调函数。其实,我们还可以使用 then 进行链式调用来指定一组按照顺序执行的回调函数,因为 then 的调用总是会返回 promise 的一个新的实例。
  其中后一个 promise 实例会依赖上一个 promise 实例的状态,如果上一个promise 实例状态是 Rejected,则后面的 promise 实例状态也是 Rejected。如果前一个回调函数返回的是一个 promise 对象(有异步操作),这时后一个回调函数会等待该 promise 对象状态发生变化时,才会进行调用。
  promise 对象的错误具有冒泡性质,会一直向后传递,直到捕获为止。错误总是会被下一个 catch 语句捕获。
  一般来说,不要在 then 方法里面定义 Reject 状态的回调函数,总是使用 catch捕获错误。
  Promise与循环
  前面提到了 Promise 可以指定一组异步操作顺序执行,那如果我们需要等待一组异步操作之后结束之后再执行呢?Promise 提供了一个很方便的方法Promise.all。
  Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
  Promise.all 方法接受一个 promise 实例数组作为参数(可以不是数组,但需要具有 iterator 接口), 如果元素不是 Promise 实例,就会先调用Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。
  Promise.all 方法返回的 Promise 实例状态分为两种情况:
  实例数组中所有实例的状态都变成 Resolved,Promise.all 返回的实例才会变成 Resolved, 并将 Promise 实例数组的所有返回值组成一个数组,传递给回调函数。
  实例数组中某个实例变为了 Rejected 状态,Promise.all 返回的实例会立即变为 Rejected 状态。并将第一个 Rejected 的实例的返回值传递给回调函数。
  Promise.race 方法跟 Promise.all 方法差不多。唯一的区别在于该方法返回的Promise 实例并不会等待所有 Proimse 都跑完,而是只要有一个 Promise 实例改变状态,它就跟着改变状态。并使用第一个改变状态实例的返回值作为返回值。
  4、 promise 兼容性
  Promise 已经被写入 ES6 规范中。由下图可以看出,各大浏览器(除IE外)都已开始支持原生 Promise 的使用。但是在低版本浏览器和运行环境中,并不支持 Promise 对象。
  要在这些环境中使用 Promise,则需要借助一些兼容 Promise 的类库。ES6 中的 Promise 规范来源于 Promises/A+ 社区,因此,在选择类库时应该考虑对Promise/A+ 兼容性。
  Promise 的 Polyfill 类库有很多,笔者经常使用的有(供参考):
  es6-promisebluebird
  参考文献:/
  本文作者:张宁(点融黑帮),就职于点融网工程部 social team 创新社。热爱前端,热爱技术。
  本文由@点融黑帮(ID:DianrongMafia)原创发布于搜狐科技,未经许可,禁止转载。
请先登录再操作
请先登录再操作
微信扫一扫分享至朋友圈
点融黑帮――一个充满激情和梦想的技术团队,吸引了来自金融及...
知名IT评论人,曾就职于多家知名IT企业,现是科幻星系创建人
未来在这里发声。
新媒体的实践者、研究者和批判者。
立足终端领域,静观科技变化。深入思考,简单陈述。
智能硬件领域第一自媒体。承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法 - 简书
承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法
每一位前端工程师上辈子都是折翼的天使。
相信很多前端工程师都同我一样,初次接触到前端时,了解了些许 HTML、CSS、JS 知识,便惊叹于前端的美好,沉醉于这种所见即所得的成就感之中。但很快我就发现,前端并没有想象中的那么美好,JS 也并不是弹一个 alert 这么简单。尤其是当我想这么干,却发现无法得到结果时:
var data = ajax('/url/to/data');
在查阅很多资料后,我知道了 JS 是事件驱动的,ajax 异步请求是非阻塞的,我封装的 ajax 函数无法直接返回服务器数据,除非声明为同步请求(显然这不是我想要的)。于是我学会了或者说接受了这样的事实,并改造了我的 ajax 函数:
ajax('/url/to/data', function(data){
//deal with data
在很长一段时间,我并没有认为这样的代码是不优雅的,甚至认为这就是 JS 区别于其他语言的特征之一 —— 随处可见的匿名函数,随处可见的 calllback 参数。直到有一天,我发现代码里出现了这样的结构:
ajax('/get/data/1', function(data1){
ajax('/get/data/2', function(data2){
ajax('/get/data/3', function(data3){
dealData(data1, data2, data3, function(result){
setTimeout(function(){
ajax('/post/data', result.data, function(ret){
这就是著名的回调金字塔!
在我的理想中,这段代码应该是这样的:
var data1 = ajax('/get/data/1');
var data2 = ajax('/get/data/2');
var data3 = ajax('/get/data/3');
var result = dealData(data1, data2, data3);
sleep(1000);
var ret = ajax('/post/data', result.data);
承诺的救赎
理想是丰满的,奈何现实太骨干。这种回调之痛在前端人心中是挥之不去的,它使得代码结构混乱,可读性变差,维护困难。在忍受这种一坨坨的代码很久之后,有一天我偶遇了 Promise,她的优雅让我久久为之赞叹:世间竟有如此曼妙的异步回调解决方案。
Promises/A+规范中对 promise 的解释是这样的: promise 表示一个异步操作的最终结果。与 promise 进行交互的主要方式是通过 then 方法,该方法注册了两个回调函数,用于接受 promise 的最终结果或者 promise 的拒绝原因。一个 Promise 必须处于等待态(Pending)、兑现态(Fulfilled)和拒绝态(Rejected)这三种状态中的一种之中。
处于等待态时
可以转移至执行态或拒绝态
处于兑现态时
不能迁移至其他任何状态
必须拥有一个不可变的值作为兑现结果
处于拒绝态时
不能迁移至其他任何状态
必须拥有一个不可变的值作为拒绝原因
通过 resolve 可以将承诺转化为兑现态,通过 reject 可以将承诺转换为拒绝态。
关于 then 方法,它接受两个参数:
promise.then(onFulfilled, onRejected)
then 方法可以被同一个 promise 调用多次:
当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
使用 Promise 后,我的 ajax 函数使用起来变成了这个样子:
ajax('/url/to/data')
.then(function(data){
//deal with data
看起来和普通的回调没什么变化是么?让我们继续研究 then 方法的神奇之处吧。
then 方法的返回值是一个新的 promise:
promise2 = promise1.then(onFulfilled, onRejected);
如果 onFulfilled、onRejected 的返回值 x 是一个 promise,promise2 会根据 x 的状态来决定如何处理自己的状态。
如果 x 处于等待态, promise2 需保持为等待态直至 x 被兑现或拒绝
如果 x 处于兑现态,用相同的值兑现 promise2
如果 x 处于拒绝态,用相同的值拒绝 promise2
这意味着串联异步流程的实现会变得非常简单。我试着用 Promise 来改写所有的异步接口,上面的金字塔代码便成为这样的:
when( ajax('/get/data/1'), ajax('/get/data/2'), ajax('/get/data/3') )
.then(dealData)
.then(sleep.bind(null,1000))
.then(function(result){
return ajax('/post/data', result.data);
.then(function(ret){
一下子被惊艳到了啊!回调嵌套被拉平了,小肚腩不见了!这种链式 then 方法的形式,颇有几分 stream/pipe 的意味。
$.Deferred
jQuery 中很早就有 Promise 的实现,它称之为 Deferred 对象。使用 jQuery 举例写一个 sleep 函数:
function sleep(s){
var d = $.Deferred();
setTimeout(function(){
d.resolve();
return d.promise(); //返回 promise 对象防止在外部被别人 resolve
我们来使用一下:
sleep(1000)
.then(function(){
console.log('1秒过去了');
.then(sleep.bind(null,3000))
.then(function(){
console.log('4秒过去了');
jQuery 实现规范的 API 之外,还实现了一对接口:notify/progress。这对接口在某些场合下,简直太有用了,例如倒计时功能。对上述 sleep 函数改造一下,我们写一个 countDown 函数:
function countDown(second) {
var d = $.Deferred();
var loop = function(){
if(second &= 0) {
return d.resolve();
d.notify(second--);
setTimeout(loop, 1000);
return d.promise();
现在我们来使用这个函数,感受一下 Promise 带来的美好。比如,实现一个 60 秒后重新获取验证码的功能:
var btn = $("#getSMSCodeBtn");
btn.addClass("disabled");
countDown(60)
.progress(function(s){
btn.val(s+'秒后可重新获取');
.then(function(){
btn.val('重新获取验证码').removeClass('disabled');
简直惊艳!离绝对的同步编写非阻塞形式的代码已经很近了!
与 ES6 Generator 碰撞出火花
我深刻感受到,前端技术发展是这样一种状况: 当我们惊叹于最新技术标准的美好,感觉一个最好的时代即将到来时,回到实际生产环境,却发现一张小小的 png24 透明图片在 IE6 下还需要前端进行特殊处理。但,那又怎样,IE6 也不能阻挡我们对前端技术灼热追求的脚步,说不定哪天那些不支持新标准的浏览器就悄然消失了呢?(扯远了...)
ES6 标准中最令我惊叹的是 Generator —— 生成器。顾名思义,它用来生成某些东西。且上例子:
生成器基本使用
这里我们看到了 function*() 的新语法,还有 yield 关键字和 for/of 循环。新东西总是能让人产生振奋的心情,即使现在还不能将之投入使用(如果你需要,其实可以通过 ES6-&ES5 的编译工具预处理你的 js 文件)。如果你了解 Python , 这很轻松就能理解。Generator 是一种特殊的 function,在括号前加一个 * 号以区别。Generator 通过 yield 操作产生返回值,最终生成了一个类似数组的东西,确切的说,它返回了 Iterator,即迭代器。迭代器可以通过 for/of 循环来进行遍历,也可以通过 next 方法不断迭代,直到迭代完毕。
生成器-next
yield 是一个神奇的功能,它类似于 return ,但是和 return 又不尽相同。return 只能在一个函数中出现一次,yield 却只能出现在生成器中且可以出现多次。迭代器的 next 方法被调用时,将触发生成器中的代码执行,执行到 yield 语句时,会将 yield 后的值带出到迭代器的 next 方法的返回值中,并保存好运行时环境,将代码挂起,直到下一次 next 方法被调用时继续往下执行。
有没有嗅到异步的味道?外部可以通过 next 方法控制内部代码的执行!天然的异步有木有!感受一下这个例子:
生成器-dead-loop
还有还有,yield 大法还有一个功能,它不仅可以带出值到 next 方法,还可以带入值到生成器内部 yield 的占位处,使得 Generator 内部和外部可以通过 next 方法进行数据通信!
生成器-interact
好了,生成器了解的差不多了,现在看看把 Promise 和 Generator 放一起会产生什么黑魔法吧!
生成器-Promise
这里写一个 delayGet 函数用来模拟费时操作,延迟 1 秒返回某个值。在此借助一个 run 方法,就实现了同步编写非阻塞的逻辑!这就是 TJ 大神 co 框架的基本思想。
回首一下我们曾经的理想,那段代码用 co 框架编写可以是这样的:
co(function*(){
var data1 = yield ajax('/get/data/1');
var data2 = yield ajax('/get/data/2');
var data3 = yield ajax('/get/data/3');
var result = yield dealData(data1, data2, data3);
yield sleep(1000);
var ret = yield ajax('/post/data', result.data);
Perfect!完美!
ES7 async-await
ES3 时代我们用闭包来模拟 private 成员,ES5 便加入了 defineProperty 。Generator 最初的本意是用来生成迭代序列的,毕竟不是为异步而生的。ES7 索性引入 async、await关键字。async 标记的函数支持 await 表达式。包含 await 表达式的的函数是一个deferred function 。await 表达式的值,是一个 awaited object。当该表达式的值被评估(evaluate) 之后,函数的执行就被暂停(suspend)。只有当 deffered 对象执行了回调(callback 或者 errback)后,函数才会继续。
也就是说,只需将使用 co 框架的代码中的 yield 换掉即可:
async function task(){
var data1 = await ajax('/get/data/1');
var data2 = await ajax('/get/data/2');
var data3 = await ajax('/get/data/3');
var result = await dealData(data1, data2, data3);
await sleep(1000);
var ret = await ajax('/post/data', result.data);
至此,本文的全部内容都已完毕。前端标准不断在完善,未来会越来越美好。永远相信美好的事情即将发生!

我要回帖

更多关于 promise 异步转同步 的文章

 

随机推荐