异步 CallBack 回顾

247 阅读3分钟

前言

这篇文章主要是想回顾一下 CallBack 的一些使用方式

异步

所谓"异步",简单说就是一个任务分成几段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。这种不连续的执行,就叫异步。

前置知识

例子中异步代码主要使用 node 读取文件,复习一下 fs 模块的 fs.readFile 方法

const fs = require('fs')

fs.readFile('./a.txt', 'utf8', function(err, data) {
  if (err) return
  console.log(data)
})

例子中 readFile 第三个参数接受一个方法,这个方法会等待文件异步读取完毕后执行并传入结果。

回调的问题

主要有两个问题,错误处理、回调地狱。接下来用代码演示

错误处理

try catch 只能捕获同步错误,对异步无能为力,比如

try {
  fs.readFile('./a.txt', 'utf8', function(err, data) {
    throw new Error('报错了!')
  })
} catch (err) {
  console.log('捕获错误:', err)
}

因为 fs.readFile 是异步执行的,所以 fs.readFile 外的 try catch 不能捕获内部错误。接下来修改代码捕获错误

fs.readFile('./a.txt', 'utf8', function(err, data) {
  try {
    throw new Error('报错了!')
  } catch (err) {
    console.log('捕获错误:', err)
  }
})

只需要把 try catch 放到内部就能捕获错误

回调地狱

比如有个需求,要求先读取a.txt的值,再读取b.txt,再读取c.txt等等等,这代码怎么写

fs.readFile('./a.txt', 'utf8', function(err, data) {
  fs.readFile('./b.txt', 'utf8', function(err, data) {
    fs.readFile('./c.txt', 'utf8', function(err, data) {
      // ....
    })
  })
})

如果再加上try catch ...

fs.readFile('./a.txt', 'utf8', function(err, data) {
  try {
    fs.readFile('./b.txt', 'utf8', function(err, data) {
      try {
        fs.readFile('./c.txt', 'utf8', function(err, data) {
          try {
            // ....
          } catch (err) {
            console.log('捕获错误:', err)
          }
        })
      } catch (err) {
        console.log('捕获错误:', err)
      }
    })
  } catch (err) {
    console.log('捕获错误:', err)
  }
})

这代码就看着想吐了。

逐步解决异步

现在有个需求,看下面代码,我想在 a.txtb.txtc.txt 都读取完毕后返回 collect 对象,这该怎么实现

const collect = {}

fs.readFile('./a.txt', 'utf8', function(err, data) {
  collect.a = data
})
fs.readFile('./b.txt', 'utf8', function(err, data) {
  collect.b = data
})
fs.readFile('./c.txt', 'utf8', function(err, data) {
  collect.c = data
})

思考:能不能写一个方法,每次在 fs.readFile 执行完后查看 collect key 的数量

const collect = {}

// 哨兵函数
function out() {
  if (Object.keys(collect).length === 3) {
    console.log(collect)
  }
}

fs.readFile('./a.txt', 'utf8', function(err, data) {
  collect.a = data
  out()
})
fs.readFile('./b.txt', 'utf8', function(err, data) {
  collect.b = data
  out()
})
fs.readFile('./c.txt', 'utf8', function(err, data) {
  collect.c = data
  out()
})

这个 out 方法就叫哨兵函数,但是这个方法用起来还是不方便。我想写一个 after 方法,接受次数和回调就能实现 out 方法比如

let out = after(3, function(data){
  console.log(data)
})

接下来实现这个 after 方法

function after(times, callback) {
  return function() {
    if (--times === 0) {
      callback()
    }
  }
}

let out = after(3, function(data){
  console.log(data)
})

// 执行 out 3次
out()
out()
out() // console.log 执行

after() 生成的 out 代替之前的 out 感觉舒服了好多。但是还是不够,如果我想在异步都执行完毕后执行多个方法怎么办

思考:用数组保存要执行都方法,当需要执行的时候依次执行

实现一个 Dep 对象,Dep 上有一个保存CallBackarr 数组、一个把CallBack添加到arron方法,一个哨兵函数emit,实现和用法如下

const collect = {}

const Dep = {
  arr: [],
  on(fn) {
    this.arr.push(fn)
  },
  emit() {
    if (Object.keys(collect).length === 3) {
      this.arr.forEach(function(fn){
        fn()
      })
    }
  }
}

Dep.on(function() {
  console.log(1)
})
Dep.on(function() {
  console.log(2)
})


fs.readFile('./a.txt', 'utf8', function(err, data) {
  collect.a = data
  Dep.emit()
})
fs.readFile('./b.txt', 'utf8', function(err, data) {
  collect.b = data
  Dep.emit()
})
fs.readFile('./c.txt', 'utf8', function(err, data) {
  collect.c = data
  Dep.emit()
})

这就是特别简单的发布订阅,只是扩展一下思路