异步与Promise

我承诺,无论生老病死都将与你不离不弃

Posted by Lorry on June 21, 2018

文章字数:13264, 阅读全文大约需要:37 分钟

Promise是ES6中一个很重要的异步语法.可以很优雅的解决异步,回调等问题.

首先来引入生成器 generator

看一个同步的例子:从server端获取json数据

try{
  // 获取整个项目
  let project = syncGetJSON('lorry.com');
  // 获取项目中的某一个任务
  let mission = syncGetJSON(project[0].missionURL);
  // 获取该任务中的具体信息
  let missionDetail = syncGetJSON(mission[0].detailURL);
} catch(e) {
  console.error(e)
}

上述例子的写法看起来十分的好理解,但也有一个严重的问题是因为写的都是同步代码,所以会阻塞掉整个逻辑的运行.如果project的内容很大,需要10s,那么我们的程序就要等待10s后才能继续运行,用户交互十分的不友好.

所以有了异步

getJSON('lorry.com', (err, project) => {
  if (err) {
    console.error(err)
    return
  }
  getJSON(project[0].missionURL, (err, mission) => {
    if (err) {
      console.error(err)
      return
    }
    getJSON(mission.detailURL, (err, detail) => {
      if(err) {
        console.error(err)
        return
      }
      console.log(detail)
    })
  })
})

上述代码可以解决之前的阻塞问题,但是是不是看起来十分的不优雅?有许多的err判断,然后一层一层的嵌套(回调地狱 callback hell)

于是乎,生成器 generator横空出世,如果能够很清楚的理解下面这段代码,那么你可以快速浏览,或许这些都是你已经知道的知识了.

// getJSON为一个Promise
async(function* () {
  try {
    const project = yield getJSON('lorry.com')
    const mission = yield getJSON(project[0].missionURL)
    const detail = yield getJSON(misson.detailURL)
  } catch(e) {
    console.error(e)
  }
})
function async (generator) {
  let iterator = generator();
  function handle(iteratorResult) {
    if (iterator.done) { return }
    const iteratorValue = iteratorResult.value
    if(iteratorValue instanceof Promise) {
      iteratorValue.then(res => handle(iterator.next(res))).catch(err => iterator.throw(err))
    }
  }
}
try {
  handle(iterator.next())
} catch(e) {
  iterator.throw(e)
}

是不是跟第一个的写法很像?

generator其实就两个东西,*yield

1 for-of可以遍历所有的yield

function* example() {
  yield(1)
  yield(2)
  yield(3)
}

for (let num of example()) {
  console.log(num) // 打印1,2,3
}

2 使用next()对generator进行控制, .value表示yield的值.done属性表示终止条件

let nums = example()   // nums为一个迭代器(iterator)
let num1 = nums.next().value // 1
let num2 = nums.next().value // 2
let num3 = nums.next().value // 3
let num4 = nums.next().value // undefined
console.log(nums.done) // true

现在可以用while来迭代了,其实for-of的就是以下while的一个语法糖.

