第六章 未来的函数:生成器和Promise | 《JavaScript忍者秘籍》学习笔记

400 阅读7分钟

《JavaScript忍者秘籍(第二版)》学习笔记

💡 生成器函数的主要用途是什么?

在异步代码中,为什么使用promise比使用简单的回调函数更好?

使用Promise.race来执行很多长期执行的任务时,promise最终会在什么时候变成resolved状态?

它什么时候会无法变成resolved状态?

6.2 生成器函数

调用生成器并不会执行生成器函数,它会创建一个叫作迭代器(iterator)的对象。

function* WeaponGenerator() {
  yield 'one'
  yield 'two'
  yield 'three'
}

for (let weapon of WeaponGenerator()) {
  console.log(weapon)
}

// one
// two
// three

6.2.1 通过迭代器对象控制生成器

const interator = WeaponGenerator()
interator.next() // {value: 'one', done: false}
interator.next() // {value: 'two', done: false}
interator.next() // {value: 'three', done: false}
interator.next() // {value: undefined, done: true}

let item
while(!(item = interator.next()).done) {
  item.value && console.log(item.value)
}

把执行权交给下一个生成器

function * W() {
  yield 'a'
  yield * N()
  yield 'd'
}

function * N() {
  yield 'b'
  yield 'c'
}

for(let w of W()) {
  console.log(w)
}

// a b c d

6.2.2 使用生成器

  1. 用生成器生成ID序列

    function * IdGenerator() {
            let id = 0
            while(true) {
                    yield ++id
            }
    }
    

    标准函数中一般不应该书写无限循环的代码。但在生成器中没问题!当生成器遇到了一个yield语句,它就会一直挂起执行直到下次调用next方法,所以只有每次调用一次next方法,while循环才会迭代一次并返回下一个ID值。

  2. 使用迭代器遍历DOM树

    function * DomTraversal(el) {
      yield el;
      el = el.firstElementChild
      while(el) {
        yield * DomTraversal(el)
        el = el.nextElementSibling
      }
    }
    
    const subTree = document.getElementById('subTree')
    for (let element of DomTraversal(subTree)) {
      element !== null && console.log(element.nodeName)
    }
    

6.2.3 与生成器交互

可以往next()中传值

如果将参数传递给生成器的next()方法,则该值将成为生成器当前yield操作返回的值。

在生成器的代码路径中的yield运算符,以及通过将其传递给Generator.prototype.next()指定新的起始值的能力之间,生成器提供了强大的控制力。

function * Gen(name) {
  let imp = yield('Hello ' + name)
  yield imp
}

const Iterator = Gen('World') // 第一次调用没有记录任何内容,因为生成器最初没有产生任何结果。
console.log(Iterator.next()) // {value: 'Hello World', done: false}
console.log(Iterator.next('Debra')) // {value: 'Debra', done: false}
// next()传了'Debra', 会成为yield('Hello ' + name)的返回值,即yield('Hello ' + name) === 'Debra'

6.2.4 探索生成器内部构成

  1. 挂起开始——创建了一个生成器后,它最先以这种状态开始。其中的任何代码都未执行。
  2. 执行——生成器中的代码已执行。执行要么是刚开始,要么是从上次挂起的时候继续的。当生成器对应的迭代器调用了next方法,并且当前存在可执行的代码时,生成器都会转移到这个状态。
  3. 挂起让渡——当生成器在执行过程中遇到了一个yield表达式,它会创建一个包含着返回值的新对象,随后再挂起执行。生成器在这个状态暂停并等待继续执行。
  4. 完成——在生成器执行期间,如果代码执行到return语句或者全部代码执行完毕,生成器就进入该状态。

image.png

6.3 使用Promise

const promiseDelay = new Promise((resolve, reject) => {
  console.log('promise is delay')
  setTimeout(() => {
    console.log('do then')
    resolve('one')
  }, 1000)
})

promiseDelay.then(res => {
  console.log(res, 'is done')
})

