JavaScript 进阶之路 -- 认识和使用 Promise, 重构你的 JS 代码
一转眼, 这 2015 年上半年就过去了, 差不多一个月没有写博客了,"罪过罪过" 啊. 进入了七月份, 也就意味着我们上半年苦逼的单身生活结束了, 从此刻起, 我们要打起十二分的精神, 开始下半年的单身生活. 大家一起加油
一直以来, JavaScript 处理异步都是以 callback 的方式, 在前端开发领域 callback 机制几乎深入人心. 在设计 API 的时候, 不管是浏览器厂商还是 SDK 开发商亦或是各种类库的作者, 基本上都已经遵循着 callback 的套路. 近几年随着 JavaScript 开发模式的逐渐成熟, CommonJS 规范顺势而生, 其中就包括提出了 Promise 规范, Promise 完全改变了 JS 异步编程的写法, 让异步编程变得十分的易于理解. 今天我们就来了解一下 Promise~~
一 , 初识 Promise
1, 什么是 promise?
Promise 可能大家都不陌生, 因为 Promise 规范已经出来好一段时间了, 同时 Promise 也已经纳入了 ES6, 而且高版本的 Chrome,Firefox 浏览器都已经原生实现了 Promise, 只不过和现如今流行的类 Promise 类库相比少些 API.
所谓 Promise, 字面上可以理解为 "承诺", 就是说 A 调用 B,B 返回一个 "承诺" 给 A, 然后 A 就可以在写计划的时候这么写: 当 B 返回结果给我的时候, A 执行方案 S1, 反之如果 B 因为什么原因没有给到 A 想要的结果, 那么 A 执行应急方案 S2, 这样一来, 所有的潜在风险都在 A 的可控范围之内了.
Promise 规范如下:
一个 promise 可能有三种状态: 等待 (pending), 已完成 (fulfilled), 已拒绝 (rejected)
一个 promise 的状态只可能从 "等待" 转到 "完成" 态或者 "拒绝" 态, 不能逆向转换, 同时 "完成" 态和 "拒绝" 态不能相互转换
promise 必须实现 then 方法 (可以说, then 就是 promise 的核心), 而且 then 必须返回一个 promise, 同一个 promise 的 then 可以调用多次, 并且回调的执行顺序跟它们被定义时的顺序一致
then 方法接受两个参数, 第一个参数是成功时的回调, 在 promise 由 "等待" 态转换到 "完成" 态时调用, 另一个是失败时的回调, 在 promise 由 "等待" 态转换到 "拒绝" 态时调用. 同时, then 可以接受另一个 promise 传入, 也接受一个 "类 then" 的对象或方法, 即 thenable 对象.
2.promise 原理分析
可以看到 promise 的规范并不是很多, 下面我们一边分析 promise 一边自己写一个 promise 的实现. Promise 实现的大致思路如下:
构造函数 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);
- });
- 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!
在这里, 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 队列中有任何一个处于完成态时即触发下一步操作.
3. 标准的 Promise
可参考 html5rocks 的这篇文章 JavaScript Promises, 目前高级浏览器如 Chrome,Firefox 都已经内置了 Promise 对象, 提供更多的操作接口, 比如 Promise.all(), 支持传入一个 promises 数组, 当所有 promises 都完成时执行 then, 还有就是更加友好强大的异常捕获, 应对日常的异步编程, 应该足够了.
现今流行的各大 JS 库, 几乎都不同程度的实现了 Promise, 如 dojo,jQuery,Zepto,when.JS,Q 等, 只是暴露出来的大都是 Deferred 对象, 当然还有 AngularJS 中的 $q. 这里以 jQuery 为例, 说一下:
- // 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.');
- });
二 , 用 Promise 组织你的 JavaScript 代码
上面我们了解了 Promise, 相信大家对 Promise 有了一定的认识. 下面我们开始动手来写代码, 通过几个简单的例子, 来加深理解. 这里我们使用浏览器自带的 Promise, 首先我们要先检测一些浏览器是否支持 Promise, 其实很简单, 如果是谷歌浏览器, 按下 F12, 打开控制台, 如图:
这里我们可以看到 Promise 的 type 是 function, 也就是说谷歌浏览器是支持 promise 的. 以此为原理, 我们可以写一段 JavaScript 代码来检测, 代码如下:
- if(typeof(Promise) === "function"){
- alert("支持 Promise");
- }
- else{
- alert("不支持 Promise");
- }
经过检测, 发现 IE11 竟然不支持 promise. 建议大家用谷歌浏览器来进行测试吧.
我们首先来写一个等待的方法, 如下:
- function wait(duration){
- return new Promise(function(resolve, reject) {
- setTimeout(resolve,duration);
- })
- }
测试这个方法的代码如下: wait(5000).then(function(){alert('hello')}), 这段代码很简单, 就是等待 5 秒以后执行一个回调, 弹出一个消息. 当然, 你还可以这样写:
wait(5000).then(function(){alert('hello')}).then(function(){console.log('world')})
怎么样? 很简单吧~~
下面来看一些我从网上收集的一些常用的 JavaScript 的 promise 的写法:
- function get(uri){
- return http(uri, 'GET', null);
- }
- function post(uri,data){
- if(typeof data === 'object' && !(data instanceof String || (FormData && data instanceof FormData))) {
- var params = [];
- for(var p in data) {
- if(data[p] instanceof Array) {
- for(var i = 0; i < data[p].length; i++) {
- params.push(encodeURIComponent(p) + '[]=' + encodeURIComponent(data[p][i]));
- }
- } else {
- params.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
- }
- }
- data = params.join('&');
- }
- return http(uri, 'POST', data || null, {
- "Content-type":"application/x-www-form-urlencoded"
- });
- }
- function http(uri,method,data,headers){
- return new Promise(function(resolve, reject) {
- var xhr = new XMLHttpRequest();
- xhr.open(method,uri,true);
- if(headers) {
- for(var p in headers) {
- xhr.setRequestHeader(p, headers[p]);
- }
- }
- xhr.addEventListener('readystatechange',function(e){
- if(xhr.readyState === 4) {
- if(String(xhr.status).match(/^2\d\d$/)) {
- resolve(xhr.responseText);
- } else {
- reject(xhr);
- }
- }
- });
- xhr.send(data);
- })
- }
- function wait(duration){
- return new Promise(function(resolve, reject) {
- setTimeout(resolve,duration);
- })
- }
- function waitFor(element,event,useCapture){
- return new Promise(function(resolve, reject) {
- element.addEventListener(event,function listener(event){
- resolve(event)
- this.removeEventListener(event, listener, useCapture);
- },useCapture)
- })
- }
- function loadImage(src) {
- return new Promise(function(resolve, reject) {
- var image = new Image;
- image.addEventListener('load',function listener() {
- resolve(image);
- this.removeEventListener('load', listener, useCapture);
- });
- image.src = src;
- image.addEventListener('error',reject);
- })
- }
- function runScript(src) {
- return new Promise(function(resolve, reject) {
- var script = document.createElement('script');
- script.src = src;
- script.addEventListener('load',resolve);
- script.addEventListener('error',reject);
- (document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script);
- })
- }
- function domReady() {
- return new Promise(function(resolve, reject) {
- if(document.readyState === 'complete') {
- resolve();
- } else {
- document.addEventListener('DOMContentLoaded',resolve);
- }
- })
- }
看到了吧, Promise 风格 API 跟回调风格的 API 不同, 它的参数跟同步的 API 是一致的, 但是它的返回值是个 Promise 对象, 要想得到真正的结果, 需要在 then 的回调里面拿到.
三, 用 Promise 组织 JavaScript 异步代码
在比较复杂的页面中, 我们会使用到大量的异步操作. 我们来看看使用 Promise 会带来怎样的便利吧~~
1, 多个异步调用, 同步 / 并行
例如我们页面调用了好几个异步函数, 我们要等待所有的异步函数执行完成后, 做一些操作, 如弹出一个消息框提示用户操作成功. 下面我们拿一个例子来说明一下:
Promise.all 跟 then 的配合, 可以视为调用部分参数为 Promise 提供的函数. 譬如, 我们现在有一个接受三个参数的函数:
- function print(a, b, c) {
- console.log(a + b + c);
- }
现在我们调用 print 函数, 其中 a 和 b 是需要异步获取的:
- var c = 10;
- print(geta(), getb(), 10); // 这是同步的写法
- Promise.all([geta(), getb(), 10]).then(print); // 这是 primise 的异步写法
如果用 callback 的话, 我们就只能一个一个调用了, 调用完了 geta, 然后在其回调函数里面调用 getb, 最后在 getb 的回调函数中调用 print 方法. 串行和并行哪个更快, 大家很清楚吧~~
2. 竞争
如果说 Primise.all 是 promise 对象之间的 "与" 关系, 那么 Promise.race 就是 promise 对象之间的 "或" 关系. 比如, 我要实现 "点击按钮或者 5 秒钟之后执行":
- var btn = document.getElementsByTagName('button');
- Promise.race(wait(5000), waitFor(btn, click)).then(function(){
- console.log('run!')
- })
3. 异常处理
异常处理一直是回调的难题, 而 promise 提供了非常方便的 catch 方法: 在一次 promise 调用中, 任何的环节发生 reject, 都可以在最终的 catch 中捕获到:
- Promise.resolve().then(function(){
- return loadImage(img1);
- }).then(function(){
- return loadImage(img2);
- }).then(function(){
- return loadImage(img3);
- }).catch(function(err){
- // 错误处理
- })
4. 复杂流程
接下来, 我们来看比较复杂的情况.
promise 有一种非常重要的特性: then 的参数, 理论上应该是一个 promise 函数, 而如果你传递的是普通函数, 那么默认会把它当做已经 resolve 了的 promise 函数.
这样的特性让我们非常容易把 promise 风格的函数跟已有代码结合起来.
为了方便传参数, 我们编写一个 currying 函数, 这是函数式编程里面的基本特性, 在这里跟 promise 非常搭, 所以就实现一下:
- function currying(){
- var f = arguments[0];
- var args = Array.prototype.slice.call(arguments,1);
- return function(){
- args.push.apply(args,arguments);
- return f.apply(this,args);
- }
- }
currying 会给某个函数 "固化" 几个参数, 并且返回接受剩余参数的函数. 比如之前的函数, 可以这么玩:
- var print2 = currying(print,11);
- print2(2, 3); // 得到 11 + 2 + 3 的结果, 16
- var wait1s = currying(wait,1000);
- wait1s().then(function(){
- console.log('after 1s!');
- })
有了 currying, 我们就可以愉快地来玩链式调用了, 比如以下代码:
- Promise.race([
- domReady().then(currying(wait,5000)),
- waitFor(btn, click)])
- .then(currying(runScript,'a.js'))
- .then(function(){
- console.log('loaded');
- return Promise.resolve();
- });
四 , 总结
我们看到, 不管 Promise 实现怎么复杂, 但是它的用法却很简单, 组织的代码很清晰, 从此不用再受 callback 的折磨了. promise 作为一个新的 API, 它的 API 本身没有什么特别的功能, 但是它背后代表的编程思路是很有价值的.
最后, Promise 是如此的优雅! 但 Promise 也只是解决了回调的深层嵌套的问题, 真正简化 JavaScript 异步编程的还是 Generator, 在 Node.JS 端, 建议考虑 Generator.
五 , 参考资料
JavaScript Promise 迷你书 (中文版) http://liubin.github.io/promises-book/
JavaScript Promise 启示录
用 Promise 组织程序 http://www.w3ctech.com/topic/721
转载自 http://www.cnblogs.com/yunfeifei/p/4453690.html
来源: http://www.qdfuns.com/article/45527/44d3c77f9895ee4668c010540af7339f.html