浅析 Promise

前言

刚开始的时候自己对 Promise 的了解不是太深,看到 Promise 就头疼,然后看阮一峰老师的 ES6 标准入门的时候根本看不下去,应该是自己之前的水平太差了,不过最近在看的时候发现基本都能理解了,所以尽可能以一个小白的观点整理一下,希望能帮助更多人理解,这篇文章适合对 Promise 了解的不多,但是想了解的人。如果能耐心读下去的话肯定会对你了解 Promise 有所帮助。

1.1 什么是 Promise

首先我们要知道 Promise 是什么,它可以解决什么问题。

假设有这么一个场景,你需要向后台请求三个字符串,然后需要等到这三个请求的结果都返回然后拼接字符串在进行相关的操作,你该怎么做?

可能你会在 ajax 请求里面的回调里面发起第二个请求,然后第二个请求的回调发起第三个请求,最后在第三个请求的回调里面进行相关操作。是不是想一想就觉得写起来很别扭,而用 Promise 的话就很好解决了


var result = Promise.all([ajax1,ajax2,ajax3]).then(()=>{
    // Todo
})

那么,Promise 到底是什么呢,来看一下定义

Promise 是异步编程的一种解决方案,比起传统的解决方案——回调函数和事件,它更合理且更强大,这一点从上面的例子也可以看出来。

Promise 简单来说就是一个容器,里面放着某个未来才会结束的事件的结果(通常是一个异步操作的结果)。比如说你做数学题的时候遇到了一个难题,然后你把这个难题交给一个大佬,然后你继续做你的事情,然后大佬做出来(或者做不出来)以后将结果告诉你,你再进行相应操作。这个你交给别人做等别人做完返回给你结果的难题就相当于一个 Promise 对象。写成代码如下

let promise = new Promise(function(resolve, reject){
   let problem = new Problem(); // 你不会做的题目
   if( solve(problem) )  return resolve('solve'); // 大佬尝试解决这个问题,如果解出来传一个 solve 给你
   else return reject(new Error('not solve')) // 大佬做不出来,传一个 not resolve 给你
});

promise.then(function(value){
    // value 就是大佬传过来的值。然后用大佬解出来的结果进行相关操作
}).catch(function(err){
    // err 就是大佬解不出来传回的值。假设大佬解不出来进行相关操作
})

它有两个特点,一是它代表的是一个异步的操作,有三种状态:Pending(进行中),Fulfilled(已成功)和 Reject(已失败)。只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这种状态,这也是 Promise 的由来,代表无法改变。

二是状态一旦改变就不会再次改变,并且再次调用的时候回立即获得结果。有两种状态改变的可能:从 Pending 变为 Fuilfilled 或者从 Pending 变为 Rejected。状态变化以后就处于 Resolved(已定型)的阶段。这就相当于你让大佬做题,大佬要么不会,要么会,会的话你什么时候问他他都会直接告诉你结果,而不是再去算一遍,不会的话他也是直接告诉你结果,也不会去重新算一遍。

但是 Promise 有两个缺点,一个是一旦新建 Promise 就会立即执行,无法中途取消。
再者,当处于 Pending 状态的时候,无法得知当前进展到哪一个阶段(刚刚开始还是即将完成)。对应到前面的例子的话就是,你把这道题交给大佬了,大佬一定会做,大佬就是喜欢钻研,有题目了一定要得到结果,所以才能称得上是大佬。

还有一个就是如果不设置回调函数, Promise 内部抛出的错误不会反应到外部。

接下来介绍一些 Promise 的基本用法。

1.2 基本用法

直接上代码

let promise = new Promise(function(resolve, reject){
    // todo
    if(/*异步操作成功*/){
        resolve(value);
    } else {
        reject(error);
    }
})
// 等同于下面这种方式,因为既然想了解 Promise 的话应该对箭头函数有所了解,所以后面会采用下面这种方式。
let promise = new Promise((resolve, reject)=>{
    // todo
    if(/*异步操作成功*/){
        resolve(value);
    } else {
        reject(error);
    }
})

Promise 构造函数接收一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们也是两个函数,有 javascript 引擎提供,不用自己部署

resolve 的作用将 Promise 对象的状态从 “未完成” 变成 “成功”,并将成功时的结果作为参数传出去。

reject 的作用是将 Promise 对象的状态从 “未完成” 变成 “失败”。并将失败的结果作为参数传出去。

Promise 实例生成以后,可以用 then 方法分别指定 Resolved 状态和 Rejected 状态的回调函数。

promise.then(val => {
    // success
},err =>{
    // fail
})

