likes
comments
collection
share

手把手带你掌握迭代器、生成器和异步函数,以及优雅的异步处理方案 -- async/await

作者站长头像
站长
· 阅读数 77

概述

为什么要将迭代器、生成器和异步放在一块来讲述呢?因为他们存在一定的联系。WTF,这不是废话吗?对的,但,是也不是。接下来我们一个个来看吧

迭代器

什么是迭代器呢?迭代器是一种能够在容器对象上(例如数组或链表等)遍历数据集合的对象。迭代器对象提供了一种统一的方式来访问数据集合中的元素,而无需暴露其底层表示。通过迭代器,可以一次访问集合中的每一个元素,而不需要知道其中元素的个数或其底层存储方式。迭代器通常使用 next() 方法来访问集合中的下一个元素,并返回一个包含元素值和是否还有更多元素的对象。 next方法有如下要求:

  • 一个无参数或者一个参数函数,返回一个应当拥有以下两个属性的对象
    1. done(boolean)
      • 如果迭代器可以产生下一个值,则为false。(这等价于没有指定done这个属性。)
      • 如果迭代器已将序列迭代完毕,则为true。这种情况下,value是可选的,如果他依然存在,即为迭代过后的默认返回值(undefined)
    2. value 迭代器返回的任何JS值,done为true时可省略。 怎么样,到这里的话,我们是不是对迭代器已经有了大致的了解。但这些理论还是有些让人头大对不对?没关系,让我们一起看一个例子就能有更加深刻的理解了。看代码吧
// 封装一个生成迭代器对象的函数
function createArrayIterator(arr) {
  let indexNum = 0
  // 1.首先,按照定义,迭代器是一个对象
  const arrayIterator = {
    // 2. 其次,这个对象必须实现一个next方法(接收0或1个参数)
    next: function() {
        // 3. 然后,这个方法必须返回一个对象
        // 这个对象包含两个参数: 
        // done --> boolean
        // value --> 具体的值或undefined
        if(indexNum < arr.length)
          return { done: false, value: arr[indexNum++] }
        else
          // 此时value可省略
          return { done: true, value: undefined }
    }
  }
  return arrayIterator 
}
const nums = [3, 2, 9]
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next()) // { done: false, value: 3}
console.log(numsIterator.next()) // { done: false, value: 2}
console.log(numsIterator.next()) // { done: false, value: 9}
console.log(numsIterator.next()) // { done: true, value: undefined}

到这里应该完全理解了吧! 接下来,我们一起来看一下可迭代对象

可迭代对象

什么是可迭代对象呢?它和迭代器是不同的概念,不过也与其有联系。当一个对象实现了iterable protocol时,它就是一个可迭代对象。具体就是这个对象必须实现@@iterator方法,在代码中使用Symbol.iterator属性来访问,该属性返回一个迭代器对象。 这样说是不是还是有些抽象,老规矩,看看代码就懂了

// 将infos变成一个可迭代对象
// 1.首先,按照定义,可迭代对象必须是一个对象。WTF???哈哈,没错,废话文学也要用起来
const info = {
  friends: ['Kobe', 'Messi', 'Taylor'],
  // 2.其次,这个对象必须拥有一个Symbol.iterator属性。
  // 3.再者,这个属性对应一个方法,这个方法就叫作@@iterator方法
  [Symbol.iterator]: function() {
	let index = 0
	// 4.最后,这个方法必须返回一个迭代器对象
	return {
	  // 5.接下来就是迭代器对象那一番操作了
	  next: () => {
	  	 if(index < this.friends.length)
	  	 	return { done: false, value: this.friends[index++] }
	  	 else
	  	 	return { done: true }
	  }
    }
  }  
}

const infoIterator = info[Symbol.iterator]()
console.log(infoIterator.next()) // { done: false, value: 'Kobe' }
console.log(infoIterator.next()) // { done: false, value: 'Messi' }
console.log(infoIterator.next()) // { done: false, value: 'Taylor' }
console.log(infoIterator.next()) // { done: true }

怎么样,迭代器与可迭代对象是不是都很简单。那么接下来一起来看看生成器吧

生成器

