JavaScript Promise启示录


Posted in Javascript onAugust 12, 2014

本篇,主要普及promise的用法。

一直以来,JavaScript处理异步都是以callback的方式,在前端开发领域callback机制几乎深入人心。在设计API的时候,不管是浏览器厂商还是SDK开发商亦或是各种类库的作者,基本上都已经遵循着callback的套路。

近几年随着JavaScript开发模式的逐渐成熟,CommonJS规范顺势而生,其中就包括提出了Promise规范,Promise完全改变了js异步编程的写法,让异步编程变得十分的易于理解。

在callback的模型里边,我们假设需要执行一个异步队列,代码看起来可能像这样:

loadImg('a.jpg', function() {
  loadImg('b.jpg', function() {
    loadImg('c.jpg', function() {
      console.log('all done!');
    });
  });
});

这也就是我们常说的回调金字塔,当异步的任务很多的时候,维护大量的callback将是一场灾难。当今Node.js大热,好像很多团队都要用它来做点东西以沾沾“洋气”,曾经跟一个运维的同学聊天,他们也是打算使用Node.js做一些事情,可是一想到js的层层回调就望而却步。

好,扯淡完毕,下面进入正题。

Promise可能大家都不陌生,因为Promise规范已经出来好一段时间了,同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类Promise类库相比少些API。

所谓Promise,字面上可以理解为“承诺”,就是说A调用B,B返回一个“承诺”给A,然后A就可以在写计划的时候这么写:当B返回结果给我的时候,A执行方案S1,反之如果B因为什么原因没有给到A想要的结果,那么A执行应急方案S2,这样一来,所有的潜在风险都在A的可控范围之内了。

上面这句话,翻译成代码类似:

var resB = B();
var runA = function() {
  resB.then(execS1, execS2);
};
runA();

只看上面这行代码,好像看不出什么特别之处。但现实情况可能比这个复杂许多,A要完成一件事,可能要依赖不止B一个人的响应,可能需要同时向多个人询问,当收到所有的应答之后再执行下一步的方案。最终翻译成代码可能像这样:

var resB = B();
var resC = C();
...

var runA = function() {
  reqB
    .then(resC, execS2)
    .then(resD, execS3)
    .then(resE, execS4)
    ...
    .then(execS1);
};

runA();

在这里,当每一个被询问者做出不符合预期的应答时都用了不同的处理机制。事实上,Promise规范没有要求这样做,你甚至可以不做任何的处理(即不传入then的第二个参数)或者统一处理。

好了,下面我们来认识下Promise/A+规范:

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
  • promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
  • then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。

可以看到,Promise规范的内容并不算多,大家可以试着自己实现以下Promise。

以下是笔者自己在参考许多类Promise库之后简单实现的一个Promise,代码请移步promiseA。

简单分析下思路:

构造函数Promise接受一个函数resolver,可以理解为传入一个异步任务,resolver接受两个参数,一个是成功时的回调,一个是失败时的回调,这两参数和通过then传入的参数是对等的。

其次是then的实现,由于Promise要求then必须返回一个promise,所以在then调用的时候会新生成一个promise,挂在当前promise的_next上,同一个promise多次调用都只会返回之前生成的_next

由于then方法接受的两个参数都是可选的,而且类型也没限制,可以是函数,也可以是一个具体的值,还可以是另一个promise。下面是then的具体实现:

Promise.prototype.then = function(resolve, reject) {
  var next = this._next || (this._next = Promise());
  var status = this.status;
  var x;

  if('pending' === status) {
    isFn(resolve) && this._resolves.push(resolve);
    isFn(reject) && this._rejects.push(reject);
    return next;
  }

  if('resolved' === status) {
    if(!isFn(resolve)) {
      next.resolve(resolve);
    } else {
      try {
        x = resolve(this.value);
        resolveX(next, x);
      } catch(e) {
        this.reject(e);
      }
    }
    return next;
  }

  if('rejected' === status) {
    if(!isFn(reject)) {
      next.reject(reject);
    } else {
      try {
        x = reject(this.reason);
        resolveX(next, x);
      } catch(e) {
        this.reject(e);
      }
    }
    return next;
  }
};

 

这里,then做了简化,其他promise类库的实现比这个要复杂得多,同时功能也更多,比如还有第三个参数——notify,表示promise当前的进度,这在设计文件上传等时很有用。对then的各种参数的处理是最复杂的部分,有兴趣的同学可以参看其他类Promise库的实现。

