ES6的Promise对象的使用

Promise对象,ES6新增的一个全新特性,虽然它出现很久了,but我相信,有很多的小伙伴还是没有怎么用过,今天让我们来好好的学习一下它。

Posted by 曲小强 on August 16, 2018

Promise对象,ES6新增的一个全新特性,虽然它出现很久了,but我相信,有很多的小伙伴还是没有怎么用过,今天让我们来好好的学习一下它。

Promise 的诞生

1、Promise 的含义

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

在日常开发中,经常需要用到ajax请求数据,拿到数据后,再进行一些处理,完成一些复杂的交互效果。

假如你需要用ajax进行多次请求,而且,每次请求都依赖上一次请求返回的数据来作为参数,然后继续发出请求,你写成这样,场景还原

1
2
3
4
5
6
7
8
9
10
11
12
$.ajax({
    success:function(res1){
        $.ajax({
            success:function(res2){
                $.ajax({
                    success:function(res3){
                    }
                });
            }    
        });
    }
});

可能会有更多的嵌套,如此一层一层的执行,无疑是消耗了更多的等待时间,而且多个请求之间如果有先后关系的话,就会出现回调地狱,ES6想了办法整治这种情况,这时候Promise 诞生了。

所以我们知道了 Promise 是异步编程的一种解决方案,比传统的回调函数和事件更合理和强大。

Promise对象有以下两个特点:

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调地狱

有好就有坏,Promise也有一些缺点。1、首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。2、其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。3、当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise 的基本用法

2、Promise 的基本用法

接下来,我们就看看它的基本用法:

1
2
3
const promise = new Promise(function(resolve, reject) {
  
});

Promise 对象是全局对象,你也可以理解为一个类,创建Promise实例的时候,要有那个new关键字。Promise构造函数接受一个函数作为参数,其中有两个参数:resolvereject,两个函数均为方法。resolve方法用于处理异步操作成功后业务(即从 pending 变为 resolved)。reject方法用于操作异步操作失败后的业务(即从 pending 变为 rejected)。在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise 的三种状态

上面也提到了,Promise 对象有三种状态:

  • 1、pending:刚刚创建一个 Promise 实例的时候,表示初始状态;
  • 2、fulfilledresolve 方法调用的时候,表示操作成功;
  • 3、rejectedreject 方法调用的时候,表示操作失败; 下面代码创造了一个Promise实例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    const promise = new Promise(function(resolve, reject) {
    //实例化后状态:pending
    
    if (/* 异步操作成功 */){
      resolve(value);
      // resolve方法调用,状态为:fulfilled
    } else {
      reject(error);
      // reject方法调用,状态为:rejected
    }
    });
    

    初始化实例后,对象的状态就变成了pending;当resolve方法被调用的时候,状态就会由pending变成了成功fulfilled;当reject方法被调用的时候,状态就会由pending变成失败rejected

Promise实例生成以后,可以用then()方法分别指定resolved状态和rejected状态的回调函数,用于绑定处理操作后的处理程序。

看以下操作:

1
2
3
4
5
promise.then(function(value) {
  // 操作成功的处理程序
}, function(error) {
  // 操作失败的处理程序
});

then()方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

说简单点就是参数是两个函数,第一个用于处理操作成功后的业务,第二个用于处理操作异常后的业务

举一个Promise对象的简单例子。

1
2
3
4
5
6
7
8
9
function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'hello world');
  });
}

timeout(100).then((value) => {
  console.log(value); // hello world
});

上面代码中,timeout()方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then()方法绑定的回调函数。

Promise 新建后就会立即执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

Promise 新建后立即执行,所以首先输出的是Promise。然后,then()方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

catach( ) 方法

对于操作异常的程序,Promise专门提供了一个实例方法来处理:catch()方法。

catch() 只接受一个参数,用于处理操作异常后的业务。

