简明 JavaScript 系列是我自己学习 JavaScript 概念时的笔记,旨在记录学习资料以及自己在实践中的感想。每一期尽量有一个有趣的 Demo 。

const readline = require('readline')
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

// 请原谅我这庸俗的剧情
said('Alice', "Will you always be here with me?")
said('Bob', "Oh dear Alice, why would I ever want to leave you?")
said('Alice', "Promise me!")

let promise = makePromise()

promise
  .then(() => {
    console.log('Alice and Bob were happy ever after.')
  })
  .catch(e => {
    console.log('Bob eventually left Alice, what a jerk.')
  })

function said (who, words) {
  console.log(`${who} said: ${words}`)
}

// 到了这里会有一个问题,试着跑一下?
function makePromise () {
  said('Bob', 'I promise I would never leave you.')
  return new Promise(function (resolve, reject) {
    rl.question('你来决定故事的走向: ', answer => {
      if (answer === 'good')
        return resolve('Bob is true to his words')
      return reject('Bob is a total ass')
    })
  })
}

女士们,先生们,看到了吗,这就是我们今天的主角 Promise。照例,以下是学习资料:

Finally! Promise 生 Promise

执行上一段代码,然后输入good之后,你就会触发美好结局,否则就是悲剧。 但你大概会发现一个问题,就是我们的程序并没有自动退出。 因为readline在没有告知close的情况下是不会自己退出的。 那我们该在哪里调用readline#close呢? 最偷懒的办法就是在thencatch里面都调用readline#close。 这样无论结局如何,程序都会自动退出。 事实上,只要记住thencatch都会返回一个新的Promise,那么我们就可以只需要在catch后面再追加一个then就好了。

// ...

promise
  .then(() => {
    console.log('Alice and Bob were happy ever after.')
  })
  .catch(e => {
    console.log('Bob eventually left Alice, what a jerk.')
  })
  .then(() => {
      rl.close()
  })
  
// ...

当然,在未来,我们也可以用Promise#finally,这样在语意上就更加的通顺。毕竟Promise#finally就是用来善后的,无论承诺是否被坚守。 只不过当前(2016-09-08),NodeJS 以及一些主流的浏览器都不支持它。

// ...

promise
  .then(() => {
    console.log('Alice and Bob were happy ever after.')
  })
  .catch(e => {
    console.log('Bob eventually left Alice, what a jerk.')
  })
  .finally(() => {
      rl.close()
  })
  
// ...

这样就干净清爽多了。

言情剧到此位置,让我们开个运动会吧。

Promise.race: 个人百米冲刺

Promise.race 会在并发的几个 Promise 中挑选出最先完成或者失败的那一个,并且返回对应返回值的 Promise。

比如有些情况下,我要向多个源请求同样一份东西,谁先给我,我就用谁的资源。这就是 Promise.race 的用武之地。

也可以看成是百米冲刺,我们只关心夺得冠军的那一位:

const echo = console.log

let names = ['Key', 'Omar', 'Patrick']
let runners = names.map(makeRunner)
race(runners)
  .then(() => {
    relay(runners)
  })

function makeRunner (name) {
  return {
    run() {
      return new Promise(function (resolve, reject) {
        let time = Math.random() * 1000
        time = Math.ceil(time)
        setTimeout(() => {
          resolve(`${name} finished the race in ${time}`)
        }, time)
      })
    }
  }
}

function race (runners) {
  echo("The annual race begins")
  let runs = runners.map(r => r.run())
  return Promise.race(runs)
    .then(result => {
      echo(`First! ${result}`)
    })
    .catch(echo)
}

Promise.all: 做咖啡

Promise.all 接受一组 Promise,并且会等所有的 Promise 都完成或失败后,将对应的值,放入一个数组并且包裹在一个 Promise 中供我们使用。

就好比,做一杯咖啡,我得烧水,磨豆粉,加热牛奶,最后再把三样东西合在一起组成咖啡。我既可以串行,也可以并发。我选择并发。

function boilWater () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      resolve('water is heated')
    }, 5000)
  })
}

function grindBean () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      resolve('bean is grinded')
    }, 1000)
  })
}

function warmMilk () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      resolve('milk is warmed')
    }, 2000)
  })
}

let then = Date.now()
Promise.all(
    [
      boidWater(),
      grindBean(),
      warmMilk()
    ]
).then(([hotwater, bean, milk]) => {
  let diff = Date.now() - then // no less than 5000
  // I have the coffee
}).catch(e => {
  // ...
})

throw 还是 reject

总的来说,能用 reject 则用 reject。

一是与运行时错误触发的 throw 所区分,二也是从语意上更加符合 Promise 的语境。

最重要的的一点是,有的时候,catch 函数是捕获不到我们人为抛出的错误的:

当我们在 Promise 中使用一个异步回调函数时,这个异步函数或者回调里抛出的错误,我们的 catch 是捕获不到的

new Promise(function (resolve, reject) {
  setTimeout(() => {
    throw new Error('I want this error to be handled by catch function')
  }, 0)
})
  .then(() => console.log('it works'))
  .catch(e => console.log("sorry mate, the error won't be caught here"))
// 这段代码只会抛出异常,并不会执行catch中的代码