JS Promise 对象的理解

为什么要用Promise

在实际项目中,有下面应用场景,在手机上浏览电影网站,其中有个海报墙页面,里面有海量的电影节目,如下图所示。考虑到性能和用户体验,启动后,我们需要串行的加载10页数据(每页9张海报),即第一页加载完成后,启动第二页的加载,以此类推。

于是不假思索的写下了下面的代码:

$(document).ready(function () {
    //获取第一页数据
    $.getJSON("json/poster.json?page=1", function (result) {
        attachPoster(result);
        //获取第二页数据
        $.getJSON("json/poster.json?page=2", function (result) {
            attachPoster(result);
            //获取第三页数据
            $.getJSON("json/poster.json?page=3", function (result) {
                attachPoster(result);
            ...
            });
        });
    });
});

一直写到自己恶心,这就是叫做"回调地狱"。

是否有解?有,那就是Promise

Promise是什么

Promise

Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。

同步和异步强调的是消息通信机制 (synchronous communication/ > > asynchronous communication)。所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。而异步则是相反,"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用

// 同步场景
function sleep(milliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + milliSeconds);
}


// 高中食堂打饭
function waitformeal(name,meal) {
    console.log(`${name}同学:我要${meal}`)
    sleep(10000);
    console.log(`食堂大妈:${meal}好了,同学`)
    console.log(`${name}同学开始干饭...`)
    console.log("....................")
}

waitformeal("赵","麻辣烫")
waitformeal("钱","黄焖鸡")
waitformeal("孙","盖浇饭")

//异步场景 麦当劳叫号模式
function waitformeal(name,meal) {
    console.log(`${name}同学:我要${meal}`)
    setTimeout(()=>{
        console.log(`麦当劳小姐姐:${meal}好了,同学`)
        console.log(`${name}同学开始干饭...`)
        console.log("....................")
    },10000)
    console.log(`${name}同学,干其他事情去了`)
}

waitformeal("赵","香辣鸡腿堡套餐")
waitformeal("钱","全家桶")
waitformeal("孙","两份全家桶")
console.log(`后厨准备中......`)

特点

  1. 对象的状态不受外界影响 (3种状态)
* Pending状态(进行中)
* Fulfilled状态(已成功)
* Rejected状态(已失败)
  1. 一旦状态改变就不会再变 (两种状态改变:成功或失败)
* Pending -> Fulfilled
* Pending -> Rejected

用法

创建Promise实例

__

var promise = new Promise(function(resolve, reject){
    // ... some code

    if (/* 异步操作成功 */) {
        resolve(value);
    } else {
        reject(error);
    }
})

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
resolve作用是将Promise对象状态由“未完成”变为“成功”,也就是Pending -> Fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而reject函数则是将Promise对象状态由“未完成”变为“失败”,也就是Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。

then

Promise实例生成后,可用then方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:

  1. Promise对象状态改为Resolved时调用 (必选)
  2. Promise对象状态改为Rejected时调用 (可选)
基本用法示例

__

function sleep(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, ms);
    })
}
sleep(500).then( ()=> console.log("finished"));

这段代码定义了一个函数sleep,调用后,等待了指定参数(500)毫秒后执行then中的函数。值得注意的是,Promise新建后就会立即执行。

执行顺序

接下来我们探究一下它的执行顺序,看以下代码:

__

let promise = new Promise(function(resolve, reject){
    console.log("AAA");
    resolve()
});
promise.then(() => console.log("BBB"));
console.log("CCC")

// AAA
// CCC
// BBB

执行后,我们发现输出顺序总是 AAA -> CCC -> BBB。表明,在Promise新建后会立即执行,所以首先输出 AAA。然后,then方法指定的回调函数将在当前脚本所有同步任务执行完后才会执行,所以BBB 最后输出

与定时器混用

首先看一个实例:

__