while(!(item = nums.next()).done) {
  console.log(item // 1,2,3
}

3 generator之间的嵌套 yield表示另一个生成器,会按顺序将嵌套生成器的迭代完,再往下执行

function* example2() {
  yield* example1();
  yield 4;
  yield 5;
}

for (let num of example2) {
  console.log(num) //1,2,3,4,5
}

4 可以在generator中写while(true)

function* count() {
  let id = 1;
  while(true) {
    yield id++
  }
}

let idIterator = count()
idIterator.next().value // 1
idIterator.next().value // 2
// 不使用生成器的实现方式,会造成变量的污染
function() {
  let id = 1; // 我本不属于这里.
  function count() {
    return id++
  }
  console.log(count())
}

这个生成器永远不会结束,.done永远为false

5 使用generator来遍历树,其中也可以看出如何使用生成器的递归调用

function* DomTraversal(element) {
  yield element;
  element = element.firstElementChild()
  while (element) {
    yield* DomTraversal(element) // 先深度遍历
    element = element.nextElementSibling; // 再广度遍历
  }
}

const head = document.getElementById('head')
for (let element of DomTraversal(head)) {
  console.log(element)
}

6 如何与generator进行通信? 将值传入next参数中

function* example3(num) {
  const num1 = yield (num + 1)
  yield (num + num1 + 1)
}

let nums = example3(1)
let num1 = nums.next().value  // 2
let num2 = nums.next(2).value // 4

这样就可以与generator进行双向通信, yield 从generator中传出, next(data) 将data传入到generator中.

但是要注意的是,无法对第一个next进行通信.

因为需要通过变量来保存传给generator的数据,然后再传到yield中,如上例中使用num1保存传入数据,再传到下一个yield语句中.由于第一个next (yield)没有变量可以储存.

不过可以通过传参的方式当成initial value传入,如上例的num

7 generator抛出错误

function* example4(name) {
  try{
    yield (`my name is ${name}`)
    fail('坦白从宽,抗拒从严')
  } catch (e) {
    console.log(e)
  }
}

let iterator = example4('lorry')
const result = iterator.next().value // my name is lorry
iterator.throw('我主动认错') // 打印错误信息.

8 generator的本质

  • 在执行generator的时候并没有执行它,而是创建了一个新的用于向生成器请求值的迭代器(iterator), generato yield一个值,然后suspend,等待下一次的next执行.
  • 包括4个状态
    • 1 suspended start –> 当generator创建时,此时没有任何generator的代码被执行
    • 2 executing –> 当next调用时
    • 3 suspended yield –> 执行时,到达yield语句后,会创建一个新对象(eg.{value: 1,done: false}),generator暂停并等待
    • 4 complete 当yield执行完毕的下一个next()或者显式return时 返回{value: undefined, done:true}

      9 注意 returngenerator 中的使用

      function* example5() {
      yield 1
      return 2
      }
      function* example6() {
      yield 1
      yield 2
      }
      let a = example5()
      for(let i of a ) {
      console.log(i) // 1,只有一个1
      }
      let b = example6()
      for(let i of b) {
      console.log(i)// 1,2
      }
      

接下来就是Promise

1 先抛一个例子

const smallPromise = new Promise((resolve, reject) => {
  resolve('yes')
  // reject('an error happen to reject')
})
smallPromise.then(res => console.log(res), err => console.log(err))// yes

2 如何实现异步的功能?

const asyncPromise = new Promise((resolve,reject) => {
  console.log('asyncPromise create')
  setTimeout(()=>{
    console.log('asyncPromise callback')
    resolve('async solving')
    console.log('async solved') 
  },500)
})

const immediatePromise = new Promise((resolve,reject) => {
  console.log('immediatePromise create')
  resolve('immediate solving')
  console.log('immediatePromise solved')
})

asyncPromise.then(res => console.log(res))
immediatePromise.then(res => console.log(res))
console.log('end code')
/**
asyncPromise create
 immediatePromise create
 immediatePromise solved
 end code
 immediate solving
 asyncPromise callback
 async solved
 async solving
*/

可以看到then之后的语句是在执行完所有的同步代码之后执行(注意end code的位置)

3 明目张胆的拒绝(reject)

const promise = new Promise((resolve,reject) => {
  reject('我不想给你任何承诺')
})

promise.then(
  () => console.log('我愿意'),
  (err) => console.log(err) 
);// 我不想给你任何承诺

除此之外还有catch的内建语法

promise
.then(() => console.log('我愿意'))
.catch((err) => {
  console.log(err);
})// 我不想给你任何承诺

4 偷偷摸摸的拒绝

const promise = new Promise((resolve, reject) => {
  undeclareVarible++;
});
promise.then(()=>console.log('我愿意')).catch(() => console.log('其实我内心是拒绝的'));

注意,不管是拒绝还是接收都是一个最终的确定态,promise会由pending转为resolved

5 实际的例子:

function getJSON(url) {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();
    request.open('get',url);
    // 如果服务器返回了消息,但是并不代表是我们预期的
    request.onload = function() {
      try {
        if (this.status === 200) {
          resolve(JSON.parse(this.response))
        } else {
          reject(this.status + ':' + this.statusText);
        }
      } catch (e) {
        reject(e.message);
      }
    }
    // 如果与服务器通信出现了问题
    request.onerror = function() {
      reject(this.status + ':' this.statusText)
    }
    request.send()
  })
}

getJSON('data/lorry.json').then(res => console.log('lorry got'), err => console.error('something wrong:', err))

6 链式Promise

看了上面的getJSON之后,对本文提出的第一个例子应该就会有不一样的理解了

getJSON('data/lorry.com')
.then(res=>getJSON(res[0].missionURL))
.then(res=>getJSON(res.missionDetaiURL))
.then(res=>console.log(res))
.catch(err=>console.error(err))

是不是清爽很多了?注意,链式操作的时候使用catch更为方便,他会将整条链上的任何错误都catch到,如果使用reject回调,会重复性的写很多错误handle代码

