重学Promise、顺带Async/Await

·  阅读 910
重学Promise、顺带Async/Await

靡不有初,鲜克有终

不积跬步无以至千里

一、Promise的理解和使用

1、Promise是什么?

1.1、理解

1.1.1、抽象表达:

  1. Promise 是一门新的技术(ES6 规范正式纳入,2017年);

  2. PromiseJS 中进行异步编程的新解决方案;旧方案关于异步操作是单纯使用回调函数,如果存在依赖过深的话,就会出现著名的回调地狱问题;

常见异步操作如下:

a、fs 文件操作

require('fs').readFile('./index.html', (err,data)=>{})

// 场景需求:依次读取1、2、3.txt文件的值,注意要求是依次
const fs = require('fs');
​
fs.readFile('./1.txt', 'utf8', (err, result1) => {
  console.log(result1)
  fs.readFile('./2.txt', 'utf8', (err, result2) => {
    console.log(result2)
    fs.readFile('./3.txt', 'utf8', (err, result3) => {
      console.log(result3)
    })
  })
});
复制代码

b、数据库操作

c、Ajax

$.get('/server', (data)=>{})

d、定时器

setTimeout(()=>{}, 2000)

1.1.2、具体表达:

  1. 从语法上来说:

Promise是一个构造函数,其实就是在原本的异步操作的外面再包裹一层promise进行异步操作,见下面例一

  1. 从功能上来说:

Promise 可以进行对象的实例化,而实例化的该对象可以用来封装一个异步操作,并且可以获取成功或者失败的结果值;

1.2、promise 的状态改变

1、pending 变为resolved

2、pending变为 rejected

Note:

promise只可能有三种状态:

等待(pending)、已完成(resolved/fulfilled)、已拒绝(rejected)

promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换;

且状态只能改变一次,即改变之后无论变为成功还是失败, 都不会再改变

then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用;

1.3、promise的基本流程

图片.png

1.4、promise的基本使用

例一:基本操作

    // 1) 创建 promise 对象(pending 状态), 指定执行器函数
      const p = new Promise((resolve, reject) => {
        // 2) 在执行器函数中启动异步任务
        setTimeout(() => {
          const time = Date.now()
          // 假设: 时间为奇数代表成功, 为偶数代表失败
          if (time % 2 === 1) {
            resolve('成功的值 ' + time)
          } else {
            reject('失败的值' + time)
          }
        }, 2000)
      })
​
      // 3) 能 promise 指定成功或失败的回调函数来获取成功的 vlaue 或失败的 reason
​
      // p.then(
      //   (value) => {
      //     // 成功的回调函数 onResolved, 得到成功的 vlaue
      //     console.log('成功的 value: ', value)
      //   },
      //   (reason) => {
      //     // 第二个参数
      //     // 失败的回调函数 onRejected, 得到失败的 reason
      //     console.log('失败的 reason: ', reason)
      //   }
      // )
​
      // --------------------还是更建议这种写法,方便直观-------------------
      p.then((value) => {
        // 成功的回调函数 onResolved, 得到成功的 vlaue
        console.log('成功的 value: ', value)
      }).catch((reason) => {
        // 第二个参数
        // 失败的回调函数 onRejected, 得到失败的 reason
        console.log('失败的 reason: ', reason)
      })
复制代码

例二:封装基于定时器的异步函数

    function doDelay(time) {
        // 1. 创建 promise 对象
        return new Promise((resolve, reject) => {
          // 2. 启动异步任务
          console.log('启动异步任务')
          setTimeout(() => {
            console.log('延迟任务开始执行...')
            const time = Date.now()
            // 假设: 时间为奇数代表成功, 为偶数代表失败
            if (time % 2 === 1) {
              resolve('成功的数据 ' + time)
            } else {
              reject('失败的数据 ' + time)
            }
          }, time)
        })
      }
​
      const promise = doDelay(2000)
​
      promise
        .then((value) => {
          console.log('成功的 value: ', value)
        })
        .catch((reason) => {
          console.log('失败的 reason: ', reason)
        })
复制代码

例三:封装ajax 异步请求函数

Note:

ajax请求本身就是异步操作;

      /*
      可复用的发 ajax 请求的函数: xhr + promise
      */
      function promiseAjax(url) {
        return new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest()
          xhr.onreadystatechange = () => {
            if (xhr.readyState !== 4) return
            const { status, response } = xhr
            // 请求成功, 调用 resolve(value)
            if (status >= 200 && status < 300) {
              resolve(JSON.parse(response))
            } else {
              // 请求失败, 调用 reject(reason)
              reject(new Error('请求失败: status: ' + status))
            }
          }
          xhr.open('GET', url)
          xhr.send()
        })
      }