let promise = new Promise(function(resolve, reject){
    console.log("1");
    resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");

// 1
// 4
// 3
// 2

可以看到,结果输出顺序总是:1 -> 4 -> 3 -> 2。1与4的顺序不必再说,而2与3先输出Promise的then,而后输出定时器任务。原因则是Promise属于JavaScript引擎内部任务,而setTimeout则是浏览器API,而引擎内部任务优先级高于浏览器API任务,所以有此结果。

Promise优缺点

优点缺点
解决回调无法监测进行状态
链式调用新建立即执行且无法取消
减少嵌套内部错误无法抛出

解决上面遗留的问题

刚才麦当劳小姐姐的异步可以写成这样:

function haveMeal(name,meal) {
    return new Promise((resolve => {
        console.log(`${name}同学:我要${meal}`)
        setTimeout(()=>{
            resolve({name,meal})
        },10000)
        console.log(`${name}同学,干其他事情去了`)
    }))
}

function mealIsOk(data){
    let {name,meal} = data
    console.log(`麦当劳小姐姐:${meal}好了,同学`)
    console.log(`${name}同学开始干饭...`)
    console.log("....................")
}

haveMeal("赵","香辣鸡腿堡套餐").then(mealIsOk)
haveMeal("钱","全家桶").then(mealIsOk)
haveMeal("孙","两份全家桶").then(mealIsOk)
console.log(`后厨准备中......`)

上面电影网站的例子,改成下面的写法:

function getPoster(page){
    const promise = new Promise(function(resolve,reject){
        $.getJSON("json/poster.json?page="+page,function(result){
            resolve(result);
        })
    });
    return promise;
}
getPoster(1).then(function(result){//获取第一页
    attachPoster(result);
    return getPoster(2);
}).then(function(result){//获取二页
    attachPoster(result);
    return getPoster(3);
}).then(funciton(result){//获取第三页 ...})

从代码结构上看,比第一种的层层嵌套是不是更清晰,更符合逻辑。Promise就是为了解决回调函数嵌套的一种解决方案。

再来看一下Promise

then与resolve

通过下面例子再次了解Promise是怎么玩的。

function getPObj(){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器");
            resolve("执行回调方法");
        },2000);
    });
    return p;
}
getPObj();

在getPObj中我们new了Promise对象p并返回该对象,在构造p对象的方法中,只有一个定时器,2s钟后打印一个日志和执行resolve入参方法。

结果:
开始执行定时器

只是执行了日志,并没有看到resolve方法的执行,这也不奇怪,因为resolve作为构造函数的入参,我们根本就没有定义。

我们将代码改成下面

function getPObj(){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器");
            resolve("执行回调方法");
        },2000);
    });
    return p;
}
getPObj().then(function(data){
    console.log("我是回调方法");
    console.log(data);
});

再次此时的执行结果:

结果:
开始执行定时器
我是回调方法
执行回调方法

then的入参函数,就是resovle的回调方法。看到这里,大家可能会问,这不就是个callback作为入参的回调么,只不过用了then的属性方法传入的,一种表示方式而已,有啥稀奇的。如果只是一层嵌套是看不出优越性,还记得我们前面海报加载的场景么,如果嵌套多层,then的链式调用就发挥巨大优先性了,它能把层层嵌套平铺开来。

我们将上面的实例再改造下:

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            resolve(num);
        },2000);
    });
    return p;
}
getPObj(1).then(function(data){
    console.log("我是回调方法");
    console.log("执行回调方法:"+data);
    return getPObj(2);
}).then(function(data){
    console.log("我是回调方法");
    console.log("执行回调方法:"+data);
    return getPObj(3);
}).then(function(data){
    console.log("我是回调方法");
    console.log("执行回调方法:"+data);
});

在每个回调执行完成后,再返回一个新的Promise对象,继续下一次操作。

开始执行定时器:1
我是回调方法
执行回调方法1
开始执行定时器:2
我是回调方法
执行回调方法2
开始执行定时器:3
我是回调方法
执行回调方法3

回过头来看我们开篇讲到海报加载的例子,到此可以理解了。

reject

细心的同学可能发现,在Promise对象的构造方法的入参中,还有个reject方法我们还没有讲到。

const promise = new Promise(function(resolve,reject){
    somethingDO();
    if (/*结果符合预期,异步操作成功*/) {
        resolve()
    }else/*不符合预期,操作失败*/
    {
        reject();
    }
})

Pomise有三种状态,分别是pending(进行中),resolved(已成功),rejected(已失败),一旦达到相应的状态,就会回调相应的方法。其实称作已成功,或者已失败并不准确,ES6中标准说法fullfiled,rejected。至于什么是已成功状态,什么是已失败状态,可以自己按照实际情况自定义。

对应的,then方法有两个入参,分别实现resolved,rejected的回调方法。

promise.then(function(value) {
  // resolved
}, function(error) {
  // rejected
});

继续上面的实例,我们在方法增加控制,生成一个1-10的随机方法,如果大于5就表示失败。

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            var i = Math.ceil(Math.random()*10); //生成1-10的随机数
            if (i<5) {
                resolve(num);
            }else{
                reject(num);
            }

        },2000);
    });
    return p;
}

getPObj(1).then(function(data){
    console.log("执行回调方法:"+data);
    return getPObj(2);
},function(data){
    console.log("执行回调方法失败:"+data);
}).then(function(data){
    console.log("执行回调方法:"+data);
    return getPObj(3);
},function(data){
    console.log("执行回调方法失败:"+data);
}).then(function(data){
    console.log("执行回调方法:"+data);
},function(data){
    console.log("执行回调方法失败:"+data);
});

执行的结果:

开始执行定时器:1
执行回调方法失败:1
执行回调方法:undefined
开始执行定时器:3
执行回调方法失败:3