在了解生成器之前,让我们先来了解一下生成器函数 那什么是生成器函数呢?生成器函数也是一个函数,但是它和普通的函数存在以下区别:

  1. 首先,生成器函数需要在function的后面加一个符号: *
  2. 其次,生成器函数可以通过yield关键字来控制函数的执行流程
  3. 最后,生成器函数的返回值是一个Generator(生成器)
    • 事实上,生成器是一种特殊的迭代器
    • 要想执行生成器函数内部的代码,需要生成器对象调用他的next方法
    • 当遇到yield时,就会中断执行

生成器函数和生成器对象的基本使用

function* countNumbers() {
  let i = 0;
  while (i < 3) {
    yield i
    i++
  }
}

const generatorObj = countNumbers()
console.log(generatorObj.next()) // { value: 0, done: false }
console.log(generatorObj.next()) // { value: 1, done: false }
console.log(generatorObj.next()) // { value: 2, done: false }
console.log(generatorObj.next()) // { value: undefined, done: true }

生成器函数返回值和参数以及生成器提前结束

// 1.定义了一个生成器函数
function* foo(name1) {
  console.log("执行内部代码:1111", name1)
  console.log("执行内部代码:2222", name1)
  const name2 = yield "aaaa"
  console.log("执行内部代码:3333", name2)
  console.log("执行内部代码:4444", name2)
  const name3 = yield "bbbb"
  // return "bbbb"
  console.log("执行内部代码:5555", name3)
  console.log("执行内部代码:6666", name3)
  yield "cccc"
  return undefined
}

// 2.调用生成器函数, 返回一个 生成器对象
const generator = foo("next1")
// 调用next方法
// console.log(generator.next()) // { done: false, value: "aaaa" }
// console.log(generator.next()) // { done: false, value: "bbbb" }
// console.log(generator.next()) //  { done: false, value: "cccc" }
// console.log(generator.next()) // {done: true, value: undefined}

// 3.在中间位置直接return, 结果
// console.log(generator.next()) // { done: false, value: "aaaa" }
// console.log(generator.next()) // { done: true, value: "bbbb" }
// console.log(generator.next()) // { done: true, value: undefined }
// console.log(generator.next()) // { done: true, value: undefined }
// console.log(generator.next()) // { done: true, value: undefined }
// console.log(generator.next()) // { done: true, value: undefined }

// 4.给函数每次执行的时候, 传入参数
// 执行内部代码:1111 next1
// 执行内部代码:2222 next1
// {value: 'aaaa', done: false}
console.log(generator.next())
// 执行内部代码:3333 next2
// 执行内部代码:4444 next2
// {value: 'bbbb', done: false}
console.log(generator.next("next2"))
// 执行内部代码:5555 next3
// 执行内部代码:6666 next3
// {value: 'cccc', done: false}
console.log(generator.next("next3"))
// {value: undefined, done: true}
console.log(generator.next())

生成器抛出异常 -- throw函数

生成器函数可以通过throw语句抛出异常,该语句可以将异常对象传递给生成器函数,生成器函数会在yield表达式处暂停并且将控制权交给调用方。在调用方中,可以使用try...catch语句捕获这个异常,也可以让异常继续向上传递。 tips:

  1. 抛出异常后可以在生成器函数中捕获异常
  2. 但是在catch语句中不能再yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行
function* myGenerator() {
  try {
    yield 1
    yield 2
    throw new Error("Oops! Something went wrong.")
    yield 3
  } catch (e) {
    console.log(e.message)
    yield 4
  }
  yield 5
}

const g = myGenerator()

console.log(g.next().value) // 1
console.log(g.next().value) // 2
// Exception thrown by caller. { value: 4, done: false }
console.log(g.throw(new Error("Exception thrown by caller.")))
console.log(g.next().value) // 5
console.log(g.next()) // { value: undefined, done: true }

yield* 生产一个可迭代对象

可以用yield* 生产一个可迭代对象,这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值

function* createArrayIterator(arr) {
  yield* arr
}
const arr = [3, 1, 2]
const gen = createArrayIterator(arr)
console.log(gen.next()) // { value: 3, done: false }
console.log(gen.next()) // { value: 1, done: false }
console.log(gen.next()) // { value: 2, done: false }
console.log(gen.next()) // { value: undefined, done: true }

生成器替代迭代器的应用场景

对迭代器代码进行重构