​
      promiseAjax('https://api.apiopen.top2/getJoke?page=1&count=2&type=video')
        .then((data) => {
          console.log('显示成功数据', data)
        })
        .catch((error) => {
          alert(error.message)
        })
复制代码

2、 为什么要用 Promise?

2.1、指定回调函数的方式更加灵活

1、旧方案: 必须在启动异步任务前指定

2、promise: 启动异步任务 => 返回promise对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定多个)

2.2、支持链式调用,可以解决回调地狱问题

2.2.1、什么是回调地狱?

回调函数嵌套调用, 内部函数执行条件(例如参数)依赖 外部回调函数异步执行的结果

2.2.2、 回调地狱的缺点?

不便于阅读,不便于异常处理,不易维护等

2.2.3、解决方案?

promise 链式调用

2.2.4、终极解决方案?

async/await

2.2.5、实例

例一、promise解决回调地狱

const fs = require('fs');
​
// --------------回调地狱---------------------// fs.readFile('./1.txt', 'utf8', (err, result1) => {
//  console.log(result1)
//  fs.readFile('./2.txt', 'utf8', (err, result2) => {
//    console.log(result2)
//    fs.readFile('./3.txt', 'utf8', (err, result3) => {
//      console.log(result3)
//    })
//  })
// });function p1 () {
  return new Promise ((resolve, reject) => {  // return一个promise对象
    fs.readFile('./1.txt', 'utf8', (err, result) => {
      resolve(result)
    })
  });
}
​
function p2 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./2.txt', 'utf8', (err, result) => {
      resolve(result)
    })
  });
}
​
function p3 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./3.txt', 'utf8', (err, result) => {
      resolve(result)
    })
  });
}
​
p1().then((r1)=> {
  console.log(r1);
  return p2();
})
.then((r2)=> {
  console.log(r2);
  return p3();
})
.then((r3) => {
  console.log(r3)
})
复制代码

例二、asyncFunc异步函数

1.在普通函数定义的前面加上async关键字 普通函数就变成了异步函数;

2.异步函数默认的返回值是promise对象,而不是单纯的undefined

3.在异步函数内部使用throw关键字进行错误的抛出,代替promise中的reject

另外,await关键字:

1.它只能出现在异步函数async中;

2.await (一个promise操作)它可以暂停异步函数的执行 等待promise对象返回结果后再向下执行函数;

async function fn() {
  throw "发生了一些错误";
}
​
fn()
  .then(function (data) {
    console.log(data);
  })
  .catch(function (err) {
    console.log(err);
  });
​
​
// --------------------async----------------------
async function p1() {
    return "p1";
}
​
async function p2() {
  return "p2";
}
​
async function p3() {
  return "p3";
}
​
console.log('我返回的不是单纯的字符串',p1());
​
async function run() {
  let r1 = await p1();
  let r2 = await p2();
  let r3 = await p3();
  console.log(r1);
  console.log(r2);
  console.log(r3);
}
​
run();
复制代码

Note:

1、因为异步函数async默认的返回值是promise对象,所以此处的p1、p2、p3函数的返回值不是单纯的字符串,而是promise对象: 图片.png

MDN官方

图片.png 2、 await 后面可以跟任何的JS 表达式

Note:

虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象;

如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决

如果await后面是 正常的表达式则按照常规代码立即执行。

例如:

图片.png

例三、asyncFuncReadFile