1
2
3
4
5
6
getJSON('/posts.json').then(function(posts) {
  // 处理成功
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

Promise 使用链式调用,是因为then方法catch方法调用后,都会返回promise对象

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

1
2
3
4
5
6
7
const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

上面代码中,promise抛出一个错误,就被catch方法指定的回调函数捕获。

如果 Promise 状态已经变成resolved,再抛出错误是无效的。

1
2
3
4
5
6
7
8
const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

上面代码中,Promiseresolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了

因为 Promise.prototype.thenPromise.prototype.catch 方法返回promise 对象, 所以它们可以被链式调用

注意: 如果一个promise对象处在fulfilledrejected状态而不是pending状态,那么它也可以被称为 settled 状态。你可能也会听到一个术语resolved ,它表示promise对象处于settled状态。

举了这么多栗子,要是没看懂,让我们串一串吧,梳理一下上面提到的。(认真的看注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 首先用 new 关键字创建一个 `Promise` 实例
const promise = new Promise(function(resolve, reject){
    // 假设 state 的值为 true
    let state = true;
    if( state ){
        // 调用操作成功方法
        resolve('操作成功');
        //状态:从pending 到 fulfilled
    }else{
        // 调用操作异常方法
        reject('操作失败');
        //状态:从pending 到 rejected
    }
});
promise.then(function (res) {
    //操作成功的处理程序
    console.log(res)
}).catch(function (error) {
    //操作失败的处理程序
    console.log(error)
})
// 控制台输出:操作成功

上面示例介绍了从 创建实例,状态转换,then方法和catch方法的使用

如果多个操作之间层层依赖,我们用Promise又是怎么处理的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const promise = new Promise(function(resolve, reject){
  if( true ){
    // 调用操作成功方法
    resolve('操作成功');
    //状态:从pending 到 fulfilled
  }else{
    // 调用操作异常方法
    reject('操作失败');
    //状态:从pending 到 rejected
  }
});
promise.then(a)
       .then(b)
       .then(c)
       .catch(requestError);

function a() {
    console.log('请求A成功');
    return '请求B,下一个是谁';
}
function b(res) {
    console.log('上一步的结果:'+ res);
    console.log('请求B成功');
    return '请求C,下一个是谁';
}
function c(res) {
    console.log('上一步的结果:'+ res);
    console.log('请求C成功');
}
function requestError() {
    console.log('请求失败');
}

如图所示:

上面的代码,先是创建一个实例,还声明了4个函数,其中三个是分别代表着请求A,请求B,请求C;有了then方法,三个请求操作再也不用层层嵌套了。我们使用then方法,按照调用顺序,很直观地完成了三个操作的绑定,并且,如果请求B依赖于请求A的结果,那么,可以在请求A的程序用使用return语句把需要的数据作为参数,传递给下一个请求,示例中我们就是使用return实现传递参数给下一步操作的。

为了更直观的看到示例所展示的情况,在下做了一张图:

再举个Promise 中微任务顺序的栗子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p = new Promise( (resolve, reject) => {
    setTimeout( () => {
        console.log('1');
    }, 3000);
    resolve(1);
}).then( () => {  // 描述:.then() 1-1
    Promise.resolve().then( () => { // 描述:.then() 2-1
        Promise.resolve().then( () => { // 描述:.then() 3-1
            console.log('2');
        })
    })
}).then( () => { // 描述:.then() 1-2
    console.log('3');
})
// 3
// 2
// 1    (3秒之后执行打印的值)

如上示例解释:

  • 1.先执行new Promise第一层的代码,遇到setTimeout,将其推入宏任务队列中(此时未执行,排在当前script代码块的宏任务之后执行),然后遇到了resolve,执行Promise后面的代码。
  • 2.遇到.then 1-1,推入微任务队列里(只是推入,并未执行,所以.then 1-2的执行时机还没有到),这个时候发现没有其他的操作需要处理(比如推其他的微任务到队列里),那么就执行当前微任务队列里的函数,也就是执行.then 1-1的回调函数。
  • 3.执行.then 1-1的回调函数的时候,发现了里面有一个完成态的Promise对象,不用管继续走,遇到了.then 2-1,推入微任务队列(只是推入,并未执行),此时.then 1-1回调执行完毕(没有return值,相当于return了一个undefined),然后Promise得以继续往下执行,遇到了.then 1-2,继续推入微任务队列(依然没执行),这时发现没有其他操作,开始顺位执行当前微任务队列里的函数(此时微任务队列里存放了.then 2-1.then 1-2的回调函数),执行.then 2-1的回调函数时,又遇到了一个完成态的Promise,不用管继续走,遇到了.then 3-1,将其推入微任务队列(未执行),然后执行.then1-2的回调,打印 3 ,此时已经没有了其他的操作,所以继续执行微任务队列里剩余的函数,即.then 3-1的回调函数,打印 2
  • 4.至此,微任务队列已经执行完毕,开始执行宏任务队列中的下一个宏任务,打印 1

再举个Promise 中微任务顺序的栗子2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p2 = new Promise( (resolve, reject) => {
    setTimeout( () => {
      console.log('1');  
    }, 3000)
    resolve(1);
}).then( () => { // 描述:.then() 1-1
    Promise.resolve().then( () => { // 描述:.then() 2-1
        console.log('2');
    }).then( () => { // 描述:.then() 1-2
        console.log('3');
    })
})
// 2
// 3
// 1   (3秒之后执行打印的值)

如上示例解释:

  • 1.如同栗子1。
  • 2.如同栗子2。
  • 3.执行.then 1-1的回调函数的时候,发现了里面有一个完成态的Promise对象,不用管继续走,遇到了.then 2-1,推入微任务队列(只是推入,并未执行),此时.then 1-1回调执行完毕(没有return值,相当于return了一个undefined),然后Promise得以继续往下执行,遇到了.then 1-2,继续推入微任务队列(依然没执行),这时发现没有其他操作,开始顺位执行当前微任务队列里的函数(此时微任务队列里存放了.then 2-1.then 1-2的回调函数),执行.then 2-1的回调函数,打印 2,然后执行.then1-2的回调,打印 3
  • 4.至此,微任务队列已经执行完毕,开始执行宏任务队列中的下一个宏任务,打印 1

除了提供了实例方法以外,Promise还提供了一些类方法,也就是不用创建实例,也可以调用的方法,下面列举几个栗子:

Promise.all( ) 方法

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。

1
2
3
4
5
6
7
8
9
10
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 3000, 'foo');
});

