Promise--优雅解决回调嵌套

最近一直在用空余时间研究node,当我写了一个简单的复制一个文件夹中的文件到另外一个位置的时候,我看到了所谓的回调地狱,虽然只是四五个回调嵌套,可是这已经让我感到惧怕,我写这么简单的一个小demo就写成这样,那稍微复杂点儿还了得?记得在看ES6的时候,里面提到过一种新的解决回调的方式---Promise,并且在node中也经常使用这个解决大量嵌套,因此这几天花了点儿时间看了看Promise,让我对Promise的认识更加清晰,因此写一些东西总结一下。node

Promise状态的理解

new Promise实例化的Promise对象有三个状态:es6

  • “has-resolution” - Fulfilled数组

    • reslove(成功时),调用onFulfilledpromise

  • "has-rejection" - Rejected异步

    • reject(失败时)。调用Rejectedasync

  • "unresolve" - Pending函数

    • 既不是resolve也不是reject状态,也就是Promise刚刚被建立后的初始化状态。this

图片描述

note:

  1. 在Chrome中输出resolve能够获得Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined},能够看出[[PromiseStatus]]中存储的就是Promise的状态,可是并无公开访问[[PromiseStatus]]的用户API,因此暂时还没法查询其内部状态。spa

  2. Promise中的then的回调只会被调用一次,由于Promise的状态只会从Pending变为Fulfilled或者Rejected,不可逆。设计

Promise的使用

在使用Promise实现有序执行异步的基本格式以下:

//defined Promise async function
function asyncFun(){
    return new Promise((reslove,reject)=>{
        if(reslove){
            reslove(/*reslove parameter*/);
        }else{
            reject(new Error(/*Error*/));
        }
    })
}

//use Promise&then
asyncFun().then(/*function*/).then(/*function*/)...

reslove方法的参数就是要传给回调函数的参数,即resolve将运行获得的结果传出来,而then接受该参数给回调继续执行后面的,若是这个then的中的函数还会返回Promise,则会重复执行该步骤直到结束。

reject方法的参数通常是包含了reject缘由的Error对象。rejectresolve同样,也会将本身的参数传出去,接收该参数的是then的第二个fun或者是catch。其实.catch只是Promise.then(onFulfilled,onRejected)的别名而已。

快捷建立Promise

通常状况下咱们会使用new Promise来建立prmise对象,除此以外咱们也可使用Promise.reslovePromise.reject来直接建立,例如Promise.resolve(42)能够认为是如下代码的语法糖

new Promise((reslove)=>{
    reslove(42);
});

这段代码可让这个Promise对象当即进入resolve状态,并将42传递给后面then里所指定的onFulfilled函数。此外Promise.resolve还有一个做用,就是将非Promise对象转换为Promise对象。

Promise.reject(value)与之相似。

Promise.then()的异步调用带来的思考