const fs = require('fs');
// 改造现有异步函数api 让其返回promise对象 从而支持异步函数语法
const promisify = require('util').promisify;
// 调用promisify方法改造现有异步API 让其返回promise对象,从而使得await‘生效’
const readFile = promisify(fs.readFile);
​
async function run () {
  let r1 = await readFile('./1.txt', 'utf8')
  let r2 = await readFile('./2.txt', 'utf8')
  let r3 = await readFile('./3.txt', 'utf8')
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
​
run();
复制代码

3、如何使用Promise?

3.1、API的使用

1、Promise构造函数: Promise (excutor) {}

(1) executor函数: 执行器(resolve, reject) => {}

(2) resolve函数: 内部定义成功时我们调用的函数 value => {}

(3) reject函数: 内部定义失败时我们调用的函数reason => {}

Note:

executor 会在Promise内部立即同步调用,异步操作在执行器中执行

图片.png

所以在我的这篇文章,传送门:JS的事件执行机制

涉及到的new Promise(function (resolve, reject) {...}操作,默认属于同步执行代码

2、Promise.prototype.then方法: (onResolved, onRejected) => {}

(1) onResolved函数: 成功的回调函数 (value) => {}

(2) onRejected 函数: 失败的回调函数(reason) => {}

Note:

指定用于得到成功value的成功回调和用于得到失败 reason的失败回调,返回一个新的 promise对象

3、Promise.prototype.catch 方法:(onRejected) => {}

(1) onRejected 函数: 失败的回调函数(reason) => {}

Note:

then()的语法糖, 相当于: then(undefined, onRejected)

4、Promise.resolve方法: (value) => {}

(1)value: 成功的数据或promise对象

Note:

如果传入的参数为 非Promise类型的对象, 则返回的结果默认为成功的promise对象

如果传入的参数为 Promise对象, 则参数的结果决定了 resolve 的结果;

let p1 = Promise.resolve(123)
console.log('p1', p1)
​
let p2 = Promise.resolve(
  new Promise((resolve, reject) => {
    // resolve('OK')
    reject('Error')
  })
)
console.log('p2', p2)
p2.catch((reason) => {
  console.log(reason)
})
复制代码

得到:

图片.png

5、Promise.reject 方法: (reason) => {}

(1) reason: 失败的原因

说明:

返回一个失败promise 对象,注意和第四点Promise.resolve方法的区别

let p1 = Promise.reject(123)
let p2 = Promise.reject('abc')
let p3 = Promise.reject(
  new Promise((resolve, reject) => {
    resolve('OK')
  })
)
​
console.log(p1)
console.log(p2)
console.log(p3)
复制代码

得到:

图片.png

6、Promise.all方法:(promises) => {}

(1) promises: 包含 n 个 promise的数组

Note:

返回一个新的promise, 只有所有的 promise 都成功才成功;

a、都成功的情况

let p1 = new Promise((resolve, reject) => {
  resolve('OK')
})
let p2 = Promise.resolve('Success')
let p2 = Promise.resolve('Oh Yeah')
​
const result = Promise.all([p1, p2, p3])
console.log(result)
复制代码

得到:

图片.png

b、有失败的情况

Note:

都成功的话就返回数组,有失败的情况结果还是,以第一个失败的值为准

let p1 = new Promise((resolve, reject) => {
  console.log(1111111)
  resolve('OK')
})
let p2 = new Promise((resolve, reject) => {
  console.log(222222)
  reject('失败了2222')
})
​
let p3 = new Promise((resolve, reject) => {
  console.log(3333333)
  reject('失败了3333')
})
const result = Promise.all([p1, p2, p3])
console.log(result)
复制代码

得到:

图片.png

7、Promise.race 方法: (promises) => {}

(1) promises: 包含 n 个 promise 的数组

Note:

返回一个新的promise, 第一个完成的 promise 的结果状态就是最终的结果状态

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK')
  }, 1000)
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Oh Yeah')
​
//调用
const result = Promise.race([p1, p2, p3])
console.log(result)
复制代码

得到:

图片.png

3.2、promise 的几个关键问题

1、 如何改变promise 的状态?

(1) resolve(value): 如果当前是 pending 就会变为 resolved

(2) reject(reason): 如果当前是 pending 就会变为rejected

(3) 抛出异常: 如果当前是 pending就会变为rejected

let p = new Promise((resolve, reject) => {
  //1. resolve 函数
  // resolve('ok') // pending   => fulfilled (resolved)
​
  //2. reject 函数
  // reject('error') // pending  =>  rejected
  
  //3. 抛出错误
  throw '出问题了' // pending  =>  rejected
})
​
console.log(p)
复制代码

Note:

状态改变后then、catch才会执行;

2、一个promise指定多个成功/失败回调函数, 都会调用吗?

Note:

promise 改变为对应状态时都会调用,如下:

let p = new Promise((resolve, reject) => {
  resolve('OK')
})
​
///指定回调 - 1
p.then((value) => {
  console.log(111, value)
})
​
//指定回调 - 2
p.then((value) => {
  console.log(222, value)
})
复制代码

得到:

图片.png

3、改变 promise状态(上面三种方式)和指定回调函数(then)谁先谁后?

