首页
JS 异步编程:Promise

异步

https://developer.mozilla.org/zh-CN/docs/Glossary/Asynchronous

  • 网络与通信:像 ajax 请求这种

  • 软件设计:像前端可以通过uuid、状态管理等不用等待后端的数据,每次的 ajax 请求只是进行一次验证,可以大大地缩减等待的时间。

后端设计里面像一些邮件发送等跟主业务无关的可以通过异步的方式去处理。甚至可以利用websocket和消息队列中间件来替代传统的http请求。

异步 JavaScript

https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous

JavaScript 是单线程(single threaded)的。 即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(main thread)。

通过 Web workers 可以把一些任务交给一个名为worker的单独的线程,这样就可以同时运行多个JavaScript代码块。一般来说,用一个worker来运行一个耗时的任务,主线程就可以处理用户的交互(避免了阻塞)。

虽然在worker里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。

线程与进程回顾

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

异步代码

为了解决同步执行耗时操作,会阻塞主进程的问题。浏览器允许我们异步运行某些操作。像Promise这样的功能就允许让一些操作运行,然后等待直到结果返回,再运行其他操作。

这里很关键的一点:JS 本身并不支持异步,异步是浏览器的功能

异步编程的几种方法

https://app.yinxiang.com/shard/s54/nl/11117579/71e132e5-de31-4d31-ae10-82fbcb4500e9/
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Introducing

JS的异步编程经历了下面四个进化阶段

回调函数 —> Promise —> Generator —> async/await。

异步 callbacks(回调函数)

当我们把回调函数作为一个参数传递给另一个函数时,仅仅是把回调函数定义作为参数传递过去 — 回调函数并没有立刻执行,回调函数会在包含它的函数的某个地方异步执行,包含函数负责在合适的时候执行回调函数

  • callbacks 的方式,如果回调嵌套回调,会造成”回调地狱“

  • 并不是所有的回调都是异步的,有些是同步的,比如Array.prototype.forEach()

function f1(callback) {
  console.log('f1 begin')
  setTimeout(function () {
    callback()
  }, 1000)
  console.log('f1 end')
}

function f2() {
  console.log('f2')
}

f1(f2)

//f1 begin
//f1 end
//f2

事件监听

document.getElementById('#myDiv').addEventListener('click', function (e) {
  console.log('我被点击了')
}, false);

通过给 id 为 myDiv 的一个元素绑定了点击事件的监听函数,我们把任务的执行时机推迟到了点击这个动作发生时。此时,任务的执行顺序与代码的编写顺序无关,只与点击事件有没有被触发有关。

发布订阅

Promise

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promise的定义

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。

本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

一个 Promise 必然处于以下几种状态之一:

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。

  • 已兑现(fulfilled): 意味着操作成功完成。

  • 已拒绝(rejected): 意味着操作失败。

待定状态的 Promise 对象要么会通过一个值被兑现(fulfilled),要么会通过一个原因(错误)被拒绝(rejected)。当这些情况之一发生时,我们用 promisethen 方法排列起来的相关处理程序就会被调用。如果 promise 在一个相应的处理程序被绑定时就已经被兑现或被拒绝了,那么这个处理程序就会被调用,因此在完成异步操作和绑定处理方法之间不会存在竞争状态。

http://image.maplejoyous.cn/post/2021/12/10/2021121014433232.png

创建Promise对象

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise

Promise() 构造器用于创建 promise对象,语法:new Promise(executor)

executor是一个双参函数,参数为resolverejectPromise的实现会立即执行executor,并传入resolve和reject函数(Promise构造器将会在返回新对象之前executor)。当resolve和reject函数被调用时,它们分别对promise执行resolve和reject。executor通常会触发一些异步运算,一旦运算成功完成,则resolve掉这个promise,如果出错则reject掉。如果executor函数执行时抛出异常,promise状态会变为rejected。executor的返回值也会被忽略。

下面这些情况需要注意:

  • 如果resolve了,后面的reject就不起作用,resolve也不会覆盖前面的,但是后面的代码还是会执行

  • 如果executor函数执行时抛出异常,promise状态会变为rejectedexecutor的返回值也会被忽略。但是如果在抛异常前resolve了,会返回resolve的值

//创建一个Promise,如果 resolve 了,reject就不起作用
const myFirstPromise = new Promise((resolve, reject) => {
  resolve(123)
  reject(456)
  console.log('会执行这句代码吗?')
  resolve('会覆盖吗')
})

// 在抛异常之前 resolve 了,会返回 resolve 的值
const errorPromise = new Promise((resolve, reject) => {
  resolve('success')
  throw new Error('运行中抛出了一个异常')
})

then/catch/finally

then/catch/finally返回的都是Promise对象,这样就可以进行链式调用操作了。

then

Promise.prototype.then() 通常用于处理fulfilled状态的Promise,但是它可以接收两个参数:
Promise的成功和失败情况的回调函数。

  • 如果接收了两个参数,catch的内容是不生效的
//语法
p.then(onFulfilled[, onRejected]);

const rejectPromise2 = new Promise((resolve, reject) => {
  reject('失败了,统一由then处理')
})

rejectPromise2
  .then(
    res => {
      console.log(res)
    },
    error => {
      console.log(error)
    }
  )
  .catch(error => {
    console.log('会调用这个吗?' + error)
  })

catch

Promise.prototype.catch()其实内部调用的就是obj.then(undefined, onRejected)。当Promiserejected或抛出异常,它会被调用。

finally

promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。

finally回调函数不需要接收参数。

Promise 链式调用

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises

Promise链式调用解决了之前callbacks的回调地狱问题。

// callbacks
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

// Promise
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

事件队列/时序问题

promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快完成,然后将结果返回到JavaScript环境。

为了避免意外,即使是一个已经变成resolve状态的Promise,传递给 then()的函数也总是会被异步调用:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

传递到 then() 中的函数被置入到一个微任务队列中,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

async 与 await

https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await

asyncawait是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码。

async

async关键字放到函数前面,使其成为async function。该方法返回的是一个Promise对象

const hello = async function () {
  return 'hello'
}

console.log(hello()) // Promise
hello().then(res => console.log(res)) // hello

await

**await 只在异步函数里面才起作用。**它使代码简单多了,更容易理解 —— 去除了到处都是 .then() 代码块!

async function myFetch() {
  const res = await fetch('coffee.jpg')
  console.log(res)
}
myFetch()

它们的缺点

async/await 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。