乐趣区

关于javascript:手把手带你掌握迭代器可迭代对象生成器和异步函数以及优雅的异步处理方案-asyncawait

概述

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

迭代器

什么是迭代器呢?迭代器是一种可能在 容器对象上(例如数组或链表等)遍历数据汇合的对象 。迭代器对象提供了一种对立的形式来拜访数据汇合中的元素,而无需裸露其底层示意。通过迭代器,能够一次拜访汇合中的每一个元素,而 不须要晓得其中元素的个数或其底层存储形式。迭代器通常应用 next() 办法来拜访汇合中的下一个元素,并返回一个蕴含元素值和是否还有更多元素的对象。
next 办法有如下要求:

  • 一个 无参数或者一个参数 函数 ,返回一个 该当领有以下两个属性的对象

    1. done(boolean)

      • 如果迭代器 能够产生下一个值,则为 false。(这等价于没有指定 done 这个属性。)
      • 如果迭代器 已将序列迭代结束,则为 true。这种状况下,value 是可选的,如果他仍然存在,即为迭代过后的默认返回值(undefined)
    1. 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 的 劣势 在于可能 使异步代码看起来更像同步代码,这使得代码更容易了解和保护。
退出移动版