在异步编程中,Promise 扮演了举足轻重的角色,比传统的解决方案(回调函数和事件)更合理和更强大。可能有些小伙伴会有这样的疑问:2020年了,怎么还在谈论Promise?事实上,有些朋友对于这个几乎天天都在打交道的“老朋友”,貌似全懂,但稍加深刻就可能疑问百出,本文带你们深刻理解这个熟悉的陌生人—— Promise.javascript
new Promise( function(resolve, reject) {...} /* executor */ )
值得注意的是,Promise 是用来管理异步编程的,它自己不是异步的,new Promise的时候会当即把executor函数执行,只不过咱们通常会在executor函数中处理一个异步操做。好比下面代码中,一开始是会先打印出2。html
let p1 = new Promise(()=>{ setTimeout(()=>{ console.log(1) },1000) console.log(2) }) console.log(3) // 2 3 1
Promise 采用了回调函数延迟绑定技术,在执行 resolve 函数的时候,回调函数尚未绑定,那么只能推迟回调函数的执行。这具体是啥意思呢?咱们先来看下面的例子:前端
let p1 = new Promise((resolve,reject)=>{ console.log(1); resolve('浪里行舟') console.log(2) }) // then:设置成功或者失败后处理的方法 p1.then(result=>{ //p1延迟绑定回调函数 console.log('成功 '+result) },reason=>{ console.log('失败 '+reason) }) console.log(3) // 1 // 2 // 3 // 成功 浪里行舟
new Promise的时候先执行executor函数,打印出 一、2,Promise在执行resolve时,触发微任务,仍是继续往下执行同步任务,
执行p1.then时,存储起来两个函数(此时这两个函数尚未执行),而后打印出3,此时同步任务执行完成,最后执行刚刚那个微任务,从而执行.then中成功的方法。java
Promise 对象的错误具备“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。具有了这样“冒泡”的特性后,就不须要在每一个 Promise 对象中单独捕获异常了。git
要遇到一个then,要执行成功或者失败的方法,但若是此方法并无在当前then中被定义,则顺延到下一个对应的函数es6
function executor (resolve, reject) { let rand = Math.random() console.log(1) console.log(rand) if (rand > 0.5) { resolve() } else { reject() } } var p0 = new Promise(executor) var p1 = p0.then((value) => { console.log('succeed-1') return new Promise(executor) }) var p2 = p1.then((value) => { console.log('succeed-2') return new Promise(executor) }) p2.catch((error) => { console.log('error', error) }) console.log(2)
这段代码有三个 Promise 对象:p0~p2。不管哪一个对象里面抛出异常,均可以经过最后一个对象 p2.catch 来捕获异常,经过这种方式能够将全部 Promise 对象的错误合并到一个函数来处理,这样就解决了每一个任务都须要单独处理异常的问题。编程
经过这种方式,咱们就消灭了嵌套调用和频繁的错误处理,这样使得咱们写出来的代码更加优雅,更加符合人的线性思惟。数组
咱们都知道能够把多个Promise链接到一块儿来表示一系列异步骤。这种方式能够实现的关键在于如下两个Promise 固有行为特性:promise
先经过下面的例子,来解释一下刚刚这段话是什么意思,而后详细介绍下链式调用的执行流程浏览器
let p1=new Promise((resolve,reject)=>{ resolve(100) // 决定了下个then中成功方法会被执行 }) // 链接p1 let p2=p1.then(result=>{ console.log('成功1 '+result) return Promise.reject(1) // 返回一个新的Promise实例,决定了当前实例是失败的,因此决定下一个then中失败方法会被执行 },reason=>{ console.log('失败1 '+reason) return 200 }) // 链接p2 let p3=p2.then(result=>{ console.log('成功2 '+result) },reason=>{ console.log('失败2 '+reason) }) // 成功1 100 // 失败2 1
咱们经过返回 Promise.reject(1) ,完成了第一个调用then建立并返回的promise p2。p2的then调用在运行时会从return Promise.reject(1) 语句接受完成值。固然,p2.then又建立了另外一个新的promise,能够用变量p3存储。
new Promise出来的实例,成功或者失败,取决于executor函数执行的时候,执行的是resolve仍是reject决定的,或executor函数执行发生异常错误,这两种状况都会把实例状态改成失败的。
p2执行then返回的新实例的状态,决定下一个then中哪个方法会被执行,有如下几种状况:
咱们再来看个例子
new Promise(resolve=>{ resolve(a) // 报错 // 这个executor函数执行发生异常错误,决定下个then失败方法会被执行 }).then(result=>{ console.log(`成功:${result}`) return result*10 },reason=>{ console.log(`失败:${reason}`) // 执行这句时候,没有发生异常或者返回一个失败的Promise实例,因此下个then成功方法会被执行 // 这里没有return,最后会返回 undefined }).then(result=>{ console.log(`成功:${result}`) },reason=>{ console.log(`失败:${reason}`) }) // 失败:ReferenceError: a is not defined // 成功:undefined
从上面一些例子,咱们能够看出,虽然使用 Promise 能很好地解决回调地狱的问题,可是这种方式充满了 Promise 的 then() 方法,若是处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程。
ES7中新增的异步编程方法,async/await的实现是基于 Promise的,简单而言就是async 函数就是返回Promise对象,是generator的语法糖。不少人认为async/await是异步操做的终极解决方案:
不过也存在一些缺点,由于 await 将异步代码改形成了同步代码,若是多个异步代码没有依赖性却使用了 await 会致使性能上的下降。
async function test() { // 如下代码没有依赖性的话,彻底可使用 Promise.all 的方式 // 若是有依赖性的话,其实就是解决回调地狱的例子了 await fetch(url1) await fetch(url2) await fetch(url3) }
观察下面这段代码,你能判断出打印出来的内容是什么吗?
let p1 = Promise.resolve(1) let p2 = new Promise(resolve => { setTimeout(() => { resolve(2) }, 1000) }) async function fn() { console.log(1) // 当代码执行到此行(先把此行),构建一个异步的微任务 // 等待promise返回结果,而且await下面的代码也都被列到任务队列中 let result1 = await p2 console.log(3) let result2 = await p1 console.log(4) } fn() console.log(2) // 1 2 3 4
若是 await 右侧表达逻辑是个 promise,await会等待这个promise的返回结果,只有返回的状态是resolved状况,才会把结果返回,若是promise是失败状态,则await不会接收其返回结果,await下面的代码也不会在继续执行。
let p1 = Promise.reject(100) async function fn1() { let result = await p1 console.log(1) //这行代码不会执行 }
咱们再来看道比较复杂的题目:
console.log(1) setTimeout(()=>{console.log(2)},1000) async function fn(){ console.log(3) setTimeout(()=>{console.log(4)},20) return Promise.reject() } async function run(){ console.log(5) await fn() console.log(6) } run() //须要执行150ms左右 for(let i=0;i<90000000;i++){} setTimeout(()=>{ console.log(7) new Promise(resolve=>{ console.log(8) resolve() }).then(()=>{console.log(9)}) },0) console.log(10) // 1 5 3 10 4 7 8 9 2
作这道题以前,读者需明白:
接下来,咱们一步一步分析:
Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。
Promise.resolve()等价于下面的写法:
Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo'))
Promise.resolve方法的参数分红四种状况。
(1)参数是一个 Promise 实例
若是参数是 Promise 实例,那么Promise.resolve将不作任何修改、原封不动地返回这个实例。
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) }) const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2 .then(result => console.log(result)) .catch(error => console.log(error)) // Error: fail
上面代码中,p1是一个 Promise,3 秒以后变为rejected。p2的状态在 1 秒以后改变,resolve方法返回的是p1。因为p2返回的是另外一个 Promise,致使p2本身的状态无效了,由p1的状态决定p2的状态。因此,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,致使触发catch方法指定的回调函数。
(2)参数不是具备then方法的对象,或根本就不是对象
Promise.resolve("Success").then(function(value) { // Promise.resolve方法的参数,会同时传给回调函数。 console.log(value); // "Success" }, function(value) { // 不会被调用 });
(3)不带有任何参数
Promise.resolve()方法容许调用时不带参数,直接返回一个resolved状态的 Promise 对象。若是但愿获得一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。
Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one two
(4)参数是一个thenable对象
thenable对象指的是具备then方法的对象,Promise.resolve方法会将这个对象转为 Promise 对象,而后就当即执行thenable对象的then方法。
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
Promise.reject()方法返回一个带有拒绝缘由的Promise对象。
new Promise((resolve,reject) => { reject(new Error("出错了")); }); // 等价于 Promise.reject(new Error("出错了")); // 使用方法 Promise.reject(new Error("BOOM!")).catch(error => { console.error(error); });
值得注意的是,调用resolve或reject之后,Promise 的使命就完成了,后继操做应该放到then方法里面,而不该该直接写在resolve或reject的后面。因此,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => { return reject(1); // 后面的语句不会执行 console.log(2); })
let p1 = Promise.resolve(1) let p2 = new Promise(resolve => { setTimeout(() => { resolve(2) }, 1000) }) let p3 = Promise.resolve(3) Promise.all([p3, p2, p1]) .then(result => { // 返回的结果是按照Array中编写实例的顺序来 console.log(result) // [ 3, 2, 1 ] }) .catch(reason => { console.log("失败:reason") })
Promise.all 生成并返回一个新的 Promise 对象,因此它可使用 Promise 实例的全部方法。参数传递promise数组中全部的 Promise 对象都变为resolve的时候,该方法才会返回, 新建立的 Promise 则会使用这些 promise 的值。
若是参数中的任何一个promise为reject的话,则整个Promise.all调用会当即终止,并返回一个reject的新的 Promise 对象。
有时候,咱们不关心异步操做的结果,只关心这些操做有没有结束。这时,ES2020 引入Promise.allSettled()方法就颇有用。若是没有这个方法,想要确保全部操做都结束,就很麻烦。Promise.all()方法没法作到这一点。
假若有这样的场景:一个页面有三个区域,分别对应三个独立的接口数据,使用 Promise.all 来并发请求三个接口,若是其中任意一个接口出现异常,状态是reject,这会致使页面中该三个区域数据全都没法出来,显然这种情况咱们是没法接受,Promise.allSettled的出现就能够解决这个痛点:
Promise.allSettled([ Promise.reject({ code: 500, msg: '服务异常' }), Promise.resolve({ code: 200, list: [] }), Promise.resolve({ code: 200, list: [] }) ]).then(res => { console.log(res) /* 0: {status: "rejected", reason: {…}} 1: {status: "fulfilled", value: {…}} 2: {status: "fulfilled", value: {…}} */ // 过滤掉 rejected 状态,尽量多的保证页面区域数据渲染 RenderContent( res.filter(el => { return el.status !== 'rejected' }) ) })
Promise.allSettled跟Promise.all相似, 其参数接受一个Promise的数组, 返回一个新的Promise, 惟一的不一样在于, 它不会进行短路, 也就是说当Promise所有处理完成后,咱们能够拿到每一个Promise的状态, 而无论是否处理成功。
Promise.all()方法的效果是"谁跑的慢,以谁为准执行回调",那么相对的就有另外一个方法"谁跑的快,以谁为准执行回调",这就是Promise.race()方法,这个词原本就是赛跑的意思。race的用法与all同样,接收一个promise对象数组为参数。
Promise.all在接收到的全部的对象promise都变为FulFilled或者Rejected状态以后才会继续进行后面的处理,与之相对的是Promise.race只要有一个promise对象进入FulFilled或者Rejected状态的话,就会继续进行后面的处理。
// `delay`毫秒后执行resolve function timerPromisefy(delay) { return new Promise(resolve => { setTimeout(() => { resolve(delay); }, delay); }); } // 任何一个promise变为resolve或reject的话程序就中止运行 Promise.race([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64) ]).then(function (value) { console.log(value); // => 1 });
上面的代码建立了3个promise对象,这些promise对象会分别在1ms、32ms 和 64ms后变为肯定状态,即FulFilled,而且在第一个变为肯定状态的1ms后,.then注册的回调函数就会被调用。
ES9 新增 finally() 方法返回一个Promise。在promise结束时,不管结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都须要执行的代码提供了一种方式。这避免了一样的语句须要在then()和catch()中各写一次的状况。
好比咱们发送请求以前会出现一个loading,当咱们请求发送完成以后,无论请求有没有出错,咱们都但愿关掉这个loading。
this.loading = true request() .then((res) => { // do something }) .catch(() => { // log err }) .finally(() => { this.loading = false })
finally方法的回调函数不接受任何参数,这代表,finally方法里面的操做,应该是与状态无关的,不依赖于 Promise 的执行结果。
假设有这样一个需求:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
三个亮灯函数已经存在:
function red() { console.log('red'); } function green() { console.log('green'); } function yellow() { console.log('yellow'); }
这道题复杂的地方在于须要“交替重复”亮灯,而不是亮完一遍就结束的一锤子买卖,咱们能够经过递归来实现:
// 用 promise 实现 let task = (timer, light) => { return new Promise((resolve, reject) => { setTimeout(() => { if (light === 'red') { red() } if (light === 'green') { green() } if (light === 'yellow') { yellow() } resolve() }, timer); }) } let step = () => { task(3000, 'red') .then(() => task(1000, 'green')) .then(() => task(2000, 'yellow')) .then(step) } step()
一样也能够经过async/await 的实现:
// async/await 实现 let step = async () => { await task(3000, 'red') await task(1000, 'green') await task(2000, 'yellow') step() } step()
使用 async/await 能够实现用同步代码的风格来编写异步代码,毫无疑问,仍是 async/await 的方案更加直观,不过深刻理解Promise 是掌握async/await的基础。给你们推荐一个好用的BUG监控工具Fundebug,欢迎免费试用!
欢迎关注公众号:前端工匠,你的成长咱们一块儿见证!