JavaScript异步

不羁的心 提交于 2020-12-01 01:49:57

 JavaScript异步类型

  • 延迟类型:setTimeout、setInterval、setImmediate
  • 监听事件:监听 new Image 加载状态、监听 script 加载状态、监听 iframe 加载状态、Message
  • 带有异步功能类型: Promise、ajax、Worker、async/await

需要说明的是,在 ES6 之前,JavaScript 语言本身没有异步,延迟类型、监听类型的异步都是由宿主提供的,并非语言的核心部分。

JavaScript常用异步编程

Promise

Promise 对象用于表示一个异步操作的最终状态,及结果值。

  Promise有几个特点:

  1. 对象的状态不受外界影响,有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。只有异步操作的结果可以决定当前是哪种状态,其他操作无法改变。
  2. 状态一旦改变,就不会再变,任何时候都可以得到这个结果。状态改变只可能是:pending -> fulfilled 或 pending -> rejected
  3. 实例化后,会立即执行一次。所以一般将其用函数包裹起来,使用的时候调用一次。
  4. 如果执行后的回调也要做一些异步操作,可以无限的.then下去,当然要保证有返回值

  方法:

  • 对象方法 reject、resolve、all、race、allSettled(ES2020)
  • 原型方法 then、catch、finally(ES9)
function promiseTest(n,msg) {
   return new Promise((resolve,reject)=>{
      setTimeout(function () {
         console.log(`执行第${n}个任务`);
         msg.code && resolve(msg.text);  // 当认为成功的时候,调用resolve函数
         !msg.code && reject(msg.text);   // 当认为失败的时候,调用reject函数
      },n*500)
   });
}
let pro = promiseTest(1,{code:true,text:"返回的数据1"});
/* 没有catch,每个then里两个回调函数,此时第一个为成功的回调,第二个为失败的回调 */
pro.then((data)=>{
   console.log(data); // 执行成功结果在这里
   // return promiseTest(2,{code:true,text:"返回的数据2"});
   return promiseTest(2,{code:false,text:"失败的数据"});
},(err)=>{
   console.log(err); // 执行失败的结果在这里
}).then((data)=>{console.log(data)},(err)=>{console.log(err)});

观察 then 和 catch 的用法:

  • 在多次 then 后最后跟一个 catch,可以捕获所有的异常
/* 多个then和一个catch */
pro.then((data)=>{
   console.log(data);
   return promiseTest(2,{code:false,text:"失败的数据"});
}).then((data)=>{
   console.log(data)
}).catch((err,data)=>{
   console.log("失败了",err);
});

all 和 rece 的用法:(这两个方法都是将若干个 Promise 实例,包装成一个新的 Promise 实例)

  • all 接收一个 promise 对象列表.在所有异步操作执行完且全部成功的时候才执行 then 回调,只要有一个失败,就执行 catch 回调(只对第一个失败的promise 对象执行)。
  • race 也接收一个 promise 对象列表,不同的是,哪个最先执行完,对应的那个对象就执行 then 或 catch 方法( then 或 catch 只执行一次)。
/* all的用法 */
Promise.all([
   promiseTest(1,{code:true,text:"返回的数据1"}),
   promiseTest(2,{code:false,text:"返回的数据2"}),
   promiseTest(3,{code:false,text:"返回的数据3"})
]).then((res)=>{console.log("全部成功",res)}).catch((err)=>{console.log("失败",err);});

/* race的用法 */
Promise.race([
   promiseTest(1,{code:false,text:"返回的数据1"}),
   promiseTest(2,{code:false,text:"返回的数据2"}),
   promiseTest(3,{code:true,text:"返回的数据3"})
]).then((res)=>{console.log("成功",res)}).catch((err)=>{console.log("失败",err);});

Generator

Generator 叫做生成器,通过 function* 关键字来定义的函数称之为生成器函数(generator function),它总是返回一个 Generator 对象。生成器函数在执行时能暂停,又能从暂停处继续执行。调用一个生成器并不会立马开始执行里面的语句,而是返回这个生成器的 迭代对象( iterator )。

Generator 对象有3个方法,都有一样的返回值 { value, done } 【与 Python 生成器的用法一样】

  • .next(value)   返回一个由yield表达式生成的值。(value 为向生成器传递的值)
  • .return(value) 该方法返回给定的值并结束生成器。(value 为需要返回的值)
  • .throw(exception)  该方法用来向生成器抛出异常,并恢复生成器的执行。(exception 用于抛出的异常)

生成器的作用:

可以和 Promise 组合使用。减少代码量,写起来更方便。在没有 Generator 时,写 Promise 会需要很多的 then,每个 then 内都有不同的处理逻辑。现在,我们将所有的逻辑写进一个生成器函数(或者在生成器函数内用 yield 进行函数调用),Promise 的每个 then 内调用同一个函数即可。