then 方法接收两个回调函数作为参数,第一个回调函数是 Promise 对象状态变为 Fulifilled 时调用,第二个回调函数是 Promise 对象状态变为 Rejected 时调用。第二个参数可选,一般采用 catch 捕捉错误,尽量不使用第二个参数。这两个 函数都接受 Promise 对象传出的值作为参数。

下面是一个简单的例子

var drink = true;
const promise = new Promise((resolve,reject)=>{
    if(drink) return resolve(drink);
    else return reject(new Error('err'));
})

promise.then(val => console.log(val))
.catch(err => console.log(err));

上面的例子创建了一个 Promise 对象,如果 drink 是 true 的话就将 drink 的值传出去,如果是 false 的话,传出 err。

还有一点要注意的是 Promise 对象中的代码会立即执行,看下面的例子

let promise = new Promise((resolve,reject)=>{
    console.log('Promise');
    resolve();
})

promise.then(() => console.log('Resolved'));

console.log('Hi');

// Promise
// Hi
// Resolved

上述代码中,Promise 新建后会立即执行,所以先输出 Promise,然后 then 方法指定的回调函数将会在当前脚本所有同步任务执行完成后才会执行,所以 Resolved 最后输出。这部分的话可以看看宏任务微任务相关内容。

下面来看一个异步加载图片的例子

function loadImgAsync(url){
    let promise = new Promise((resolve,reject) =>{
        var img = new Image();
        img.src = url;
        img.onload = resolve(img);
        img.onerror = reject(new Error('Could not load image at ' + url));
    });
    return promise;
}

在上述代码中,封装了一个图片加载的异步操作,如果加载成功就调用 resolve 方法,如果出错了,就调用 reject 方法。下面来试一下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Promise</title>
</head>
<body>
    <div class=main ></div>
    <script>

    let url = 'https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg';

    let loadImg = loadImgAsync(url)
    loadImg.then((img)=>{
        let div = document.querySelector('.main');
        div.appendChild(img);
        console.log('success');
    });



    function loadImgAsync(url){
        let promise = new Promise((resolve,reject) =>{
            var img = new Image();
            img.onload = resolve(img);
            img.onerror = reject(new Error('Could not load image at ' + url));
            img.src = url;
        });
        return promise;
    }

    </script>
</body>
</html>

效果如下

假设 url 有问题呢
我们修改 url 为一个不存在的地址

例如 let url = 'https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.png';

由这个例子可以看出,resolve 和 reject 调用时如果带有参数,那么这些参数会被传递给回调函数。一般情况下,reject 函数的参数通常是 Error 对象的实例,表示抛出错误;resolve 函数的参数除了正常的值外,还可能是另外一个 Promise 对象,如下所示

var p1 = new Promise((resolve,reject) =>{
    // todo
});

var p2 = new Promise((resolve,reject) =>{
    // todo
    resolve(p1);
});

上述代码中,p2 的 resolve 方法将 p1 作为参数,即一个异步操作的结果是返回另一个异步操作

此时 p1 的状态就会传给 p2。也就是说,p1 的状态决定了 p2 的状态。如果 p1 的状态是 Pending,那么 p2 就会等待 p1 的状态改变,如果 p1 已经是 Resolved 或者 Rejected,那么 p2 的回调函数将会立即执行。

var p1 = new Promise((resolve,reject) => {
    reject(new Error('fail'));
});

var p2 = new Promise((resolve,reject) => {
    resolve(p1);
});

p2.then(result => console.log(result))
.catch(error => console.log(error))
// Error:fail

上面的代码中,由于 p2 返回的是 p1,所以 p2 的状态无效,由 p1 的状态决定 p2 的状态,所以后面的 then 语句都是针对后者(p1)的。p1 变为 rejected,触发 catch 方法指定的回调函数。

还有一点要注意就是调用 resolve 和 rejecte 并不会终止 Promise 函数执行

new Promise((resolve,reject)=>{
    resolve(1);
    console.log(2);
}).then(val => console.log(val));

// 2
// 1

所以一般在 resolve 和 rejecte 前面加上 return

new Promise((resolve,reject)=>{
    return resolve(1);
    console.log(2);
}).then(val => console.log(val));

// 1

这样后面的语句就不会执行了。

1.3 Promise.prototype.then()

下面来看一下 promise 对象的方法,首先是 then 方法,它有两个参数,第一个参数是成功时候调用的函数,第二个是失败时候调用的函数,它的返回是一个新的 Promise 实例,因此可以采用链式写法,即 then 方法后面在调用另一个 then 方法

new Promise((resolve,reject)=>{
    return resolve({a:1});
}).then(val => console.log(val.a))
.then(val => console.log(val));

// 1

1.4 Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null,rejection) 的别名,用于指定发生错误时的回调函数。

 p.then(val => console.log('fulfilled:',val))
 .catch(err => console.log('rejected:',err));

 // 等同于
  p.then(val => console.log('fulfilled:',val))
 .then(null,err => console.log('rejected:',err));