var promise = new Promise(function (resolve){
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then(function(value){
    console.log(value); // 3
});
console.log("outer promise"); // 2

/*输出:
"inner promise"
"outer promise"
42
*/

从以上的这段代码咱们能够看出Promise.then()是异步调用的,这也是Promise设计上规定的,其缘由在于同步调用和异步调用同时存在会致使混乱

以上那段代码若是在调用onReady以前DOM已经载入的话,对回调函数进行同步调用,若是在调用onReady以前DOM尚未载入的话,经过注册DOMContentLoader事件监听器来对回调进行异步调用。这会致使该代码在源文件中不一样位置输出不一样的结果,关于这个现象,有以下几点:

  • 绝对不能对异步函数(即便在数据已经就绪)进行同步调用

  • 若是对异步回调函数进行同步调用,处理顺序可能会与预期不符,带来意外的结果

  • 对异步回调函数进行同步调用,还可能致使栈溢出或者异常处理错乱等问题

  • 若是想在未来的某个时刻调用异步回调,可使用setTimeout等异步API

因此以上代码应该使用 setTimeout(fn, 0)进行调用。

function onReady(fn) {
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
        setTimeout(fn, 0);
    } else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

因此在Promise中then是异步的。

Promise链式调用

各个Task相互独立

若是想实现Promise的链式调用,要求每次链式调用都返回Promise。因此每一个异步执行都须要使用Promise包装,这里有一个误区:每一个thencatch会返回也会反回一个新的Promise,可是这仅仅实现了链式调用,若是不将异步操做用Promise进行包装,依然不行。下面的例子就是错误的

function pro1(){
    return new Promise((reslove,reject)=>{
        if(reslove){
            setTimeout(()=>{console.log(1000)},1000);
            reslove();
        }
    })
}

function pro2(){
    setTimeout(()=>{console.log(2000)},2000);
}

function pro3(){
    setTimeout(()=>{console.log(3000)},3000);
}

pro1().then(pro2).then(pro3);
//or
function pro1(){
    setTimeout(()=>{console.log(1000)},1000);
}

Promise.resolve().then(pro1).then(pro2).then(pro3);

上面的写法有两处错误:

  1. 虽然在第一个函数返回了一个Promise,可是因为后面的异步操做并无被Promise包装,因此并不会起任何做用,正确的作法是每个异步操做都要被Promise包装

  2. resolve()调用的时机不对,resolve须要在异步操做执行完成后调用,因此须要写在异步操做内部,若是像上面那样写在异步操做外面,则不会起做用。

因此正确写法以下:

//直接返回Promise
function pro1(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(1000);resolve();},1000);
        
    })
}
function pro2(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(5000);resolve();},5000);
        
    });
}
function pro3(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(500);resolve();},500);
    })
}
pro1().then(pro2).then(pro3);

//or使用Promise.reslove()

function pro1(cb){setTimeout(()=>{console.log(1000);cb()},1000)};
function pro2(cb){setTimeout(()=>{console.log(3000);cb()},3000)};
function pro3(cb){setTimeout(()=>{console.log(500);cb()},500)};


Promise.resolve()
       .then(()=>new Promise(resolve=>pro1(resolve)))
       .then(()=>new Promise(resolve=>pro2(resolve)))
       .then(()=>new Promise(resolve=>pro3(resolve)));

各个Task须要参数的传递

在Promise的链式调用中,有可能各个task之间存在相互依赖,例如TaskA想给TaskB传递一个参数,像下面这样:

/*例1.使用Promise.resolve()启动*/
let task1 = (value1)=>value1+1;
let task2 = (value2)=>value2+2;
let task3 = (value3)=>{console.log(value3+3)};

Promise.resolve(1).then(task1).then(task2).then(task3);//console => 7


