【汽车之家】Promise的用法和实践

275 阅读8分钟

Promise是什么?

Promise 是异步编程的一种解决方案。 从语法上说,promise是一个对象,提供了统一的API,可以获取异步操作的消息,并让各种异步操作都可以用同样的方法进行处理。
ES6(ES2015)发布之前,各种框架都有自己开源的promise实现,而es6将promise内置到浏览器直接支持, 使其成为ES6中最重要的特性之一,并列为正式规范。
本文将以es6中的Promise为例进行阐述。

我们在浏览器控制台使用console.dir(Promise), 打印出Promise对象的所有属性和方法。

很直观的看到Promise对象本质上是一个构造函数。 具有all、resolve、reject等方法,prototype原型上有then、catch等方法。

Promise基本结构

const promise = new Promise((resolve, reject) => {
    let value = 2;
    if (value > 1) {
        resolve('成功'); // 满足条件,将值'成功'传入resolve
    } else {
        reject('失败');
    }
})
.then((resolved) => {
    console.log(resolved); // 返回 '成功'
}, (rejected) => {
    console.log(rejected);
});
复制代码

Promise是一个构造函数,通过new来构建一个Promise对象,该对象必须接受一个函数作为参数,我们称该函数为handle,handle又包含resolve和reject两个参数,它们都是函数, 可以用于改变 Promise 的状态和传入 Promise 的值,这里 resolve 传入的 "成功" 就是 Promise 的值。

resolve函数的作用: 在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject 函数的作用:在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去;

promise的三种状态
pending: 初始状态,既不是成功,也不是失败状态。
fulfilled: 操作完成。
rejected: 操作失败
promise状态有两个特点:
1.对象的状态不受外界影响,只有异步操作的结果可以决定promise的状态;
2.一旦状态改变,就不会再变,任何时候都可以得到这个结果,称为 resolved(已定型)
Promise对象的状态改变,只有两种可能:
(1) 从pending变为fulfilled
(2) 从pending变为rejected
且状态改变之后会一直保持这个状态,任何其他操作都无法改变,状态改变后,对Promise对象添加回调函数,只会立即返回之前那个改变后的状态值;
这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

let promise = new Promise(function (resolve, reject) {
    // 使用setTimeout创建一个异步操作
    setTimeout(function () {
        console.log('resolve done');
        resolve('成功');
        // Promise的状态的从pending变为fulfilled并传递一个值给状态处理方法resolve
    }, 3000);
    setTimeout(function () {
        console.log('reject done');
        reject('失败');
        // Promise的状态的从pending变为rejected并传递一个值给状态处理方法reject
    }, 2000);
});
promise.then(function(resolved) {
    console.log(resolved);
}, function(rejected) {
    console.log(rejected);
});
复制代码

执行结果:

以上代码,优先执行了'reject done', Promise的状态的从pending变为rejected,且状态不再改变,虽然后面'resolve done'也执行了,但是返回结果被丢弃,状态仍然为rejected
可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了。

.then()

then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。

const promise = new Promise((resolve, reject) => {
    let value = 2;
    if (value < 1) {
        resolve('成功'); 
    } else {
        reject('失败'); //满足条件,将'失败'传入reject
    }
})
.then((resolved) => {
    console.log(resolved);
}, (rejected) => {
    console.log(rejected); //返回 '失败'
});
复制代码

Promise任务链
then方法将返回一个 resolved 或 rejected 状态的 Promise。 对象用于链式调用,因此可以一直调用then,且Promise 对象的值就是上一个then的返回值。

const promise = new Promise((resolve, reject) => {
    let value = 2;
    if (value > 1) {
        resolve('成功'); // 满足条件,将'成功'传入resolve
    } else {
        reject('失败');
    }
})
.then((resolved) => {
    console.log(resolved); // 返回值 '成功'
    return 11;
}, (rejected) => {
    console.log(rejected); 
    return 22;
})
.then((resolved) => {
    console.log(resolved); // 返回 11;
    return new Promise((resolve, reject) => {
        let value = 2;
        if (value < 1) {
            resolve('success');
        } else {
            reject('failed'); // 满足条件,将'failed'传入reject
        }
    });
})
.then((resolved) => {
    console.log(resolved); 
})
.catch((rejected) => {
    console.log(rejected); // 返回 'failed'
});
复制代码

.catch

then中没有第二个回调的情况,则进入catch;如果有第二个回调,则不会进入catch。

const promise = new Promise((resolve, reject) => {
    let value = 2;
    if (value < 1) {
        resolve('成功');
    } else {
        reject('失败'); //满足条件,将'失败'传入reject
    }
})
.then((resolved) => {
    console.log(resolved);
})
.catch((error) => {
    console.log(error); //返回 '失败'
})
.finally(() => {
    console.log('finally'); //无论成功或失败,都会执行 'finally'
});