第一次执行时,i的随机值就大于5,所以执行了rejected的方法。但是和我们预期的还是有点不一样,如果返回失败,我们希望终止掉整个链条,但是从实际结果看,是继续往下执行。这是因为,回调第一个reject的方法后,没有返回值,Promise会自动返回一个undefined,传入下一个链条的resolve方法中,并继续后面的then链。

有没有方法,一旦执行失败,就中断后面的then链条呢?有,各位继续往下。

catch

try…catch我们常用捕获异常的方法,在promise对象中也有catch的方法。用来捕获then回调方法中抛出的各类异常,用法如下:

p.then(function(){
         ...
}).catch(e){
          ....
}

现在我们用上面的实例构造一个异常。

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            var i = Math.ceil(Math.random()*10); //生成1-10的随机数
            if (i<5) {
                resolve(num);
            }else{
                reject(num);
            }
        },2000);
    });
    return p;
}
getPObj(1).then(function(data){
    console.log("执行回调方法:"+data);
    //x没有定义,抛出异常
    x+2;
}).catch(function(e){
    console.log(e);
})

执行的结果如下:

开始执行定时器:1
执行回调方法:1
ReferenceError:x is not defined

捕获并打印了异常。

我们来解决前面提的问题,利用catch可终止then链条。如下

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            var i = Math.ceil(Math.random()*10); //生成1-10的随机数
            if (i<5) {
                resolve(num);
            }else{
                reject(num);
            }

        },2000);
    });
    return p;
}

getPObj(1).then(function(data){
    console.log("执行回调方法:"+data);
    return getPObj(2);
}).then(function(data){
    console.log("执行回调方法:"+data);
    return getPObj(3);
}).then(function(data){
    console.log("执行回调方法:"+data);
}).catch(function(e){
    console.log(e);
})

reject构造一个error的入参,抛出异常,为catch捕获。从执行结果看,后面的then的没有执行,达到目的。

同学们看到这个实例中,每个then的reject的方法都删除了,catch方法实际就是实现了全局的reject方法。
在实际开发中,我们建议采用catch代替reject

finally

try…catch…finally是黄金组合,做过java开发的道友们对肯定非常熟悉。finally表示无论什么状态,必定都会执行。

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            var i = Math.ceil(Math.random()*10); //生成1-10的随机数
            if (i<5) {
                resolve(num);
            }else{
                reject(new Error("出错了"));
            }
        },2000);
    });
    return p;
}

getPObj(1).then(function(data){
    console.log("执行回调方法:"+data);
    return getPObj(2);
}).then(function(data){
    console.log("执行回调方法:"+data);
    return getPObj(3);
}).then(function(data){
    console.log("执行回调方法:"+data);
}).catch(function(e){
    console.log(e);
}).finally(function(){
    console.log("finally");
});

all

Promise.all可以将几个Promise对象封装成一个,格式如下:

Promise.all([p1,p2,p3]).then(function(data){…})

当这几个对象都变成resolved状态后,总状态变为resolved;否则,其中有一个为rejected状态,则变成reject,其他的可以忽略。可以理解为p1&&p2&&p3。

那返回的data是什么样子,如果是resolved状态,则是各个对象data的组合;如果是rejected,则是第一个到达rejected状态返回的data值。以例为证。

都为resolved状态:

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            resolve(num);
        },2000);
    });
    return p;
}

Promise.all([getPObj(1),getPObj(2),getPObj(3)]).then(function(data){
    console.log("resolve");
    console.log(data);
}).catch(function(e){
    console.log("error");
    console.log(e);
})

其中有一个返回rejected状态:

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            var i = Math.ceil(Math.random()*10); //生成1-10的随机数
            if (i<5) {
                resolve(num);
            }else{
                reject(num);
            }
        },2000);
    });
    return p;
}

Promise.all([getPObj(1),getPObj(2),getPObj(3)]).then(function(data){
    console.log("resolve");
    console.log(data);
}).catch(function(e){
    console.log("error");
    console.log(e);
})

race

race与all类似,页可以将几个Promise对象封装成一个,格式如下:

Promise.race([p1,p2,p3]).then(function(data){...})

不同的时,看谁执行的快,then就回到回调谁的结果。可以理解为p1||p2||p3

看实例:

function getPObj(num){
    var p = new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log("开始执行定时器:"+num);
            var i = Math.ceil(Math.random()*10); //生成1-10的随机数
            if (i<5) {
                resolve(num);
            }else{
                reject(num);
            }
        },2000);
    });
    return p;
}

Promise.race([getPObj(1),getPObj(2),getPObj(3)]).then(function(data){
    console.log("resolve");
    console.log(data);
}).catch(function(e){
    console.log("error");
    console.log(e);
})

当接受到第一个对象的resolved状态后,其他的两个抛弃处理。

总结

本章主要阐述了Promise的基本知识,在实际项目,有很多已经分装好的库可以使用,如jquery,微信小程序等,万变不离其踪,只要了解了基本的原理,这些库使用起来也会得心用手。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值