(1) 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调

(2) 如何先改状态再指定回调?

① 在执行器中直接调用 resolve()/reject()

② 延迟更长时间才调用then()

(3) 什么时候才能得到数据?

① 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据

② 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据

说简单点

其实就是同步代码、微任务、宏任务的执行顺序区别;

传送门:JS的事件执行机制

3.1、当Promise内部改变状态的操作(resolve, reject, throw),不包含在操作异步里面时

let p = new Promise((resolve, reject) => {
  console.log(1) // 同步
  resolve('OK') // 微任务
  console.log(22) // 同步
})
​
p.then(
  (value) => {
    console.log(value)
  },
  (reason) => {}
)
复制代码

得到:

图片.png

3.2、当Promise内部改变状态的操作(resolve, reject, throw),包含在操作异步里面时

let p = new Promise((resolve, reject) => {
  console.log(111) // 同步
  setTimeout(() => { // 异步宏任务
    console.log(222) // 异步宏任务 --- 同步任务
    resolve('OK') // 异步宏任务 --- 微任务
    console.log(333) // 异步宏任务 --- 同步任务
  }, 1000)
  console.log(444) // 同步
})
​
p.then(
  (value) => {
    console.log(value)
  },
  (reason) => {}
)
复制代码

得到:

图片.png

4、promise.then()返回的新promise的结果状态由什么决定?

(1) 简单表达:

then()指定的回调函数执行的结果决定

(2) 详细表达:

① 如果抛出异常, 新promise 的状态变为rejected

② 如果返回的是非 promise的任意值, 新 promise的状态 自动默认为 resolved

③ 如果返回的是另一个新 promise,此 promise 的结果就会成为新promise的结果

let p = new Promise((resolve, reject) => {
  resolve('ok')
})
//执行 then 方法
let result = p.then(
  (value) => {
    // console.log(value);
    //1. 抛出错误------失败的状态reject
    // throw '出了问题'
​
    //2. 返回结果是非 Promise 类型的对象------自动变为成功
    // return 521
​
    //3. 返回结果是 Promise 对象----以新返回的为准
    return new Promise((resolve, reject) => {
      // resolve('success');
      reject('error')
    })
  },
  (reason) => {
    console.log('注意一下,前面的失败和我这里毫无关系')
    console.warn(reason)
  }
)
console.log(result)
复制代码

5、promise如何串连多个操作任务?

(1) promisethen()返回一个新的 promise, 可以写成成then()的链式调用

(2) 通过then的链式调用串连多个同步/异步任务

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK')
  }, 1000)
})
​
p.then((value) => {
  console.log(value) // 一秒钟之后打印 Ok
  return new Promise((resolve, reject) => {
    resolve('success')
  })
})
  .then((value) => { // 前面的then返回一个新的 promise, 可以开成then()的链式调用
    console.log(value) // success
  })
  .then((value) => { // 前面的then无返回值
    console.log(value) // undefined  如果前面无返回值,即默认返回undefined
  })
复制代码

6、 promise 异常穿透?

(1) 当使用 promisethen链式调用时, 可以在最后指定失败的回调,

(2) 前面任何操作出了异常, 都会传到最后失败的回调中处理

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK')
    // reject('Err')
  }, 1000)
})
​
p.then((value) => {
  console.log(value)
  throw '失败啦!'
})
  .then((value) => {
    console.log(222)
  })
  .then((value) => {
    console.log(333)
  })
  .catch((reason) => {
    console.log('执行了catch')
    console.log(reason)
  })
复制代码

得到:

图片.png

7、中断promise链 ?

(1) 当使用promisethen链式调用时, 在中间中断, 不再调用后面的回调函数

(2) 办法有且只有一个: 在回调函数中返回一个 pendding 状态的promise 对象

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK')
  }, 1000)
})
​
p.then((value) => {
  console.log(111) // 只会打印 111
  //有且只有一个方式: 返回pending,中断promise链 (前面说过,只有状态改变了才会执行)
  return new Promise(() => {}) 
})
  .then((value) => {
    console.log(222) // 已经中断,不会打印
  })
  .then((value) => {
    console.log(333) // 已经中断,不会打印
  })
  .catch((reason) => {
    console.warn(reason) // 已经中断且无报错,不会打印
  })
复制代码

二、Async / Await

1、知识点:

a、async 关键字在函数声明前使用,例如:async function test(){}