下面是一个例子

var promise = new Promise((resolve,reject)=>{
    throw new Error('test');
});

promise.catch(err => console.log(err));

// Error:test
// 等同于下面两种写法
var promise = new Promise((resolve,reject)=>{
    try {
        throw new Error('test');
    } catch(e) {
        reject(e);
    }
});

promise.catch(err => console.log(err));

// 或

var promise = new Promise((resolve,reject) =>{
    reject(new Error('test'));
});
promise.catch(err => console.log(err));

比较以上两种写法,可以发现 rejecte 方法的作用等同于抛出错误。如果 Promise 的状态已经变成 Resolved,再抛出错误时无效的。

var promise = new Promise((resolve,reject) =>{
    resolve('ok');
    throw new Error('test');
});
promise.then( val => console.log(val))
.catch( err => console.log(err));
// ok

上面代码中,Promise 在 resolve 语句后面在抛出错误,并不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就会保持下去。
Promise 对象的错误具有 “冒泡” 的性质,会一直向后传递,直到被捕获为止,也就是说,错误总是会被下一个 catch 语句捕获。

需要注意的是,catch 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then 方法。

var someAsyncThing = function(){
    return new Promise((resolve,reject)=>{
        // 下一行会报错,因为 x 没有声明
        resolve( x + 2 );
    });
};

someAsyncThing().catch(err => console.log('oh no',err))
.then(()=>console.log('carry on'));
// oh no ReferenceError: x is not defined
// carry on

1.5 Promise.resolve()

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

Promise.resolve('foo')

等价于

new Promise(resolve => resolve('foo'))

Promise.resolve 方法的参数分为以下四种情况

  • 参数是一个 Promise 实例

如果参数是 Promsie 实例,则不做任何修改,直接返回

  • 参数是一个 thenable 对象

thenable 对象指的是具有 then 方法的对象,如下面这个对象

let thenable = {
    then:function(resolve,reject){
        resolve(42);
    }
}

Promise.resolve 方法会将这个对象转换为 Promise 对象,然后立即执行 thenable 对象的 then 方法。

let thenable = {
    then:function(resolve,reject){
        resolve(42);
    }
}

let p1 = Promise.resolve(thenable);

p1.then(val => console.log(val)); // 42

上述代码中,thenable 对象的 then 方法执行后,对象 p1 的状态就变成 resolved,从而立即执行最后的 then 方法指定的回调函数,输出 42;

  • 参数不是具有 then 方法的对象或者不是对象

如果对象是一个基本类型的值,或者是一个不具有 then 方法的对象,那么 Promise.resolve 方法返回一个新的 Promise 对象,状态为 Resolved。

var promise = Promise.resolve('Hello');

promise.then(val => console.log(val));
// Hello

上面的代码生成一个新的 Promsie 对象的实例 promise,由于字符串 Hello 不属于异步操作(因为字符串对象不具有 then 方法),返回 Promise 实例的状态从生成起就是 Resolved,所以回调函数会立即执行。Promise.resolve 方法的参数会同时传给回调函数

  • 不带任何参数

Promise.resolve 方法允许在调用时不带任何参数,直接返回一个 Resolved 状态的 Promise 对象。

所以如果想获得一个 Promise 对象,比较方便的方法就是直接调用 Promsie.resolve 方法

var p = Promise.resolve();

p.then(()=>{
    // todo
})

上面代码中的变量 p 就是一个 Promise 对象。要注意的是,立即 resolve 的 Promise 对象是在本轮 “事件循环” 结束时,而不是在下一轮事件循环开始时,这里可以参考事件循环的相关知识。

1.6 Promise.reject()

Promise.reject 方法也会返回一个新的 Promise 实例,状态为 Rejected。

var promise = Promise.reject('出错了');

// 等同于

var promise = new Promise((resolve,reject) => reject('出错了'));

p.then((null,err) => console.log(err))
// 出错了

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

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

const thenable = {
    then(resolve,reject){
        reject('出错了');
    }
};

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

1.7 Promsie.all()

Promise.all 方法用于将多个 Promise 实例包装成一个新的 Promise 实例

Promise.all 接收一个数组作为参数,如果该参数不是数组,就会调用 Promise.resolve 方法将参数转为 Promise 实例,再进一步处理(Promise.all 方法的参数不一定是数组,但是必须有 Iterator 接口,且返回的每个成员都是 Promise 实例)

var promise = new Promise([p1,p2,p3]);