只有执行了了resolve, Pomise才到达fulfilled状态

6.3.5 链式使用Promise

同时返回 success 和 success again

const promiseDelay = new Promise((resolve, reject) => setTimeout(resolve, 1000, 'success'))

promiseDelay.then(res => {
  console.log(res)
  return 'success again'
}).then(res => {
  console.log(res)
})

间隔1秒 返回success 和 success again

const promiseDelay = new Promise((resolve, reject) => setTimeout(resolve, 1000, 'success'))

promiseDelay.then(res => {
  console.log(res)
  return new Promise((resolve, reject) => setTimeout(resolve, 1000, 'success'))
}).then(res => {
  console.log(res)
})

// 第一个then的return不能直接return promiseDelay,因为这个promise已经是fulfill状态了,会直接执行下一个then

6.3.6 等待多个promise

全部成功后才进入then,只要有一个失败就直接进入catch

const promiseDelay1 = new Promise((resolve, reject) => setTimeout(resolve, 1000, '1 success'))
const promiseDelay2 = new Promise((resolve, reject) => setTimeout(resolve, 2000, '2 success'))
const promiseDelay3 = new Promise((resolve, reject) => setTimeout(resolve, 500, '3 success'))

Promise.all([promiseDelay1, promiseDelay2, promiseDelay3])
  .then(([r1, r2, r3]) => {
    console.log(r1, r2, r3)
  })

// 2秒后返回  => 1 success 2 success 3 success

// 只要有一个fail
const promiseDelay1 = new Promise((resolve, reject) => setTimeout(resolve, 1000, '1 success'))
const promiseDelay2 = new Promise((resolve, reject) => setTimeout(resolve, 2000, '2 success'))
const promiseDelay3 = new Promise((resolve, reject) => setTimeout(reject, 500, '3 fail'))

Promise.all([promiseDelay1, promiseDelay2, promiseDelay3])
  .then(([r1, r2, r3]) => {
    console.log(r1, r2, r3)
  }).catch(e => {
    console.log(e) // 500毫秒后 返回 这个fail
  })

6.3.7 promise竞赛

只执行最快返回的结果,无论fail还是success

const promiseDelay1 = new Promise((resolve, reject) => setTimeout(reject, 1000, '1 fail'))
const promiseDelay2 = new Promise((resolve, reject) => setTimeout(resolve, 2000, '2 success'))
const promiseDelay3 = new Promise((resolve, reject) => setTimeout(resolve, 500, '3 success'))

Promise.race([promiseDelay1, promiseDelay2, promiseDelay3])
  .then((result) => {
    console.log(result) // 3 success
  }).catch(e => {
    console.log(e)
  })

6.4 生成器 + promise

(下面这个例子我也不大明白意义在哪里)

const promiseDelay1 = new Promise((resolve, reject) => setTimeout(reject, 1000, '1 fail'))
const promiseDelay2 = new Promise((resolve, reject) => setTimeout(resolve, 2000, '2 success'))
const promiseDelay3 = new Promise((resolve, reject) => setTimeout(resolve, 500, '3 success'))

const iteratorFn = function* () {
  try {
    yield promiseDelay1
    yield promiseDelay2
    yield promiseDelay3
  } catch (e) {
    console.log(e)
  }
}

function asyncFn(generator) {
  const iterator = generator()

  function handle(result) {
    if (result.done) return
    const iteratorVal = result.value
    if (iteratorVal instanceof Promise) {
      iteratorVal.then(res => {
        handle(iterator.next(res))
      }).catch(e => iterator.throw(e)) // fail后面就不会再执行了
    }
  }

  try {
    handle(iterator.next())
  }
  catch (e) { iterator.throw(e) }
}

asyncFn(iteratorFn)

async