在then的基础上,应该还需要至少两个方法,分别是完成promise的状态从pending到resolved或rejected的转换,同时执行相应的回调队列,即resolve()reject()方法。

到此,一个简单的promise就设计完成了,下面简单实现下两个promise化的函数:

function sleep(ms) {
  return function(v) {
    var p = Promise();

    setTimeout(function() {
      p.resolve(v);
    }, ms);

    return p;
  };
};

function getImg(url) {
  var p = Promise();
  var img = new Image();

  img.onload = function() {
    p.resolve(this);
  };

  img.onerror = function(err) {
    p.reject(err);
  };

  img.url = url;

  return p;
};

由于Promise构造函数接受一个异步任务作为参数,所以getImg还可以这样调用:

function getImg(url) {
  return Promise(function(resolve, reject) {
    var img = new Image();

    img.onload = function() {
      resolve(this);
    };

    img.onerror = function(err) {
      reject(err);
    };

    img.url = url;
  });
};

接下来(见证奇迹的时刻),假设有一个BT的需求要这么实现:异步获取一个json配置,解析json数据拿到里边的图片,然后按顺序队列加载图片,没张图片加载时给个loading效果

function addImg(img) {
  $('#list').find('> li:last-child').html('').append(img);
};

function prepend() {
  $('<li>')
    .html('loading...')
    .appendTo($('#list'));
};

function run() {
  $('#done').hide();
  getData('map.json')
    .then(function(data) {
      $('h4').html(data.name);

      return data.list.reduce(function(promise, item) {
        return promise
          .then(prepend)
          .then(sleep(1000))
          .then(function() {
            return getImg(item.url);
          })
          .then(addImg);
      }, Promise.resolve());
    })
    .then(sleep(300))
    .then(function() {
      $('#done').show();
    });
};

$('#run').on('click', run);

这里的sleep只是为了看效果加的,可猛击查看demo!当然,Node.js的例子可查看这里。

在这里,Promise.resolve(v)静态方法只是简单返回一个以v为肯定结果的promise,v可不传入,也可以是一个函数或者是一个包含then方法的对象或函数(即thenable)。

类似的静态方法还有Promise.cast(promise),生成一个以promise为肯定结果的promise;

Promise.reject(reason),生成一个以reason为否定结果的promise。

我们实际的使用场景可能很复杂,往往需要多个异步的任务穿插执行,并行或者串行同在。这时候,可以对Promise进行各种扩展,比如实现Promise.all(),接受promises队列并等待他们完成再继续,再比如Promise.any(),promises队列中有任何一个处于完成态时即触发下一步操作。

标准的Promise

可参考html5rocks的这篇文章JavaScript Promises,目前高级浏览器如chrome、firefox都已经内置了Promise对象,提供更多的操作接口,比如Promise.all(),支持传入一个promises数组,当所有promises都完成时执行then,还有就是更加友好强大的异常捕获,应对日常的异步编程,应该足够了。

第三方库的Promise

现今流行的各大js库,几乎都不同程度的实现了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出来的大都是Deferred对象,以jQuery(Zepto类似)为例,实现上面的getImg()

function getImg(url) {
  var def = $.Deferred();
  var img = new Image();

  img.onload = function() {
    def.resolve(this);
  };

  img.onerror = function(err) {
    def.reject(err);
  };

  img.src = url;

  return def.promise();
};

当然,jQuery中,很多的操作都返回的是Deferred或promise,如animateajax

// animate
$('.box')
  .animate({'opacity': 0}, 1000)
  .promise()
  .then(function() {
    console.log('done');
  });

// ajax
$.ajax(options).then(success, fail);
$.ajax(options).done(success).fail(fail);

// ajax queue
$.when($.ajax(options1), $.ajax(options2))
  .then(function() {
    console.log('all done.');
  }, function() {
    console.error('There something wrong.');
  });

jQuery还实现了done()fail()方法,其实都是then方法的shortcut。

处理promises队列,jQuery实现的是$.when()方法,用法和Promise.all()类似。

其他类库,这里值得一提的是when.js,本身代码不多,完整实现Promise,同时支持browser和Node.js,而且提供更加丰富的API,是个不错的选择。这里限于篇幅,不再展开。

尾声

我们看到,不管Promise实现怎么复杂,但是它的用法却很简单,组织的代码很清晰,从此不用再受callback的折磨了。

最后,Promise是如此的优雅!但Promise也只是解决了回调的深层嵌套的问题,真正简化JavaScript异步编程的还是Generator,在Node.js端,建议考虑Generator。