复制代码

promise.all()

多任务处理,具备并行执行异步操作的能力,在所有的异步操作完成后,才执行回调。

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("task 1");
        resolve("成功1");
    }, 2000);
});
let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("task 2");
        resolve("成功2");
    }, 1000);
});
let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("task 3");
        resolve("成功3");
    },3000);
});
    
Promise.all([p3, p1, p2]).then((resolved) => {
    console.log(resolved);
    // 异步操作的结果,顺序上是跟all数组内,执行的异步操作顺序是一致的;
}).catch((rejected) => {
    console.log(rejected);
});
复制代码

执行结果:

Promise.all接收一个数组作为参数,数组里可以是Promise对象,也可以是别的值,Promise会等待状态改变,当所有的数组内子Promise异步操作都执行完成后,才会进入到then里面,all会把所有的异步操作的结果按照执行的顺序放在一个数组中传给then,有任何一个失败,该Promise.all失败,返回值是第一个失败的子Promise结果。

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("失败1");
    }, 2000);
});
let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("成功2");
    }, 1000);
});
let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("失败2");
    }, 3000);
});
    
Promise.all([p1, p2, p3]).then((resolved) => {
    console.log(resolved); 
}).catch((rejected) => {
    console.log(rejected); // 返回第一个失败的值'失败1'
});
复制代码

根据定时器时间设定,p1是第一个执行失败的异步操作,此时Promise.all 返回的最终结果便是p1的失败值 '失败1' ,其他异步操作无论怎么执行,都不会改变该结果。

Promise.race

顾名思义,Promse.race就是赛跑的意思,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("失败1");
        console.log('任务1');
    }, 1000);
});
let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("成功");
        console.log('任务2');
    }, 1500);
});
let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("失败2");
        console.log('任务3');
    }, 50);
});
    
Promise.race([p1, p2, p3]).then((resolved) => {
    console.log(resolved); 
})
.catch(function(rejected){
    console.log(rejected); // 返回 '失败2'
});
复制代码

执行结果:

任务p3 定时50ms, 执行的最快,所以Promise.race最终返回的值为 "失败2",其他的任务按照定时器时间继续执行,但结果被丢弃。
常见用法: ajax异步操作和定时器放在一起使用,如果定时器先触发,就认为超时并告知用户,对超时的接口,就可以上报预警。

promise错误处理

  1. 把throw new Error语句放延时函数里,只会报错,不执行reject函数或者catch函数
new Promise((resolve, reject) => {
    setTimeout(() => {
        throw new Error('错误');
        resolve('ok');
    }, 1000);
})
.then((resolved) => {
    console.log(resolved);
}).catch(function(rejected){
    console.log(rejected);
});
复制代码

执行结果:

  1. throw new Error语句不放到延时函数里,就能在reject函数里执行
new Promise((resolve, reject) => {
    throw new Error('错误');
    resolve('ok');
})
.then((resolved) => {
    console.log(resolved);
}).catch(function(rejected){
    console.log(rejected); // 返回错误
});
复制代码

执行结果:

3. 不用throw new Error,而是直接reject(),不论是否放在延时函数里都能正常被捕获

new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('哈哈');
    }, 500);
})
.then((resolved) => {
    console.log(resolved);
})
.catch(function(rejected){
    console.log(rejected); // 返回'哈哈'
});
复制代码

Promise构造函数执行器里代码是同步还是异步执行,then呢?

new Promise((resolve, reject) => {
    resolve("OK");
    console.log(1);
    setTimeout(() => {
        console.log(2);
    }, 0);
    console.log(3);
})
.then((resolved) => {
    console.log(resolved);
    console.log(4);
})
.catch(function(rejected){
    console.log(rejected);
});
console.log(5);
复制代码

我们先来了解浏览器event loop的机制:
Javascript是单线程的,所有的同步任务都会在主线程中执行,当主线程中的任务,都执行完之后,系统会 “依次” 读取任务队列里的事件,与之相对应的异步任务进入主线程,开始执行。
创建Promise实例,构造函数执行器是同步执行的,属于宏任务,而回调then方法是基于微任务的异步执行的,宏任务的优先级高于微任务,每一个宏任务执行完毕都必须将当前的微任务队列清空。
setTimeout异步会放到异步队列中等待执行。
promise.then异步会放到microtask queue(微任务队列)中。
microtask队列中的内容经常是为了需要直接在当前脚本执行完后立即发生的事, 所以当同步脚本执行完之后,就调用microtask队列中的内容,然后把异步队列中的setTimeout放入执行栈中执行,所以最终结果是先执行Promise构造函数里的宏任务,再执行promise.then的异步(微任务),最后执行setTimeout异步任务。
主线程会不断重复上面的步骤,直到执行完所有任务;
执行结果:



作者:汽车之家-前端体验部-李红雷