7 Promise.all 和 Promise.race 如果有多个相互之间不引用的Promise可以这么写

Promise.all([getJSON(url1),getJSON(url2),getJSON(url3)].then(results => {
  const res1 = results[0], res2 = results[1], res3 = results[2] // 返回所有的结果,包裹在数组中
}).catch(err => console.error(err))

Promise.race([getJSON(url1),getJSON(url2),getJSON(url3)].then(res => {
  console.log(res + 'is the first one finished'); // 只返回最快返回的结果
})).catch(err=>console.error(err))

能看到这里相信你就能够理解文章最开始写的例子了

最后:asyncawait 华丽登场,其实本质上就是 promisegenerator 的结合,可以让异步的代码写得跟同步代码一样

async function(){
  try {
    const project = await getJSON('lorry.com');
    const mission = await getJSON(project[0].missionURL);
    const missionDetail = await getJSON(mission.detailURL);
  }
}

async 告诉js引擎这个函数是异步的函数,可以不用等待结果继续执行.

await表示需要等待异步的返回结果之后才会继续执行,即此处是阻塞的.

相当于之前写的promise resolve 之后的执行过程

await 只是一个表达式,跟加减乘除没什么区别,哪怕不是异步代码,也可以使用await等,只是跟不加await一样而已.

function takeTime() {
  return new Promise(resolve => setTimeout(resolve(2),1000))
}

async function test() {
  /**相当于
    let result
    takeTime().then(res => res = result)
    少了个外层变量的声明
  */
  let result = await takeTime();
  console.log(result)
}
// 返回的依然是一个Promise对象,状态为resolved
test();

// 也可以await同步代码
function sync() {
  return 2
}
async function add() {
  const a = await takeTime() 
  const b = a + await takeTime() 
  const c = a + await sync() 
  console.log(a) // 2
  console.log(b) // 4
  console.log(c) // 4
}

function err() {
  return new Promise((resolve,reject)=>reject('我是故意的')) 
}
async function testError () {
  let error
  await err().catch(err=> error=err) // 把err()是为一个promise即可
  console.log(error)
}

在这里想强调一个 await 中一个比较容易跳进去的坑.

async function wait_for_possible() {
  await Promise(r => setTimeout(r, 1000))
  const coin = Math.random();
  if (coin > 0.5) {
    return 'yahhhh'
  } else {
    throw Error('bad luck!')
  }
}
// case 1
async function foo_1() {
  try {
    wait_for_possible()
  } catch(e) {
    return 'I caught you!'
  }
}
// case 2
async function foo_2() {
  try {
    await wait_for_possible()
  } catch(e) {
    return 'I caught you!'
  }
}
// case 3
async function foo_3() {
  try {
    return wait_for_possible()
  } catch(e) {
    return 'I caught you!'
  }
}
// case 4
async function foo_4() {
  try {
    return await wait_for_possible()
  } catch(e) {
    return 'I caught you!'
  }
  finally {
    // 把所有需要 release 掉的东西放这里, 在 break 之后只有这个地方会执行.
  }
}

在于 try-catch联合使用时一定要注意同异步的问题, 因为 catch 只能 catch 到同步的错误, 不管是直接执行( case 1) 还是返回( case 3), 都 catch 不到错误, 因为 promise 还在 pending, 并没有错误, 所以 foo 的代码已经执行完毕, 之后 throw 出来的错误就会到未捕获错误里.只有 case 2 和 case 4 是可以正常使用和捕获的.

async 会将整个函数的返回值📦成一个Promise, 不信你可以wait_for_possible() instanceof Promise 试试, 返回一定是 true.也就是说 async 本身就是promise.wrap 的一种实现方式

for-await, 可以在循环中使用 await, 保证每个 iterator 都决议完毕之后再进入循环.可以将生成器和 async-await 结合

async function* asyncRandomNumber() {
  const url = 'https://www.random.org/decimal-fractions/?num=1&dec=10&col=1&format=plain&rnd=new';
  while(true) {
    const response = await fetch(url);
    // 该 text 也为异步方法
    const text = await response.text();
    yield Number(text)
  }
}

async function foo() {
  for await (const number of asyncRandomNumber()) {
    console.log(number)
    if (number > 0.9) break;
  }
}

foo()

使用 [Symbol.asyncIterator]

function streamAsyncIterator(stream) {
  let i = 0;
  return {
    next() {
      // Stream reads already resolve with {done, value}, so
      // we can just call read:
      return {done:false, value: i++}
    },
    return() {
      // Release the lock if the iterator terminates.
      return {done:true, value: 'iterator done'};
    },
    // for-await calls this on whatever it's passed, so
    // iterators tend to return themselves.
    [Symbol.asyncIterator]() {
      return this;
    }
  };
}

async function foo() {
  for await (const a of streamAsyncIterator()) {
    if (a > 10) {
      return a;
    }
    console.log(a)
  }
}

foo().then(v => console.log(v))

上述代码打印的结果为 0,1,2,3,4,5,6,7,8,9,10, 11

既然Promise那么优秀, 可以实现异步中控制权的反转再反转, 还可信. 有没有一种办法可以直接将普通的回调函数转化为Promise呢?可以借助一些第三方库,也可以自己写一个简单的Promise.wrap函数, 假设回调都是error-first的风格

if(!Promise.wrap) {
  Promise.wrap = fn => (...args) => new Promise((resolve, reject) => {
    fn.apply(null, args.concat((err, value) => {
      err && reject(err)
      resolve(value)
    }))
  })
}

这样就完成了一个简单的封装, 也是高阶函数的应用.

接下来看看如何使用吧

function fake(a, b, cb) {
  setTimeout(()=> cb(a,b), 2000);
}
const wrapFunc = Promise.wrap(fake)
wrapFunc(null, 2).then((res) => console.log(res)); // 2s后打印2

generator中的辅助函数

step

function step(gen) {
  // 闭包
  const it = gen();
  let last;
  return (value) => {
    // 不管yield出来的是什么都会传回去
    last = it.next(value).value;
    return last
  }
}
// 使用方法
let i = 1;
function *gen1() {
  while(true) {
    yield (i ++)
  }
}
function *gen2() {
  let x = yield (i = i + 2);
  yield x * 10
}
let newGen1 = step(gen1)
let newGen2 = step(gen2)
newGen1() // 1
newGen1() // 2
newGen1() // 3
newGen2() // 6
newGen2(10) // 100
newGen1() // 6

相信在开发中不会写这样的代码,只是为了更好的演示控制权在生成器中的交换以及信息的传递

ES6中有一个新的基本类型 Symbol,


let something = (() => {
  let nextVal = 0;
  return {
    [Symbol.iterator]: function() {return this},
    next: () => {
      if (nextVal === 0) {
        nextVal = 1
      } else {
        nextVal = (3 * nextVal)
      }
      return {done: false, value: nextVal}
    }
  }
})();
// 还可以把iterator抽离出来
let nextObj = {
  nextVal: 0,
  next: function() {
      if (this.nextVal === 0) {
        this.nextVal += 1
      } else {
        this.nextVal *= 3
      }
      return {done: false, value: this.nextVal}
    },
}
let something = (() => {
  return {
    [Symbol.iterator]: () => nextObj,
  }
})()

something就是一个可迭代的对象, 并且永远可迭代.使用for...of来看看

for (let ret of something) {
  if (ret < 100) {
    console.log(ret)
  } else {
    break;
  }
}

每一个可迭代的对象都会有一个[Symbol.iterator], 用来每次迭代的时候返回的内容, 会调用next方法, 并将最后return的value值传回.而且会保存状态

let a = [1,2,3,4]
const it = a[Symbol.iterator]()
it.next().value; // 1
for (let i of it) {
  console.log(i); // 2,3,4 
}

[Symbol.iterator]从同一个iterator对象出来的链式[Symbol.iterator]每一个不同但是效果相同

let a = [1,2,3,4]
const it = a[Symbol.iterator]()
const it2 = it[Symbol.iterator]()
const it3 = it2[Symbol.iterator]()
const it4 = it[Symbol.iterator]()
Boolean(it == it2 || it2 == it3 || it3 == it4) // false
it.next().value;// 1
it2.next().value;// 2
it3.next().value;// 3
it4.next().value;// 4

注意上述的例子中使用了break来阻止死循环,其实generator自己有一个跳出循环的方法, it.return()

function *something() {
  try {
    let nextVal = 0;
    while(true) {
      if (nextVal === 0) {
        nextVal += 1
      } else {
        nextVal *= 3
      }
      yield nextVal
    }
  }
  finally {
    console.log('clean up')
  }
}
const it = something()
for (let ret of it) {
  if (ret < 100) {
    console.log(ret)
  } else {
    console.log(it.return('hello world').value)
  }
}