下一篇,研究下Generator。

github原文: https://github.com/chemdemo/chemdemo.github.io/issues/6

Javascript 相关文章推荐
javascript中万恶的function实例分析
May 25 Javascript
分享2个jQuery插件--jquery.fileupload与artdialog
Dec 26 Javascript
jquery实现仿新浪微博评论滚动效果
Aug 06 Javascript
jQuery实现类似标签风格的导航菜单效果代码
Aug 25 Javascript
JS数组合并push与concat区别分析
Dec 17 Javascript
JavaScript编写检测用户所使用的浏览器的代码示例
May 05 Javascript
用jQuery的AJax实现异步访问、异步加载
Nov 02 Javascript
移动开发之自适应手机屏幕宽度
Nov 23 Javascript
JS+HTML5 FileReader对象用法示例
Apr 07 Javascript
vue 标签属性数据绑定和拼接的实现方法
May 17 Javascript
node将geojson转shp返回给前端的实现方法
May 29 Javascript
electron实现静默打印的示例代码
Aug 12 Javascript
深入理解Javascript中this的作用域
Aug 12 #Javascript
javascript实现在某个元素上阻止鼠标右键事件的方法和实例
Aug 12 #Javascript
JavaScript弹出窗口方法汇总
Aug 12 #Javascript
Javascript中3种实现继承的方法和代码实例
Aug 12 #Javascript
jQuery判断checkbox是否选中的3种方法
Aug 12 #Javascript
jquery判断浏览器后退时候弹出消息的方法
Aug 11 #Javascript
jQuery根据ID获取input、checkbox、radio、select的示例
Aug 11 #Javascript
You might like
使用php清除bom示例
2014/03/03 PHP
php微信公众开发之获取周边酒店信息的方法
2014/12/22 PHP
浅谈php和js中json的编码和解码
2016/10/24 PHP
jquery的$(document).ready()和onload的加载顺序
2010/05/26 Javascript
jQuery实现类似滑动门切换效果的层切换
2013/09/23 Javascript
JS实现控制表格内指定单元格内容对齐的方法
2015/03/30 Javascript
javascript原始值和对象引用实例分析
2015/04/25 Javascript
javascript函数特点实例分析
2015/05/14 Javascript
jQuery插件制作之全局函数用法实例
2015/06/01 Javascript
基于jQuery的ajax方法封装
2016/07/14 Javascript
Bootstrap基本组件学习笔记之下拉菜单(7)
2016/12/07 Javascript
jQuery时间验证和转换为标准格式的时间格式
2017/03/06 Javascript
解决JQuery全选/反选第二次失效的问题
2017/10/11 jQuery
微信小程序实现鼠标拖动效果示例
2017/12/01 Javascript
VueRouter导航守卫用法详解
2017/12/25 Javascript
关于Vue的路由权限管理的示例代码
2018/03/06 Javascript
webpack4+react多页面架构的实现
2018/10/25 Javascript
Vue中用props给data赋初始值遇到的问题解决
2018/11/27 Javascript
vue 中Virtual Dom被创建的方法
2019/04/15 Javascript
如何进行微信公众号开发的本地调试的方法
2019/06/16 Javascript
Vue图片浏览组件v-viewer用法分析【支持旋转、缩放、翻转等操作】
2019/11/04 Javascript
JavaScript canvas实现雪花随机动态飘落
2020/02/08 Javascript
[46:47]2014 DOTA2国际邀请赛中国区预选赛 DT VS HGT
2014/05/22 DOTA
基于Python的关键字监控及告警
2017/07/06 Python
python滑块验证码的破解实现
2019/11/10 Python
python绘制规则网络图形实例
2019/12/09 Python
Web页面中八种创建多列等高(等高列布局)的实现技术
2012/12/24 HTML / CSS
美国汽车交易网站:Edmunds
2016/08/17 全球购物
米兰必去买手店排行榜首位:Antonioli
2016/09/11 全球购物
加拿大国民体育购物网站:National Sports
2018/11/04 全球购物
美国正宗设计师眼镜在线零售商:EYEZZ
2019/03/23 全球购物
婚纱摄影师求职信
2014/03/07 职场文书
环保倡议书50字
2014/05/15 职场文书
2014年单位法制宣传日活动总结
2014/11/01 职场文书
2015年中秋节活动总结
2015/03/23 职场文书
开场白怎么写
2015/06/01 职场文书