/*例2.普通的返回一个Promise*/
function task1(value1){
  return new Promise((resolve,reject)=>{
    if(resolve){
      resolve(value1+1);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}

function task2(value2){
  return new Promise((resolve,reject)=>{
    if(resolve){
      resolve(value2+2);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}
function task3(value3){
  return new Promise((resolve,reject)=>{
    if(resolve){
      console.log(value3+3);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}

task1(1).then(task2).then(task3);//console => 7

关于reslovereject有如下两点说明:

  • reslove函数的做用是将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操做成功时调用,并将异步操做的结果做为参数传递出去

  • reject函数的做用是将Promise对象状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操做失败时候调用,并将异步操做报出的错误做为参数传递出去

因此从上面的例子和它们的用法能够看出,若是想要传递给后面task有两种方法:

  • 若是使用Promise.resolve()启动Promise,则像例1中那样在须要传递的参数前面加return便可。

  • 若是是利用Promise包装了任务,则把想要传递给下一个task的参数传入resolve()便可。

特别说明:若是须要resolve()日后传递多个参数,不能直接写resolve(a1,a2,a3),这样只能拿到第一个要传的参数,须要以数组或对象去传递

let obj = {a1:a1,a2:a2,a3:a3};
resolve(obj)
//or
let arr =[a1,a2,a3];
resolve(arr);

thencatch返回新的Promise

在Promise中不管是then仍是catch方法,都会返回返回一个新的Promise对象。

图片描述

var aPromise = new Promise(function (resolve) {
    resolve(100);
});
var thenPromise = aPromise.then(function (value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
    console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true

因此像下面这样将链式调用分开写是不成功的

// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
    resolve(100);
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    console.log("1: " + value); // => 100
});

因为每次调用then方法都会返回一个新的Promise,因此致使最终输出100而不是100 2 2。

Promise.all()的使用

有时候须要多个彼此没有关联的多个异步任务所有执行完成后再执行后面的操做,这时候就须要用到Promise.all(),它接收一个Promise的对象的数组做为参数,当这个数组里的全部Promise对象所有变成resolve或者reject的时候,它才会去调用后面的.then()

这里须要说明一点,两个彼此无关的异步操做会同时执行,每一个Promise的结果(即每一个返回的Promise的resolve或reject时传递的参数)和传递给Promise.all的Promise数组的顺序一致。也就是说,假设有两个异步操做TaskA和TaskB,若是传入顺序为Promise.all([TaskA,TaskB]),则执行完成后传给.then的顺序为[TaskA,TaskB]。

function setTime(time){
  return new Promise((resolve)=>{
    setTimeout(()=>resolve(time),time);
  })
}

let startTime = Date.now();

Promise.all([setTime(1),setTime(100),setTime(200)])
       .then((value)=>{
         console.log(value);    //[1,100,200]
         console.log(Date.now() - startTime); //203
       });

从上面函数的输出值能够看出Promise.all()里的异步操做是同时执行的并且传给.then()的顺序和Promise.all()里的顺序同样。最终执行时间约为200ms,为何不是200ms,这里涉及到关于setTimeout的精准问题,不在这里讨论。

Promise.race()的使用

Promise.rance()的用法与Promise.all()相似,不一样的地方在于Promise.all()是在接收到的全部Promise都变为FulFilled或者Rejected状态以后才会继续进行后面的处理,而Promise.rance()只要有一个Promise对象进入FullFilled或者Rejected状态,就会继续进行后续处理。这至关于Promise.all()进行运算而Promise.rance()进行运算。可是这里有一点须要注意一下:

var taskA = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is taskA');
            resolve('this is taskA');
        }, 4);
    });
var taskB = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is taskB');
            resolve('this is taskB');
        }, 1000);
    });

Promise.race([winnerPromise, loserPromise]).then(function (value) {
    console.log(value);
});

/*
输出结果:
this is taskA
this is taskA
this is taskB
*/

从这里能够看出,在第一个Promise变为FulFiled状态运行then里的回调后,后面的Promise并无中止运行,而是接续执行。也就是说, Promise.race 在第一个promise对象变为Fulfilled以后,并不会取消其余promise对象的执行。

Promise的reject和异步操做error的理解

function ReadEveryFiles(file){
    return new Promise((resolve,reject)=>{
        if(resolve){
            fs.readFile(`${__dirname}/jQuery/${file}`,(err,data)=>{
                if(err){
                    console.log(err);
                }else{
                    let obj = {data:data,file:file};
                    resolve(obj);
                }
            });
        }else{
            //promise reject error
        }
    });
}

这里的readFile的error和Promise的reject不同,一个是readFile过程当中致使的错误,而另外一个是Promise作处理的时候致使的错误,能够这样理解,假设读取文件成功了,可是Promise还须要讲这个异步操做获得的数据拿处处理,在Promise作这些操做的时候可能出错。

写在最后

这几天开始用Promise写了一些东西,发现其实若是用Promise,会使得代码量加大,由于每个异步都要被Promise封装,可是这样换来的倒是更加容易的维护,因此仍是值得的,当代码写完后,咱们很容易就能看出代码的执行过程,相对于原来用嵌套去写要直观许多,而若是想要解决Promise的代码量过大的问题,咱们可使用Generator函数,另外,在ES7标准中推出了更加牛的异步解决方案Async/Await,关于它们,我将会在随后继续深刻。

参考

JavaScript Promise迷你书(中文版)
ECMAScript 6 入门---Promise对象

相关文章
相关标签/搜索