JS现代化异步

异步

asynchronous

允许程序在不等待某操作完成时,继续后续的操作

假设有以下代码

// code 1
// 这里一定是异步吗?
ajax('http://github.com/good.txt', data => {
  console.log(data)
})
console.log('ajax called')

如果以上ajax函数是同步进行,程序需要等到这个函数结束后才能继续运行,页面会出现无响应状态。

问题:为什么有些函数是同步的

通常异步处理一些耗时的操作

回调

callback

通过函数参数传递到其他代码的,某一块可执行代码的引用

以下的cb都是回调吗?

// code 2
const cb = () => console.log('haha')
setTimeout(cb, 1000)
// code 3
const cb = () => console.log('cb')
const a = () => {
  cb()
}
a()

回调的问题

信任问题

function myTimeout(sec, cb) {
  setTimeout(() => {
    doSth()
    cb()
  }, sec * 1000)
}

myTimeout(1000, () => {
  console.log('done')
})

cb作为我们自己写的函数,被传进myTimeout之后,就不受我们自己控制了,而是myTimeout的实现者来控制调用,那就会出现信任问题,如果在myTimeout里,我写出了bug,调用了cb多次,那么最后的输出结果为多个done,同理没有被调用的话,则没有输出

同步or异步?

以下函数实现如果已经获取到值,则立即返回,如果没有获取到,则先获取再通知

let num = null
let a = 0

function getA(cb) {
  if (num) {
    a = num / 2
    return cb(a)
  }
  ajax('http://geta.com/', n => {
    num = n
    a = num / 2
    cb(a)
  })
}

上面的函数,有时候是同步的,有时候是异步的,一般来说不会出问题,但如果我有以下代码

let num = null
let a = 0

getA(cb)

console.log(a)

上面代码,num的初始值决定了第三句console.log(a)的输出,如果初始值不为null,则输出num/2,如果为null,则输出0

不直观

先来看一段代码

假设以下函数都是异步函数,函数的调用顺序是什么

funcA(() => {
  funcB(() => {
    funcC(() => {
      funcD()
    })
    funcE()
  })
})
funcF()

回调地狱

// code 4
setTimeout(name => {
  show(`welcome ${name}`)
  setTimeout(name => {
    show(`welcome ${name}`)
    setTimeout(name => {
      show(`welcome ${name}`)
      setTimeout(name => {
        show(`welcome ${name}`)
        setTimeout(name => {
          show(`welcome ${name}`)
        }, 1000, 'tom')
      }, 1000, 'hanmeimei')
    }, 1000, 'lilei')
  }, 1000, 'jim')
}, 1000, 'jack')

门:所有人都到齐了,才能进门

也就是所有异步操作都完成了,才能进行下一步操作

在下面的函数异步取两个值,最后对这两个值进行运算后输出

let a = null
let b = null

function aGetted(num) {
  a = num / 2
  if (a && b) {
    cacl(a, b)
  }
}

function bGetted(num) {
  b = num * 2
  if (a && b) {
    calc(a, b)
  }
}

function calc(a, b) {
  console.log(a + b)
}

ajax('http://geta.com/', aGetted)
ajax('http://getb.com/', bGetted)

竞态

竞态:竞争一方赢了,则结束

只处理第一个完成的异步操作

let getted = false

function aGetted(num) {
  if (getted === false) {
    console.log(num / 2)
    getted = true
  }
}

function bGetted(num) {
  if (getted === false) {
    console.log(num * 2)
    getted = true
  }
}

ajax('http://geta.com/', aGetted)
ajax('http://getb.com/', bGetted)

Promise

先来看一下Promise的典型使用方法

const promise = new Promise((resolve, reject) => {
  ajax('http://example.com/', data => {
    if (data.status === 0) {
      resolve(data.payload)
    } else {
      reject(data.status)
    }
  })
})

promise.then(data => {
  console.log(data)
}).catch(error => {
  console.error(error)
})

!! 仍然使用回调技术,只不过规定了回调形式,传递必要数据给我们自己处理

三种状态

对象的状态不受外部改变

一旦状态改变,就不会再变

强制回调

Promise的构造函数传入一个回调,该回调规定接受两个回调函数,resolvereject,分别表示操作成功和失败,如果调用了resolve,状态从Peding变为Fulfilled,如果调用了reject,状态从Pending变为Rejected,如果没有调用,则一直是Pending

resolve函数决定promise.then函数被调用,reject函数决定promise.catch函数被调用,如果一起为Pending状态,则不会结束,没有函数被调用

所以机制上,不会造成我们传进去的回调由于实现者BUG的原因,造成没有被调用或者被多调用的情况

强制异步

Promise内部的状态只会变化一次,如果第一次从Pending变成了Fulfilled,那么后续的.then,也依然是异步调用

链式调用

我们可以像以下调用一样,对Promise做链式调用

Promise.then((a) => {
  // print name a
}).then((b) => {
  // print name b
})

对应上面的回调地狱,我们可以这样写

function welcome(time, name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      show(`welcome ${name}`)
      resolve()
    })
  })
}

welcome(1000, 'jack').then(() => {
  return welcome(1000, 'jim')
}).then(() => {
  return welcome(1000, 'lilei')
}).then(() => {
  return welcome(1000, 'hanmeimei')
}).then(() => {
  return welcome(1000, 'tom')
})

这样从人类可角度看,是非常易读的

Promise.all

我们来看一下Promise如何解决门问题

function ajaxPromise(url) {
  return new Promise((resolve, reject) => {
    ajax(url, num => {
      resolve(num)
    })
  })
}

const promiseA = ajaxPromise('http://geta.com')
const promiseB = ajaxPromise('http://getb.com')

Promise.all([promiseA, promiseB]).then(result => {
  const sum = result.reduce((a, b) => {
    return a + b
  })
  console.log(sum)
})

Promise.race

function ajaxPromise(url) {
  return new Promise((resolve, reject) => {
    ajax(url, num => {
      resolve(num)
    })
  })
}

const promiseA = ajaxPromise('http://geta.com')
const promiseB = ajaxPromise('http://getb.com')

Promise.race([promiseA, promiseB]).then(result => {
  console.log(result)
})

Generator

概念

Generator函数是一个状态机,封装了多个内部状态,并且返回一个遍历器对象

示例

function* numGenerator() {
  yield 1;
  yield 2;
  return 3;
}

const gen = numGenerator()

console.log(gen.next())
console.log(gen.next())
console.log(gen.next())
console.log(gen.next())

输出:

{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: true}
{value: undefined, done: true}

调用了四次next,第一个next使用Generator函数内部代码开始执行,直到遇到第一个yield为止,返回{value: 1, done: false},其中的value为yield后面的值,done表示操作还没有完成

最后一次调用表示完成了,并且已经没有返回值,所以返回{value: undefined, done: true}

所以generator生成了一系列的值,这也就是他名字的由来

yield

yield只能用在Generator函数里面,用在普通函数里会出错,如:

(function () {
  yield 'hello'
})()
function* fibonacci() {
  let [prev, curr] = [0, 1]
  while (true) {
    [pref, curr] = [curr, prev + curr]
    yield curr
  }
}

for (const n of fibonacci()) {
  if (n > 1000) {
    break
  }
  console.log(n)
}

06 Sep 2017