Promise.all([promise1, promise2, promise3]).then(values => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

解析:

因为实例promise3还没有进入成功fulfilled状态;等到了3000毫秒以后,实例promise3也进入了成功fulfilled状态,Promise.all( )才会进入then方法,然后在控制台输出:[3, 42, “foo”]

应用场景:我们执行某个操作,这个操作需要得到需要多个接口请求回来的数据来支持,但是这些接口请求之前互不依赖,不需要层层嵌套。这种情况下就适合使用Promise.all( )方法,因为它会得到所有接口都请求成功了,才会进行操作。

注意:如果传入的 promise 中有一个失败(rejected),Promise.all异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成。

Promise.finally( ) 方法

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。这避免了同样的语句需要在then()catch()中各写一次的情况。

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

1
2
3
4
5
server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

如果你想在 promise 执行完毕后无论其结果怎样都做一些处理或清理时,finally() 方法可能是有用的。

Promise.race( ) 方法

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

1
2
3
4
5
6
7
8
9
10
const p = Promise.race([
  fetch('index.php'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let promise1 = new Promise(function(resolve){
    setTimeout(function () {
        resolve('promise1实例成功');
    },4000);
});
let promise2 = new Promise(function(resolve,reject){
    setTimeout(function () {
        reject('promise2实例失败');
    },2000);
});
Promise.race([promise1, promise2]).then(function(result){
    console.log(result);
}).catch(function(error){
    console.log(error);
});
// expected output: promise2实例失败

由于promise2实例中2000毫秒之后就执行reject方法,早于实例promise15000毫秒,所以最后输出的是:promise2实例失败。

Promise.resolve( ) 方法

有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。

1
const jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代码将 jQuery 生成的deferred对象,转为一个新的 Promise 对象。

Promise.resolve等价于下面的写法。

1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有“then” 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。

警告不要在解析为自身的thenable 上调用Promise.resolve。这将导致无限递归,因为它试图展平无限嵌套的promise。一个例子是将它与Angular中的异步管道一起使用。

使用静态Promise.resolve方法

1
2
3
4
5
Promise.resolve("Success").then(function(value) {
  console.log(value); // "Success"
}, function(value) {
  // 不会被调用
});

resolve一个数组

1
2
3
4
var p = Promise.resolve([1,2,3]);
p.then(function(v) {
  console.log(v[0]); // 1
});

Resolve另一个promise

1
2
3
4
5
6
7
8
9
10
11
12
var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
  console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));

/*
*  打印顺序如下,这里有一个同步异步先后执行的区别
*  original === cast ? true
*  value: 33
*/
Promise.resolve( ) 方法

Promise.reject(reason)方法返回一个带有拒绝原因reason参数的Promise对象。

1
2
3
4
5
6
7
8
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

上面代码生成一个 Promise 对象的实例 p,状态为rejected,回调函数会立即执行。

注意Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

1
2
3
4
5
6
7
8
9
10
11
const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true

举一个promise 应用的栗子:

我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

1
2
3
4
5
6
7
8
const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

Generator 函数与 Promise 的结合

使用 Generator函数 管理流程,遇到异步操作的时候,通常返回一个Promise对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);

上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。

我目前所写的项目大多数都是Generator函数 与 Promise 的结合。

这个篇幅有点藏,如果你没有收藏可以收藏,以后慢慢的观看。

一下是我的公众号,关注我,会让你有意想不到的收获~



回到顶部 返回
顶部