function* createArrayIterator(arr) {
  for(let i = 0; i < arr.length; i++) {
	yield arr[i]
  }
}
const nums = [2,3,4]
const gen = createArrayIterator(nums)
console.log(gen.next()) // { value: 2, done: false }
console.log(gen.next()) // { value: 3, done: false }
console.log(gen.next()) // { value: 4, done: false }
console.log(gen.next()) // { value: undefined, done: true }

生成某个范围内的值

function* createRangeGenerator(start, end) {
  for(let i = start; i < end; i++) {
	yield i
  }
}
const rangeGen = createRangeGenerator(6, 9)
console.log(rangeGen.next()) // { value: 6, done: false }
console.log(rangeGen.next()) // { value: 7, done: false }
console.log(rangeGen.next()) // { value: 8, done: false }
console.log(rangeGen.next()) // { value: undefined, done: true }

异步函数

异步

首先,什么是异步呢?异步是指一种非阻塞的编程方式,让代码在执行事件或网络操作时,不会阻塞主线程的其他任务。通常在JS中,网络请求和事件处理都是异步的。常见的异步编程方式包括回调函数、Promise、async/await等。

异步函数

那什么是异步函数呢?JS中,用关键字async声明的函数就称之为异步函数。 异步函数可以有多种形式,但核心是必须使用关键字async进行声明,需要注意的是,异步函数内的代码仍是同步的,也就是从上至下一次执行。如果遇到其他异步函数就要具体问题具体分析了,可能会涉及到微任务、宏任务、事件队列、事件循环等,这个以后我们结合Promise,async/await等知识再单独开一期唠唠。

async foo1() {}
const foo2 = async function() {}
const foo3 = async () => {}
class Foo {
  async foo() {}
}

异步函数的返回值

异步函数内的代码会同步执行,这点和普通函数是一致的。 但异步函数有返回值时,和普通函数会有区别:

  1. 异步函数的返回值相当于被包裹到Promise.resolve()中
  2. 如果异步函数的返回值是Promise,状态将会由新的Promise决定
  3. 如果异步函数的返回值是一个对象,并且实现了thenable,那么状态就会由对象的then方法来决定
// 返回值的区别
// 1.普通函数
// function foo1() {
//   return 123
// }
// foo1() // 123

// 2.异步函数
async function foo2() {
  // 1.返回一个普通的值
  // -> Promise.resolve(321)    // then中得到 321
  return ["abc", "cba", "nba"]

  // 2.返回一个Promise
  // return new Promise((resolve, reject) => {
  //   setTimeout(() => {
  //     resolve("aaa")  // then中得到 "aaa"
  //   }, 3000)
  // })

  // 3.返回一个thenable对象
  // return {
  //   then: function(resolve, reject) {
  //     resolve("bbb") // then中得到 "bbb"
  //   }
  // }
}

foo2().then(res => {
  console.log("res:", res) // res: (3) ['abc', 'cba', 'nba']
})

异步函数的异常

还有一点需要注意的是,在async中抛出了异常,程序并不会像普通函数一样报错,而是会作为Promise的reject来传递。

// 如果异步函数中有抛出异常(产生了错误), 这个异常不会被浏览器立即处理
// 会进行如下处理: Promise.reject(error)
async function foo() {
  console.log("---------1");  // 1
  console.log("---------2");  // 2
  // "abc".filter();
  throw new Error("async function error"); 
  // 以下的代码都不再执行了
  console.log("---------3");

  // return new Promise((resolve, reject) => {
  //   reject("err rejected")
  // })

  return 123;
}

// promise -> pending -> fulfilled/rejected
foo()
  .then((res) => {
    console.log("res:", res);
  })
  .catch((err) => {
    console.log("err:", err); // err: Error: async function error
    console.log("继续执行其他的逻辑代码");
  })

async与await结合使用

异步函数的另一个特殊之处在于可以在它的内部使用await关键字,而普通函数中是不可以的。 那await关键字有什么特点呢?

  1. 通常await后面会跟上一个表达式,这个表达式会返回一个Promise
  2. await会等到Promise的状态变成fulfilled之后继续执行异步函数

tip:await使用条件: 必须在异步函数中使用,在同步函数中使用会报错,终止程序执行。