promise 的状态有 p1,p2,p3 共同决定,分为两种情况

  1. 只有三个状态都变成 Fulfilled,promise 的状态才会变成 Fulfilled,此时,p1,p2,p3 的返回值组成一个数组,传递给 promise 的回调函数。
  2. 只要其中一个被 Rejected,promise 的状态就会变成 Rejected,此时第一个被 Rejected 的实例的返回值会传递给 promise 的回调函数

我们来用之前的异步加载图片做一个具体的例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Promise</title>
</head>
<body>
    <div class=main ></div>
    <script>

    let img1 = loadImgAsync('http://img.zcool.cn/community/01b34f58eee017a8012049efcfaf50.jpg@1280w_1l_2o_100sh.jpg');
    let img2 = loadImgAsync('http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg@1280w_1l_2o_100sh.jpg');
    let img3 = loadImgAsync('http://img07.tooopen.com/images/20170316/tooopen_sy_201956178977.jpg');

    let imgPromise = Promise.all([img1,img2,img3]);

    imgPromise.then((imgArr) => {
        let main = document.querySelector('.main');
        console.log(imgArr);
        let fragment = document.createDocumentFragment();
        imgArr.forEach(ele => {
            fragment.append(ele);
        });
        main.append(fragment);
    }).catch( err => console.log(err) );

    function loadImgAsync(url){
        let promise = new Promise((resolve,reject) =>{
            var img = new Image();
            img.src = url;
            img.onload = resolve(img);
            img.onerror = reject(new Error('Could not load image at ' + url));
        });
        return promise;
    }

    </script>
</body>
</html>

随便找了三张图,结果如下

上述代码中,我们创建了 3 个 Promise 对象,当这三个 Promise 对象都加载成功的时候才会调用 imgPromise 的 then 方法,然后将三张图片添加到文档碎片中,最后将文档碎片添加到页面。

还有一点要注意的是如果作为参数的 Promise 实例自身定义了 catch 方法,那么它被 rejected 时不会触发 Promise.all 的 catch 方法。

举个栗子

 const p1 = new Promise((resolve,reject) => {
     resolve('hello');
 })
 .then( result => result)
 .catch( err => err);

 const p2 = new Promise((resolve,reject) => {
     throw new Error('报错了');
 })
 .then( result => result)
 .catch( err => err);

 Promise.all([p1,p2])
 .then(result => console.log(result))
 .catch(err => console.log(err));
 // ['Hello',Error:报错了]

上述的代码中,p1 会 resolved,p2 会首先 rejected,但是 p2 有自己的 catch 方法,该方法返回的是一个新的 Promise 对象,p2 实际上指的是这个实例,该实例执行完 catch 方法以后也会变成 resolved,导致 Promise.all() 方法中两个实例都会 resolved,因此会调用 then 方法指定的回调函数,而不会调用 catch 方法指定的回调函数。
如果 p2 没有自己的 catch 方法,就会调用 Promise.all 中的 catch 方法。

1.8 Promise.race()

Promise.race() 方法同样是将多个 Promise 对象包装成一个新的 Promise 实例。但是只要其中一个实例的状态率先改变,p 的状态就跟着改变,那个率先改变状态的 Promise 实例的返回值就传给 Promise.race() 包装的新对象。

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

还是以异步加载图片为例,只需要将 Promise.all 改成 Promise.race

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Promise</title>
</head>
<body>
    <div class=main ></div>
    <script>

    let img1 = loadImgAsync('http://img.zcool.cn/community/01b34f58eee017a8012049efcfaf50.jpg@1280w_1l_2o_100sh.jpg');
    let img2 = loadImgAsync('http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg@1280w_1l_2o_100sh.jpg');
    let img3 = loadImgAsync('http://img07.tooopen.com/images/20170316/tooopen_sy_201956178977.jpg');

    let imgPromise = Promise.race([img1,img2,img3]);

    imgPromise.then((img) => {
        let main = document.querySelector('.main');
        console.log(img);
        main.append(img);
    }).catch( err => console.log(err));

    function loadImgAsync(url){
        let promise = new Promise((resolve,reject) =>{
            var img = new Image();
            img.src = url;
            img.onload = resolve(img);
            img.onerror = reject(new Error('Could not load image at ' + url));
        });
        return promise;
    }

    </script>
</body>
</html>

这里可以看出,当 img1 的状态改变时,imgPromise 就调用了它的回调函数

总结

这篇文章写的有些繁琐了,因为基本把 Promise 的基础知识都整理出来了,其实主要需要掌握 Promise 的基本用法,以及 Promise.all 和 Promise.race 就行,如果看完了想实践一下或者觉得概念太抽象想通过具体的例子来看的话可以参考这篇文章
八段代码彻底掌握 Promise

猜你喜欢

转载自blog.csdn.net/zhang6223284/article/details/82185560
今日推荐