ES6 Promise 实践 Tips Pt.2 (升级现有代码)

前言

在上一篇 (ES6 Promise 实践 Tips Pt.1) 中, 我们总结了一些在使用ES6 Promise模型进行编程时的注意事项,但多面向基于promise实例方法来编写异步流程的场景。现在我们回过头来关注如何改造现有需要传入回调函数来实现异步执行的功能代码,使其返回promise对象实例。

现状

由于JavaScript中没有线程模型,为了在执行需要耗时较久的任务(比如网络请求和动画)时不阻塞后续逻辑或失去对用户操作的反馈,我们一般的做法是指定一个回调函数,让需要长时间执行的任务在状态发生变化时调用以便获取任务执行结果(比如XMLHttpRequest对象实例上的onreadystatechange方法 ,或正在执行动画的DOM元素上的transitionend事件触发函数)。

例如:

function massiveTask(requirement , successCB, errorCB){
     console.log("begin working on :" + requirement);
     setTimeout(function(){
        try{             
        var result = requirement + "done";
        successCB && successCB(result);
        } catch (err){
        errorCB && errorCB(err);                    
        }
      },3000)
}

// massiveTask 调用见下文

如果我们现在需要把上面的代码改造为支持 ES6 Promise ,我们该怎么做呢?

在Promise对象创建时处理异步逻辑

在大多数介绍 ES6 Promise 的文章中或例子中,都会把业务逻辑封装为Promise构造函数所需要的参数, 例如下面这样

function massiveTask(requirement){
    var p = new Promise(function(resolve,reject){
        console.log("begin working on :" + requirement);
        setTimeout(function(){
          try{        
            var result = requirement + "  done";
            resolve(result);
           }catch (err){
            reject( err);           
           }
        },3000);
    })
    return p;
}

这里特别需要注意的是, Promise 构造函数所需要的参数的类型是一个有两个参数的函数 , 这两个参数又分别是用来传递结果和用来报告错误的的函数.

这种方式生成Promise对象最为直接,不过如果想更完备一些,我们需要考虑

考虑以上几点后的代码会进化成这样:


function massiveTask(requirement ,successCB, errorCB){
    var DummyPromise = function( func ){
        func();
    }
    var P = window.Promise || DummyPromise;
    
    var p = new P(function(resolve,reject){
        console.log("begin working on :" + requirement);
        setTimeout(function(){
        try{        
            var result = requirement + "  done";
            successCB && successCB(result);            
            resolve && resolve(result);
        } catch (err){
            errorCB && errorCB(err);            
            reject && reject(err);
          }
        },3000)
        
    })
    
    return  window.Promise ? p ; null ;
}

在进行了如上改造后, 我们就既能以传统 callback 函数方式调用

massiveTask("earn 100 million",
            function(res){
                console.log("i need report");
                console.log(result) ;
                console.log("good job");                
            },
            function(error){
                console.log("WTF?  you are been fired ");                
            }
           );

也能用 promise 方式调用


var workResult = massiveTask("earn 100 million");

workResult.then(function(result){
    console.log("i need report");
    console.log(result) ;
    console.log("good job");                
}).catch(function(error){
    console.log("WTF?  you are been fired ");
})

如果是新写的代码或需要改造的逻辑比较简单,可以采用上面这种直观,完备的改造方法。 但如果需要改造的代码较为复杂,上面的方法就会暴露改造范围较大且引入多一层嵌套的问题,使得代码Review和后续维护都不易进行. 这种情况下,我们就要考虑用不同的方法来解决问题.

在异步业务开始前创建Promise对象

认真观察上面经过改造过支持Promise的代码,我们能发现要支持Promise实际上最关键的只需要在支持Promise的环境中 (1.)创建并返回Promise对象. (2.)使用resolve和reject来传递结果或报告错误. 我们能不能以某种形式让这部分代码和我们待改造的代码分离呢? 比如如下这样:

function massiveTask(requirement , successCB, errorCB){
    var promiseData = {promise: null,resolver: null,rejecter: null};
    if (!successCB && window.Promise){
      promiseData.promise = new Promise(function (resolve, reject) {
        promiseData.resolver = resolve;
        promiseData.rejecter = reject;
      });
    }
    
    console.log("begin working on: " + requirement);
    setTimeout(function(){
        try{
            var result = requirement + "  done";
            promiseData.promise && promiseData.resolver(result);        
            successCB && successCB(result);
        }catch (err){
            promiseData.promise && promiseData.rejecter( err);        
            errorCB && errorCB(err);            
        }
    },3000)
    
    if ( promiseData.promise ){
        return promiseData.promise;
    }
}

虽然代码的行数稍微有些增长,但我们做到了最大程度的Promise支持与原有业务代码的分离. 在正常业务代码部分仅需在原本需要回调函数执行的位置增加一行利用提前存储起来的resolver来传递执行结果的代码即可。

总结

作为ES6引入新特性中被当前浏览器支持得较好,同时又能被较好的在老旧浏览器中被polyfill补全的重要特性, Promise已经获得了越来越广泛的使用, 如果你想给你手头的老旧代码带来新的活力,不妨现在就开始为她增加ES6 Promise支持。