function bar() {
  console.log("bar function")
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(123)
    }, 3000)
  })
}

async function foo() {
  console.log("-------")
  // await后续返回一个Promise
  // 会等待Promise有结果之后, 才继续执行后续的代码
  const res1 = await bar()
  console.log("await后面的代码:", res1)
  const res2 = await bar()
  console.log("await后面的代码:", res2)

  console.log("+++++++")
}

// -------
// bar function
// 3s后输出
// await后面的代码: 123
// bar function
// 6s后再输出
// await后面的代码: 123
// +++++++
foo()

异步处理方案优化

说到这里,不知道你有没有一些疑惑?生成器与async/await与我们要说的异步处理方案的优化有什么关系呢? 别着急,让我们先来看一下传统的异步处理方案和基于Promise的处理方案,然后再看一下基于生成器的处理方案和基于async/await的处理方案,我们就能知道他们的优缺点了。

// 封装请求的方法: url -> promise(result)
function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url)
    }, 2000)
  })
}

 /*
 需求: 
    1.向服务器发送三次网络请求获取数据
    2.第二次请求依赖第一次请求的结果
    3.第三次请求依赖第二次请求的结果
    4.第三次请求结束后才能得到最终想要的结果
*/

1. 传统的异步处理方案:导致回调地狱

// 方式一: 层层嵌套(回调地狱 callback hell)
function getData() {
  // 1.第一次请求
  requestData("aaa").then(res1 => {
    console.log("第一次结果:", res1)

    // 2.第二次请求
    requestData(res1 + "bbb").then(res2 => {
      console.log("第二次结果:", res2)

      // 3.第三次请求
      requestData(res2 + "ccc").then(res3 => {
        console.log("第三次结果:", res3)
      })
    })
  })
}

2. 基于Promise的处理方案

// 方式二: 使用Promise进行重构(解决回调地狱)
// 链式调用
function getData() {
  requestData("aaa").then(res1 => {
    console.log("第一次结果:", res1)
    return requestData(res1 + "bbb")
  }).then(res2 => {
    console.log("第二次结果:", res2)
    return requestData(res2 + "ccc")
  }).then(res3 => {
    console.log("第三次结果:", res3)
  })
}

3. 基于生成器的处理方案

function* getData() {
  const res1 = yield requestData("aaa")
  console.log("res1:", res1)

  const res2 = yield requestData(res1 + "bbb")
  console.log("res2:", res2)

  const res3 = yield requestData(res2 + "ccc")
  console.log("res3:", res3)
}

const generator = getData()
generator.next().value.then(res1 => {
  generator.next(res1).value.then(res2 => {
    generator.next(res2).value.then(res3 => {
      generator.next(res3)
    })
  })
})

4. 基于async/await的处理方案(最优)

async function getData() {
  const res1 = await requestData("aaa")
  console.log("res1:", res1)

  const res2 = await requestData(res1 + "bbb")
  console.log("res2:", res2)

  const res3 = await requestData(res2 + "ccc")
  console.log("res3:", res3)
}

// 2s后输出:res1: aaa
// 4s后输出:res2: aaabbb
// 6s后输出:res3: aaabbbccc
const generator = getData()

通过上面几种方案的比较我们可以得出结论:

  1. 传统的异步处理方案会导致回调地狱,这还只是三个请求,如果是十个甚至更多请求的话,想想有多恐怖!
  2. 基于生成器的方案虽然写法上较为简洁,但是调用时似乎又回到了回调地狱上了。
  3. 基于Promise的优化方案,看起来足够简洁,采用链式调用的方式使用起来也非常方便,但是还是不够优雅,对,你没听错,就是优雅! 咱们也要和雷布斯学,写出诗一样的代码嘛!
  4. 基于async/await的处理方案,不但逻辑清晰,看起来足够简洁,使用起来也足够方便。其实async/await就是基于Promise进行封装的,想想我们上面所说的异步函数的返回值就能清楚,为什么返回一个普通的值也是用Promise进行包裹起来的呢,对吧。 async关键字将函数转换成返回Promise对象的函数,await关键字用于等待Promise对象的完成。 async/await的优势在于能够使异步代码看起来更像同步代码,这使得代码更容易理解和维护。
转载自:https://juejin.cn/post/7250268273638637629
评论
请登录