初窥js异步之路

301

从一道面试题开始

借鉴杨大佬整理的壹题中第10题

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout1');
    Promise.resolve().then(function() {
      console.log('Timer promise1')
    })
}, 0)

setTimeout(function() {
    console.log('setTimeout2');
    Promise.resolve().then(function() {
      console.log('Timer promise2')
    })
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');

执行结果先按下不表,先来探究一下js的异步

异步实现

js是单线程的,但宿主环境是多线程的。

但要能正确执行异步,两个要素必须保证,正确的执行时机和正确的执行环境

  1. 任务队列

遇到异步任务时,将任务回调注册到任务队列中,每当主线程空闲时,会处理任务队列中可执行的回调。这就几乎保证了正确的执行时机(需要等主线程空闲,不能保证timer等时机完全符合) 在遵循上述原则的情况下,node和浏览器略有不同

image

但是在node 11+,node主动抚平差异

image

  1. 作用域链

函数被创建时,都会有[[scope]]属性,记录当前作用域链。当函数执行完,执行环境会被销毁。但内部函数依然被引用,则作用域链仍然保持着对父函数活动对象的引用,这就保证了正确的执行环境。

接着我们分析下文章开头的例题

image

下面进入正题,探究js异步发展历程

事件/回调

从js异步实现,直接产生了最直接异步方式---回调, 以node为例,有经典的Error-first回调模式,node原生模块异步APIU几乎都是这种模式

fs.readFile('/test.txt',function(err,data){
    if (err) {
        thro
    } else {
        console.log(data)
    }
})

存在问题:不良代码习惯容易造成回调地狱

fs.readFile('/test.txt',function(err,data){
  if (err) {
    throw err
  } else {
    fs.readFile('/test1.txt',function(err,data){
      if (err) {
        throw err
      } else {
        fs.readFile('/test2.txt',function(err,data){
          if (err) {
            throw err
          } else {
            console.log(data)
          }
        })
      }
    })
  }
})

发布/订阅

作为一种常用的开发模式,通过解决各个模块间的通讯,而让相互依赖的各模块,集中依赖一个消息中心的抽象。

常用包:PubSub

简单实现:

function PubSub() {
  this.handles = {
    // eventName: [] // cbList
  }
}
PubSub.prototype.subscribe = function (eventName, callback) {
  if (this.handles.hasOwnProperty(eventName)) {
    this.handles[eventName].push(callback)
  } else {
    this.handles[eventName] = [callback]
  }
}

PubSub.prototype.notify = function (eventName, ...rest) {
  if (this.handles[eventName]) {
    this.handles[eventName].map(cb => cb.apply(this, rest))
  }
  return this
}

使用:

const readFilePub = new PubSub()

function readFile(path) {
  fs.readFile(path, (err,data) => {
    if (err) {
      throw err
    } else {
      readFilePub.notify(path, data)
    }
  })
}

readFile('/test1.txt')

readFilePub.subscribe('/test1.txt', () => readFile('/test2.txt'))

readFilePub.subscribe('/test2.txt', () => readFile('/test3.txt'))

readFilePub.subscribe('/test3.txt', () => {})

存在问题:不利于链式异步操作

promise

很多js初学者都认为promise是为了解决异步回调地狱,在我初次接触promise时,也是这种想法。其实不然,promise之前,无论你如何组织代码,都必定通过回调,而回调必然存在一个严重的问题---控制反转。

A.readFile('/test2.txt',function(err,data){
  if (err) {
    throw err
  } else {
    console.log(data)
  }
})

例如这个readFile,如果是一个第三方api的,他可能会存在多调,少调,早调,晚调。。。等等,因为调用的权限已经交到了第三方手上。

所谓‘一诺千金’,从Promises/A+规范提取几点关键要求:

  • 一个promise必须处于以下三种状态之一pending、fulfilled和rejected。状态改变只能是pending到fulfilled或者pending到rejected。状态改变不可逆。
  • promise被实现或者被拒绝时,必须具有一个值,该值不能更改(并不表示深层的不变性,即引用类型的属性依然可变)
  • promise的then方法接收两个可选参数,表示该promise状态改变时的回调。then方法返回一个promise。
  • Promises/A+的then方法可以与已实现的其他Promises规范then方法相互操作。它还允许Promises/A+实现以合理的then方法“同化”不合格的实现

常用包:promise

简单实现:

const PENDING = 'pending' // 等待状态
const FULFILLED = 'fulfilled' // 成功状态
const REJECTED = 'rejected' // 失败状态
function Promise(executor) {
  const self = this
  self.status = PENDING
  self.onFulfilled = []
  self.onRejected = []
  self.value = null

  function resolve(value) {
    if (self.status === PENDING) {
      self.status = FULFILLED
      self.value = value
      self.onFulfilled.map(cb => cb())
    }
  }

  function reject(value) {
    if (self.status === PENDING) {
      self.status = REJECTED
      self.value = value
      self.onRejected.map(cb => cb())
    }
  }

  try {
    executor(resolve, reject)
  } catch (e) {
    reject(e)
  }
}

Promise.prototype.then = function (onFulfilled, onRejected) {
  const self = this
  const promise2 = new Promise((resolve, reject) => { // then返回一个promise
    if (self.status === FULFILLED) {
      setTimeout(() => {
        try {
          const x = onFulfilled(self.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    } else if (self.status === REJECTED) {
      setTimeout(() => {
        try {
          const x = onRejected(self.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    } else if (self.status === PENDING) {
      self.onFulfilled.push(() => {
        setTimeout(() => {
          try {
            const x = onFulfilled(self.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
      self.onRejected.push(() => {
        setTimeout(() => {
          try {
            let x = onRejected(self.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
  })
  return promise2
}

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) { // 避免循环
    reject(new TypeError('Chaining cycle'))
  }
  if (x && typeof x === 'object' || typeof x === 'function') {
    let used  // 使用标记,规避其他promise实现多次调用问题
    try {
      const then = x.then  // thenable是promise的鸭子类型,通过then使各种promise类库得以连接
      if (typeof then === 'function') {
        then.call(x, (y) => {
          if (used) return
          used = true
          resolvePromise(promise2, y, resolve, reject)
        }, (err) => {
          if (used) return
          used = true
          reject(err)
        })
      }else{
        if (used) return
        used = true
        resolve(x)
      }
    } catch (err) {
      if (used) return
      used = true
      reject(err)
    }
  } else {
    resolve(x)
  }
}

当然es6还补充了Promise.all、Promise.race等等Api便于实际业务开发,这部分不在这次探讨范畴

使用:

function readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err,data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data)
      }
    })
  })
}

readFile('/test1.txt')
  .then(() => readFile('/test2.txt'))
  .then(() => 1, () => 2)
  .then(() => readFile('/test3.txt'))

相对前面异步组织方式,promise具有以下优势:

  • 兼容各种promise类库实现链式调用,方便组织同步或异步代码
  • 规避第三方库异步api控制权反转问题

async/await

用同步代码表达异步逻辑,这才是我们的终极梦想吧!

研究async/await之前,我们先复习以下Generator

function *gen (a) {
  let b = yield a
  let c = yield b
  return c
}
const it = gen(1)
console.log(it)
let r1 = it.next()
console.log(r1)
r1 = it.next(2)
console.log(r1)
r1 = it.next(3)
console.log(r1)

// 打印结果
Object [Generator] {}
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }

仅从答应结果,我们就能知道生成器一些意图,下面沿着异步的应用进一步探究以下

既然生成器能生成多次输入和输出,那么我们在输出的位置放上promise,即迭代器执行next返回的value是一个promise

还是上面的例子

function readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err,data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data)
      }
    })
  })
}
function *readAllFile() {
  const data1 = yield readFile('/test1.txt')
  const data2 = yield readFile('/test2.txt')
  const data3 = yield readFile('/test3.txt')
  return 'done'
}

但我们在调用readAllFile的迭代器,还需要手动判定promise的值,再执行next,未免画蛇添足。

这里我们再实现一个能运行生成器的运行器,并将promise连接起来,基本上就能满足我们的需求了。

function run(gen, ...rest) {
  const it = gen(...rest)
  function handleNext(value) {
    const next = it.next(value)
    console.log(next)
    return handleResult(next)
  }
  function handleResult(next) {
    if (next.done) {
      return next.value
    } else {
      return Promise.resolve(next.value).then(handleNext)
    }
  }
  return Promise.resolve().then(handleNext)
}

这样,我们就能通过run,直接运行生成器中异步代码,并得到最终返回结果

这种生成器+promise+运行器运作巧妙的完成了我们终极梦想---同步代码表达异步逻辑。 es7也将这种运作方式纳为标准,也就是我们熟知的async函数

async function readAllFile() {
  const data1 = await readFile('/test1.txt')
  const data2 = await readFile('/test2.txt')
  const data3 = await readFile('/test3.txt')
  return 'done'
}

至此,js异步的发展历程归纳完毕,就目前看来async也基本是最终形态了。