Ray1993/MyBlog

ES6 - 对Promise对象的学习理解

Opened this issue · 0 comments

概述

这篇主要是对Promise对象的学习以及Promise的应用

Promise是由es6提供的一个原生对象,在Promise之前,在js中的异步编程都是采用回调函数和事件的方式,但是这种编程方式在处理复杂业务的情况下,容易出现callback hell(回调地狱),使得代码难以理解和维护,Promise就是改善这种情形的异步编程的解决方案。好了,下面我们就一起来学习一下promise吧。

先来看看Promise是啥

我们可以通过Chrome控制台输入console.dir(Promise) ,将Promise打印出来,简单粗暴~

image

不难发现,Promise是一个构造函数,自己身上有all,resolve,reject 这几个方法,原型上有then,catch等方法,所以说Promise new 出来的对象一定有then、catch等方法。

resolve和reject的用法

下面我们先来new一个Promise对象看看:

var p = new Promise((resolve, reject) => {
    //做一些异步操作
    setTimeout(() => {
        console.log('执行完成');
        resolve('随便什么数据');
    }, 2000);
});

Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为完成态(Fulfilled),reject是将Promise的状态置为拒绝态(Rejected),当还没有调用resolve和reject方法的时候,Promise的状态为等待态(Pending)。

在上面的代码中,我们执行了一个异步操作,也就是setTimeout,2秒后,输出“执行完成”,并且调用resolve方法。

运行代码,会在2秒后输出“执行完成”。注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:

function runAsync(){
    var p = new Promise((resolve, reject) => {
        //做一些异步操作
        setTimeout(() => {
            console.log('执行完成');
            resolve('随便什么数据');
        }, 2000);
    });
    return p;
}
runAsync()

Promise 的三种状态

  • 等待态(Pending): 处于等待态时,可以迁移至完成态或拒绝态。
  • 完成态(Fulfilled): 处于完成态时,不能迁移至任何状态,并且拥有一个不可变的终值。
  • 拒绝态(Rejected): 处于拒绝态时,不能迁移至任何状态,并且拥有一个不可变的拒因。

注意,这种状态的改变只会出现从等待态向完成态或拒绝态转化,不能逆反。完成态和拒绝态不能互相转化,而且,状态一旦转化,将不能更改。
image

看到这里,你也许会有两个疑问:
1.上面代码包装那么一个runAsync( )函数有啥用?
2. 函数里面的 resolve(‘随便什么数据’)是干嘛的?

下面我们继续来讲,在我们调用runAsync( )函数后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。还记得Promise对象上有then、catch方法吧?这就是强大之处了,看下面的代码:

runAsync().then((data) => {
    console.log(data);  //随便什么数据
    //后面可以用传过来的数据做些其他操作
    //......
});

在runAsync()的返回上直接调用then方法,then方法一般接受两个参数,下面我们会细说,这里的then只接收一个参数,是函数,并且会拿到我们在runAsync中调用 resolve(‘随便什么数据’)时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。

then和catch的用法

这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。

链式操作的用法(例:promise.then().then().then())

从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(() => {
            console.log('异步任务1执行完成');
            resolve('随便什么数据1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(() => {
            console.log('异步任务2执行完成');
            resolve('随便什么数据2');
        }, 2000);
    });
    return p;            
}

//链式写法
runAsync1()                     // 异步任务1执行完成
.then((data) = > {
    console.log(data);       //随便什么数据1
    return runAsync2();    // 异步任务2执行完成
})
.then((data) = > {
    console.log(data);      //随便什么数据2
    return ‘直接返回数据’;
})
.then((data) = > {
    console.log(data);     //直接返回数据
});

then方法的参数

promise 的 then 方法接受两个参数:promise.then(onFulfilled, onRejected) 参数可选
onFulfilled 和 onRejected 都是可选参数。

  • 如果 onFulfilled 不是函数,其必须被忽略(被视为null),如果onFulfilled 是函数,其第一个参数是Promise方法resolve传过来的值(也就是Promise的终值)
  • 如果 onRejected 不是函数,其必须被忽略(被视为null),如果onRejected 是函数,其第一个参数是Promise方法reject传过来的值(也就是Promise的拒因)
    不过只有当调用了promise的resolve或者reject方法才会对应去执行then里面的onFulfilled 和 onRejected函数,否则then里面的两个函数参数是不会被执行的

注:如果我们只想传onRejected而不想传onFulfilled,可以这么写:pormise.then(null, onRejected)

catch方法

Promise对象除了有then方法还有一个catch方法,其实它和then的第二个参数一样,用来指定reject的回调,用法如下:

function getNumber(){
    var p = new Promise((resolve, reject) => {
        //做一些异步操作
        setTimeout(() => {
            var num = Math.ceil(Math.random()*10); //生成1-10的随机数
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
        }, 2000);
    });
    return p;            
}

getNumber().then((data) => {
    console.log('resolved');
    console.log(data);    //生成1-10的随机数
}).catch((reason) => {
    console.log('rejected');
    console.log(reason);   //数字太大了
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中

all和race的用法

all的用法

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调,all 可以接收一个由 Promise 对象组成的数组作为参数,当这个数组里面所有的 Promise 对象都变完成态时,Promise才为完成态,否则为拒绝态,例如下面的例子,只有当p1和p2都为完成态的时候,Promise才是完成态,只要其中一个为拒绝态,Promise则为拒绝态。

var p1 = new Promise((resolve,reject) => {
    setTimeout(() =>  {
        resolve("Hello");
    }, 3000);
});

var p2 = new Promise((resolve,reject) => {
    setTimeout(() =>  {
        resolve("World");
    }, 1000);
});

Promise.all([p1, p2]).then((result)  => {
    console.log(result); // ["Hello", "World"]
});

上面的例子模拟了传输两个数据需要不同的时长,虽然 p2 的速度比 p1 要快,但是 Promise.all 方法会按照数组里面的顺序将各自Promise的终值打包成数组传给方法then的第一个参数onFulfilled,所以输出result会输出 ["Hello", "World"]
有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法

相同的是,race方法提供了并行执行异步操作的能力,也可以接收一个由 Promise 对象组成的数组作为参数,不同的是,只要数组里面任何一个Promise的状态率先改变,调用race方法的Promise的状态也会随着改变,而且状态由参数数组里面率先改变状态的Promise决定。

例如下面的代码:

var p1 = new Promise((resolve,reject) => {
      setTimeout(() => {
          resolve('p1')
     },10000)
})
var p2 = new Promise((resolve,reject) => {
      setTimeout(() => {
          reject(new Error('time out'))
     },10)
})
var p = Promise.race([p1,p2]);
p.then( ret => console.log(ret)) .catch( err => console.log(err.toString()));    //Error: time out

只要p1, p2中任意一个实例率先改变状态,则p的状态就跟着改变,可以看出p2的速度要比p1快,因为p2是拒绝态,所以p也是拒绝态,进而执行catch。

感谢你的阅读,有不足之处请为我指出。