乐趣区

关于javascript:你真的懂Promise吗

前言

在异步编程中,Promise 表演了无足轻重的角色,比传统的解决方案(回调函数和事件)更正当和更弱小。有些敌人对于这个简直每天都在打交道的“老朋友”,貌似全懂, 但稍加深刻就可能疑难百出,本文带大家深刻了解这个相熟的陌生人—— Promise.

根本用法

1. 语法

new Promise(function(resolve, reject) {...} /* executor */ )

  • 构建 Promise 对象时,须要传入一个 executor 函数,次要业务流程都在 executor 函数中执行。
  • Promise 构造函数执行时立刻调用 executor 函数,resolve 和 reject 两个函数作为参数传递给 executor,resolve 和 reject 函数被调用时,别离将 promise 的状态改为 fulfilled(实现)或 rejected(失败)。一旦状态扭转,就不会再变,任何时候都能够失去这个后果。
  • 在 executor 函数中调用 resolve 函数后,会触发 promise.then 设置的回调函数;而调用 reject 函数后,会触发 promise.catch 设置的回调函数。

值得注意的是,Promise 是用来治理异步编程的,它自身不是异步的,new Promise 的时候会立刻把 executor 函数执行,只不过咱们个别会在 executor 函数中解决一个异步操作。比方上面代码中,一开始是会先打印出 2。

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('JAVA_朴学生')
  console.log(2)
})
// then: 设置胜利或者失败后处理的办法
p1.then(result=>{
 //p1 提早绑定回调函数
  console.log('胜利'+result)
},reason=>{console.log('失败'+reason)
})
console.log(3)
// 1
// 2
// 3
// 胜利 JAVA_朴学生

new Promise 的时候先执行 executor 函数,打印出 1、2,Promise 在执行 resolve 时,触发微工作,还是持续往下执行同步工作,
执行 p1.then 时,存储起来两个函数(此时这两个函数还没有执行), 而后打印出 3,此时同步工作执行实现,最初执行刚刚那个微工作,从而执行.then 中胜利的办法。

错误处理

Promise 对象的谬误 具备“冒泡”性质,会始终向后传递,直到被 onReject 函数解决或 catch 语句捕捉为止。具备了这样“冒泡”的个性后,就不须要在每个 Promise 对象中独自捕捉异样了。

要遇到一个 then,要执行胜利或者失败的办法,但如果此办法并没有在以后 then 中被定义,则顺延到下一个对应的函数

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 固有行为个性:

  • 每次你对 Promise 调用 then,它都会创立并返回一个新的 Promise,咱们能够将其链接起来;
  • 不论从 then 调用的实现回调(第一个参数)返回的值是什么,它都会被主动设置为被链接 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 中哪一个办法会被执行,有以下几种状况:

  • 不论是胜利的办法执行,还是失败的办法执行(then 中的两个办法),但凡执行抛出了异样,则都会把实例的状态改为失败。
  • 办法中如果返回一个新的 Promise 实例(比方上例中的 Promise.reject(1)),返回这个实例的后果是胜利还是失败,也决定了以后实例是胜利还是失败。
  • 剩下的状况基本上都是让实例变为胜利的状态,上一个 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

async & await

从下面一些例子,咱们能够看出,尽管应用 Promise 能很好地解决回调天堂的问题,然而这种形式充斥了 Promise 的 then() 办法,如果解决流程比较复杂的话,那么整段代码将充斥着 then,语义化不显著,代码不能很好地示意执行流程。

ES7 中新增的异步编程办法,async/await 的实现是基于 Promise 的,简略而言就是 async 函数就是返回 Promise 对象,是 generator 的语法糖。很多人认为 async/await 是异步操作的终极解决方案:

  • 语法简洁,更像是同步代码,也更合乎一般的浏览习惯;
  • 改良 JS 中异步操作串行执行的代码组织形式,缩小 callback 的嵌套;
  • Promise 中不能自定义应用 try/catch 进行谬误捕捉,然而在 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 

做这道题之前,读者需明确:

  • 基于微工作的技术有 MutationObserver、Promise 以及以 Promise 为根底开发进去的很多其余的技术,本题中 resolve()、await fn()都是微工作。
  • 不论宏工作是否达到工夫,以及搁置的先后顺序,每次主线程执行栈为空的时候,引擎会优先解决微工作队列,解决完微工作队列里的所有工作,再去解决宏工作。

接下来,咱们一步一步剖析:

  • 首先执行同步代码,输入 1,遇见第一个 setTimeout,将其回调放入工作队列(宏工作)当中,持续往下执行
  • 运行 run(), 打印出 5,并往下执行,遇见 await fn(),将其放入工作队列(微工作)
  • await fn() 以后这一行代码执行时,fn 函数会立刻执行的, 打印出 3,遇见第二个 setTimeout,将其回调放入工作队列(宏工作),await fn() 上面的代码须要期待返回 Promise 胜利状态才会执行,所以 6 是不会被打印的。
  • 持续往下执行,遇到 for 循环同步代码,须要等 150ms, 尽管第二个 setTimeout 曾经达到工夫,但不会执行,遇见第三个 setTimeout,将其回调放入工作队列(宏工作),而后打印出 10。值得注意的是,这个定时器 推迟时间 0 毫秒实际上达不到的。依据 HTML5 规范,setTimeOut 推延执行的工夫,起码是 4 毫秒。
  • 同步代码执行结束,此时没有微工作,就去执行宏工作,下面提到曾经到点的 setTimeout 先执行,打印出 4
  • 而后执行下一个 setTimeout 的宏工作,所以先打印出 7,new Promise 的时候会立刻把 executor 函数执行,打印出 8,而后在执行 resolve 时,触发微工作,于是打印出 9
  • 最初执行第一个 setTimeout 的宏工作,打印出 2

罕用的办法

1、Promise.resolve()

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
});

2、Promise.reject()

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);
})

3、Promise.all()

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 对象。

4、Promise.allSettled()

有时候,咱们不关怀异步操作的后果,只关怀这些操作有没有完结。这时,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 的状态, 而不论是否解决胜利。

5、Promise.race()

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 注册的回调函数就会被调用。

6、Promise.prototype.finally()

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 的根底。

退出移动版