定义生成器:

function add(a,b) {
   console.log("+");
   return a+b;
}
function cut(a,b) {
   console.log("-");
   return a-b;
}
function mul(a,b) {
   console.log("*");
   return a*b;
}
function division(a,b) {
   console.log("/");
   return a/b;
}
function* compute(a, b) {
  yield add(a,b);
  yield cut(a,b);
  let value = yield mul(a,b);
  console.log("value",value); // 第三次调用.next()时无法为value赋值,需要第四次调用才能为其赋值
  yield mul(a,b);
  yield division(a,b);
}

使用生成器:

// 执行一下这个函数得到 Generator 实例,调用next()方法执行,遇到yield暂停
let generator = compute(4, 2);

function promise() {
   return new Promise((resolve, reject) => {
      let res = generator.next();
      if(res.value > 5)
      {
         resolve("OK");
      }else
      {
         reject("小于5")
      }
   });
}

let proObj = promise();
proObj.then((data)=>{
   console.log(data);
   let res = generator.next();
   console.log("Promise res1",res);
}).then((data)=>{
   let res = generator.next();
   // let res = generator.return();
   console.log("Promise res2",res);
}).then((data)=>{
   let res = generator.next("qwe"); // 第四次next()时,向生成器传数据
   console.log("Promise res3",res)
}).catch((err)=>{
   console.log("出错",err);
});

Generator 函数的特点:

  • 最大特点就是可以交出函数的执行权(暂停执行)。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
  • 可以将 yield 关键字使得生成器函数可以与外接交流:可以将内部的值传到外界,也可以将外接的值传入

yield 和 yield* :

  • 生成器函数在执行过程中,遇到 yield 会暂停执行,并返回一个值
  • yield* 表达式用于委托给另一个 generator 函数(即可以将当前生成器函数的执行权交给另一个生成器函数)或 可迭代对象 
function* g1() {
  yield 2;
  yield 3;
  yield 4;
}

function* g2() {
  yield 1;
  yield* g1();
  yield 5;
  yield* ["a",  "b"];
  yield* "cd";
}

var iterator = g2();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: "a", done: false }
console.log(iterator.next()); // { value: "b", done: false }
console.log(iterator.next()); // { value: "c", done: false }
console.log(iterator.next()); // { value: "d", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

async/await

优点:简洁,节约了不少代码

  • async 函数就是 Generator 函数的语法糖。要将 Generator 函数转换成 async 函数,只需将 * 替换成 async ,yield 替换成 await 即可
  • 被 async 修饰的函数,总会返回一个 Promise 对象。如果代码中返回值不是 promise 或者没有返回值,也会被包装成 promise 对象
  • await 只能在 async 函数内使用。它是一个操作符,等待一个函数或表达式。经过该操作符处理后,输出一个值。

如果在异步函数中,每个任务都需要上个任务的返回结果,可以这么做:

function takeLongTime(n) {
   return new Promise((resolve,reject) => {
      setTimeout(() => {resolve(n + 200)}, n);
   });
}

function step1(n) {
   console.log(`step1 with ${n}`);
   return takeLongTime(n);
}

function step2(m, n) {
   console.log(`step2 with ${m} and ${n}`);
   return takeLongTime(m + n);
}

function step3(k, m, n) {
   console.log(`step3 with ${k}, ${m} and ${n}`);
   return takeLongTime(k + m + n);
}


async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

如果这几个任务没有关联,可以这样做:

async function doIt() {  // 函数执行耗时2100ms
   console.time("doIt");
   await step1(300).catch((err)=>{console.log(err)});  // 异常处理
   await step1(800);
   await step1(1000);
   console.timeEnd("doIt");
}
doIt();

当然,最好这样做:

async function doIt() { // 函数执行耗时1000ms
   console.time("doIt");
   const time1Pro = step1(300);
   const time2Pro = step1(800);
   const time3Pro = step1(1000);
   await time1Pro;
   await time2Pro;
   await time3Pro;
   console.timeEnd("doIt");
}
或
async function doIt() { // 函数执行耗时1000ms
   console.time("doIt");
   const [ time1Pro, time2Pro, time3Pro ] = await Promise.all([step1(300), step1(800), step1(1000)])
   console.timeEnd("doIt");
}
doIt();