b、async 函数总是返回一个Promise 对象,不论函数是否 return Promise对象,例如:

async function main() {
  //1. 如果返回值是一个非Promise类型的数据
  return 123
}
​
let result = main()
console.log(result) // 实际上是一个promise对象
复制代码

得到:

图片.png

async function main() {
  //2. 如果返回的是一个Promise对象
  return new Promise((resolve, reject) => {
    resolve('OK');
  })
}
复制代码

得到:

图片.png

async function main() {
  //3. 抛出异常
  throw 'Oh NO'
}
​
let result = main()
console.log(result)
复制代码

得到:

图片.png

c、await 只能用在 async 函数中,用于其它地方会直接报错

d、async/awaitPrimose对象在本质上是一样的

e、async/await作用是用同步方式,执行异步操作

f、如果awaitpromise失败了, 就会抛出异常, 需要通过try...catch捕获处理

2、解决了什么问题?

Promise.then语法已经解决了以前一直存在的多层回调嵌套的问题,那问什么还要用async/await呢?先来看一段Promise代码:

function login () {
  return new Promise(resolve => {
    resolve('aaaa')
  })
}
function getUserInfo (token) {
  return new Promise(resolve => {
    if (token) {
      resolve({
        isVip: true
      })
    }
  })
}
function getVipGoods (userInfo) {
  return new Promise(resolve => {
    if (userInfo.isVip) {
      resolve({
        id: 'xxx',
        price: 'xxx'
      })
    }
  })
}
function showVipGoods (vipGoods) {
  console.log(vipGoods.id + '----' + vipGoods.price)
}
login()
  .then(token => getUserInfo(token))
  .then(userInfo => getVipGoods(userInfo))
  .then(vipGoods => showVipGoods(vipGoods))
复制代码

如例子所示,每一个Promise相当于一个异步的网络请求,通常一个业务流程需要多个网络请求,而且网络请求网络请求都依赖一个的请求结果,上例就是Promise模拟了这个过程,再来看看用async/await会有什么不同:

async function call() {
  const token = await login()
  const userInfo = await getUserInfo(token)
  const vipGoods = await getVipGoods(userInfo)
  showVipGoods(vipGoods)
}
call()
复制代码

Promisethen链调用相比,async/await的调用更加清晰简单,和同步代码一样

3、值得注意的地方

使用async/await我们经常会忽略一个问题,同步执行带来的时间累加,有时候我们的代码可以写成并发执行,但是由于async/await做成了继发执行,来看一个例子:

function test () {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('test')
      resolve()
    }, 1000)
  })
}
function test1 () {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('test1')
      resolve()
    }, 1000)
  })
}
function test2 () {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('test2')
      resolve()
    }, 1000)
  })
}
async function call () {
  await test()
  await test1()
  await test2()
}
call ()   // 大概需要花3000ms。。。
复制代码

实际上,如果这段代码执行顺序我并不关心,继发执行就浪费大量执行时间,下面改成并发执行:

function call () {
  Promise.all([test(), test1(), test2()])
}
call() // 大概1000ms就好了,并行执行。。。
复制代码

4、看个栗子?

// ----------------------------------------------------案例一
async function async() {
  return { a: 123, b: 456 }
}
​
async function testFunc() {
  console.log('顺序1')
​
  const promise = async()
  console.log('顺序2---async函数默认返回promise对象', promise)
​
  const value = await async() // 相当于使用then包含后面的所有操作,见下
  console.log('顺序3', value)
​
  console.log('顺序4')
}
​
testFunc()
​
// ----------------------------------------------------案例二
async function async() {
  return { a: 123, b: 456 }
}
​
async function testFunc() {
  console.log('顺序1')
​
  const promise = async()
  console.log('顺序2---async函数默认返回promise对象', promise)
​
  Promise.resolve(async()).then((val) => {
    const value = val
    console.log('顺序3', value)
​
    console.log('顺序4')
  })
}
​
testFunc()
复制代码

都会得到一样的结果:

图片.png

5、自己怎么实现 async/await 的原理?

因为async/await本身就是promise+generator语法糖,使用ES6里的迭代函数generator也可以实现一样的效果,具体细节,可以看一下大佬的这篇文章:

7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?

🚀🚀🚀

建议: 看完这篇文章后,再看一下这两篇文章会更香!

JS的事件执行机制

setTimeout+Promise+Async输出顺序?很简单呀!

分类:
前端
收藏成功!
已添加到「」, 点击更改