承诺之美 —— 浅谈基于 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){
                        //...
                    });
                }, 1000);
            });             
        });    
    });
});

这就是著名的回调金字塔

承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法_第1张图片
金字塔

在我的理想中,这段代码应该是这样的:

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)这三种状态中的一种之中。

  1. 处于等待态时
  • 可以转移至执行态或拒绝态
  1. 处于兑现态时
  • 不能迁移至其他任何状态
  • 必须拥有一个不可变的值作为兑现结果
  1. 处于拒绝态时
  • 不能迁移至其他任何状态
  • 必须拥有一个不可变的值作为拒绝原因

通过 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);

如果 onFulfilledonRejected 的返回值 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();
    }, s); 
    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);
    };
    loop();
    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 —— 生成器。顾名思义,它用来生成某些东西。且上例子:

承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法_第2张图片
生成器基本使用

这里我们看到了 function*() 的新语法,还有 yield 关键字和 for/of 循环。新东西总是能让人产生振奋的心情,即使现在还不能将之投入使用(如果你需要,其实可以通过 ES6->ES5 的编译工具预处理你的 js 文件)。如果你了解 Python , 这很轻松就能理解。Generator 是一种特殊的 function,在括号前加一个 * 号以区别。Generator 通过 yield 操作产生返回值,最终生成了一个类似数组的东西,确切的说,它返回了 Iterator,即迭代器。迭代器可以通过 for/of 循环来进行遍历,也可以通过 next 方法不断迭代,直到迭代完毕。

承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法_第3张图片
生成器-next

yield 是一个神奇的功能,它类似于 return ,但是和 return 又不尽相同。return 只能在一个函数中出现一次,yield 却只能出现在生成器中且可以出现多次。迭代器的 next 方法被调用时,将触发生成器中的代码执行,执行到 yield 语句时,会将 yield 后的值带出到迭代器的 next 方法的返回值中,并保存好运行时环境,将代码挂起,直到下一次 next 方法被调用时继续往下执行。

有没有嗅到异步的味道?外部可以通过 next 方法控制内部代码的执行!天然的异步有木有!感受一下这个例子:

承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法_第4张图片
生成器-dead-loop

还有还有,yield 大法还有一个功能,它不仅可以带出值到 next 方法,还可以带入值到生成器内部 yield 的占位处,使得 Generator 内部和外部可以通过 next 方法进行数据通信!

承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法_第5张图片
生成器-interact

好了,生成器了解的差不多了,现在看看把 Promise 和 Generator 放一起会产生什么黑魔法吧!

承诺之美 —— 浅谈基于 Promise 的异步 Javascript编程方法_第6张图片
生成器-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 索性引入 asyncawait关键字。async 标记的函数支持 await 表达式。包含 await 表达式的的函数是一个deferred functionawait 表达式的值,是一个 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 的异步 Javascript编程方法)