注意:

  1. async/await 并没有脱离 Promise,它的出现能够更好地协同 Promise 工作。
    • 怎么体现更好地协同?它替代了then catch的写法。使得等待 promise 值的操作更优雅,更容易阅读和书写。
  2. 函数仅仅加上 async 并没有意义,它仍然是同步函数,只有与 await 结合使用,它才会变成异步函数。
    • 这需要精准理解 await。它在等待的时候并没有阻塞程序,此函数也不占用 CPU 资源,使得整个函数做到了异步执行。当 async 函数在执行的时候,第一个 await 之前的代码都是同步执行的。
  3. doIt() 函数内部是串行执行的,但它本身是异步函数。
  4. 在这个异步函数内,可能会做很多操作 ABC,他们有执行的先后顺序。这时你可能会想,A、B、C之间没有关联,他们之间可以是并行执行的,并不需要串行,那怎么办?
    • 【错误想法】这样想没错,但是没必要。因为他们已经存在于异步函数内了,所有的操作已经是异步的。在同样的环境情景下,底层执行的效率是相同的,并不见得因为A和B之间互相异步而提高效率。
    • 【正确想法】这样想是有必要的。参照两个 doIt() ,调用的函数返回 promise 对象,前者是依次生成 promise 对象(依次执行任务),依次等待返回结果。等待总时长取决于所有任务执行时间之和。后者则是同时生成 promise 对象(同时执行任务),依次等待。等待总时长取决于耗时最长的任务。后者的 CPU 运用率更高。
  5. async 函数内任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。为了不中断后面的操作,我们可以将 await 语句放在 try ... catch 结构内,或者在 await 后面的 Promise 对象跟一个 catch 方法。
  6. 错误处理。最标准的方法是使用 try...catch 语句,但是它不仅会捕捉到 promise 的异常,还会将所有出现的异常捕获。因此,可以使用 .catch ,只会捕获 promise 相关的异常。

关于错误处理,可以这样做:

function takeLongTime(n) {
   return new Promise((resolve,reject) => {
      setTimeout(() => {resolve(n + 200)}, n);
   }).then(data=>[data,null]).catch(err=>[null,err]);
}

async doIt(){
    let [data, err] = await takeLongTime(1000);
  console.log(data, err);  
}

另外,async函数有多种使用形式:

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};
const foo = async () => {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

异步生成器函数

即异步函数和生成器函数的结合体:async function*() {}。它就是 Generator 和 async-await 的完美结合,支持两者的用法和特性。

以前我以为,async-await 可以完全代替 Generator ,但其实不然,前者的优点在于更优雅地处理异步操作,后者能够支持函数内外进行数据交流。

异步生成器函数会返回一个异步迭代器,这个异步迭代器有两种使用方式:

  1. 通过 for await of 遍历得到值,非常方便
  2. 通过循环 .next() 得到

两种方式又有不同:

  1. 前者不能得到异步生成器内 return 的值,后者可以
  2. 前者不能给 yield 传值,后者可以通过 .next() 方法传值
  3. 除此之外,可以将后者看成前者的手动实现

如何进一步理解异步生成器呢?其实可以看成是为异步函数提供了一种异步返回、多次返回的机制。在非异步生成器函数中,return 只能有一个,且是函数结束的标志。而异步生成器函数就可以做到:间断地返回多个值,不同的返回值之间可以有同步操作也可以有异步操作。这正是集 Generator 和 async-await 的优点于一身,有利于解耦,有利于逻辑的分离。

关于异步迭代器的遍历顺序:完全按照 yield 的顺序来,没有变化。不会因为哪个耗时短而改变顺序。await 也是一样,多个 await 相互之间的顺序是固定的,无法调整,在这里只能串行执行。

关于性能:对于 ES6(+) 本身来说,以上所有的异步方式性能都 OK,但在真实的生产环境中都要由 babel 编译成 ES5 语法,结果会导致代码体积增加,执行过程中会执行另外一段代码,总体性能会低一些。

实验代码:

const asyncFunc1 = () => new Promise((resolve, reject) => {
    setTimeout(() => { resolve("async-1") }, 1000);
});

const asyncFunc2 = () => new Promise((resolve, reject) => {
    setTimeout(() => { resolve("async-2") }, 1500);
});

const asyncGenerator = async function* () {
    const promise1 = asyncFunc1(); // 1000ms
    const promise2 = asyncFunc2(); // 1500ms
    const res1 = await promise1;
    const res2 = await promise2;
    yield res1
    yield res2;
    
    // const a = yield res1;
    // const b = yield res2;
    return "这是异步生成器返回值";
};

const iter = asyncGenerator();
const array = [];

/* 通过 for await of 遍历 */
(async () => {
    console.time("记时");
    for await (const i of iter) {
        array.push(i);
        console.timeLog("记时");
        console.log("遍历", i);
    }
    console.timeEnd("记时");
    console.log("遍历结果", array);
})()

/* 通过循环 .next() 获得 */
// (async() => {
//     console.log("手动循环.next()循环")
//     while(true) {
//         const next = iter.next("next传值");
//         console.log("得到next", next);
//         const { value, done } = await next;
//         console.log(value, done);
//         if (done) break;
//     }
// })()

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!