通过在关键字function之前使用关键字async,可以表明当前的函数依赖一个异步返回的值。在每个调用异步任务的位置上,都要放置一个await关键字,用来告诉JS引擎,请在不阻塞应用执行的情况下在这个位置上等待执行结果。
async function asyncFn() {
  try {
    const pms1 = await promiseDelay1
    const pms2 = await promiseDelay2
    const pms3 = await promiseDelay3

    console.log(pms1)
    console.log(pms2)
    console.log(pms3)
                // 全部处理完毕后同时返回
  } catch(e) {
    console.log('Error:', e) // 如果有错误,会依次返回错误(不会执行后面正确的了)
  }
}

asyncFn()

小结

  • 生成器是一种不会在同时输出所有值序列的函数,而是基于每次的请求生成值。
  • 不同于标准函数,生成器可以挂起和回复它们的执行状态。当生成器生成了一个值后,它将会在不阻塞主线程的基础上挂起执行,随后静静地等待下次请求。
  • 生成器通过在function后面加一个星号(*)来定义。在生成器函数体内,我们可以使用新的关键字yield来生成一个值并挂起生成器的执行。如果我们想让渡到另一个生成器中,可以使用yield操作符。
  • 在我们控制生成器的执行过程中,通过使用迭代器的next方法调用一个生成器,它能够创建一个迭代器对象。除此之外,我们还能够通过next函数向生成器中传入值。
  • promise是计算结果值的一个占位符,它是对我们最终会得到异步计算结果的一个保证。promise既可以成功也可以失败,一旦设定好了,就不能够有更多改变。
  • promise显著地简化了我们处理异步代码的过程。通过使用then方法来生成promise链,我们就能轻易地处理异步时序依赖。并行执行多个异步任务也同样简单:仅使用Promise.all方法即可。
  • 通过将生成器和promise相结合我们能够使用同步代码来简化异步任务。

练习

function * EvenGenerator() {
  let num = 2
  while (true) {
    yield num
    num += 2
  }
}

let generator = EvenGenerator()

let a1 = generator.next().value // 2
let a2 = generator.next().value // 4
let a3 = EvenGenerator().next().value // 2
let a4 = generator.next().value // 6
function * NinjaGenerator() {
  yield 'ninja1'
  yield 'ninja2'
  yield 'ninja3'
}

const ninjas = []
for (let ninja of NinjaGenerator()) {
  ninjas.push(ninja)
}

console.log(ninjas) // ['ninja1', 'ninja2', 'ninja3']

// 用while实现for of
**const ninjas = []
const generator = NinjaGenerator()
let ninja = generator.next()
while(!ninja.done) {
  ninjas.push(ninja.value)
  ninja = generator.next()
}

//** 如果NinjaGenerator里面有return, 后面的(包括return这一句)都不再执行
  1. 向next()传参

    除了可以作为函数的中间返回语句使用,yield关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。

    这里有个地方不太好理解——第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数。

    function * Gen(val) {
      val = yield val *2 
      yield val
    }
    let generator = Gen(2)
    let a1 = generator.next(3).value // 4 这里传的参数3会被忽略,因为这次的调用是为了开始执行生成器函数
    let a2 = generator.next(5).value // 5 这个参数5作为yield val * 2的返回值,然后赋值给val
    
    function *Gen(val) {
      val = yield val + 2
      yield val
      yield val
    }
    
    let generator = Gen(2)
    console.log(generator.next()) // 4 第一次传不传参都一样
    console.log(generator.next(9)) // 9 这里9会作为yield val + 2的返回值,赋值给val
    console.log(generator.next(5)) // 9 这里传5没有赋值给val,val还是9
    
    function *Gen(val) {
      val = yield val + 2
      val = yield val
      yield val
    }
    
    let generator = Gen(2)
    console.log(generator.next()) // 4
    console.log(generator.next(9)) // 9
    console.log(generator.next(5)) // 5
    
const promise = new Promise((resolve, reject) => {
  resolve(1) // Promise状态已经完成,不会再调用reject了
  setTimeout(() => reject(2), 500)
})

promise
  .then(val => console.log('Success:', val)) // 只会输出Success: 1
  .catch(e => console.log('Error:', e)) // 不会被调用到