关于JS中的回调函数(Callback)以及如何解决地狱回调问题

273 阅读3分钟

1、回调函数

先来一个回调函数简单的例子

ajax(url, () => {
  // 处理逻辑
})

2、地狱回调

但是回调函数存在一个问题,那就是容易写出地狱回调(Callback hell)。假设很多个请求存在依赖性,就可能会导致这样的结果

ajax(url, () => {
  // 处理逻辑
  ajax(url1, () => {
    // 处理逻辑
    ajax.(url2, () =>{
      // 处理逻辑
    })
  })
})

以上代码看起来不利于阅读和维护,想要利于阅读,可能会这样写

function firstAjax() {
  ajax(url1, () => {
    // 处理逻辑
  })
};

function secondAjax() {
  ajax(url2, () => {
    // 处理逻辑
  })
};

ajax(url, () => {
  // 处理逻辑
  firstAjax()
});

看上去是利于阅读了,但还是没有解决根本问题。

回调地狱的根本问题是:

  • 嵌套的函数存在耦合性,一旦有所改动,就会牵一发而动全身
  • 嵌套的函数一多,就很难处理错误

回调函数还存在其它几个缺点,比如不能使用 try catch 捕获错误,不能直接 return 。

3、使用 Generator 解决地狱回调问题

Generator 顾名思义就是一个生成器,使用 function* 语法和一个或多个 yield 表达式以创建一个函数即为生成器,当然它的返回值就是一个迭代器即生成器,Generator 最大的特点就是可以控制函数的执行。

先看一个简单的例子

function *foo(x) {
  let y = yield x + 2;
  return y;
};

Generator 函数和普通函数的区别在于前者是可以暂停执行的,所以函数名前加 * 号以示区分,其实整个 Generator 函数是一个封装的异步任务,异步操作需要暂停的地方,都用 yield 语句注明。

再来看一段代码

function *foo(x) {
  let y = 2 * (yield (x + 1));
  let z = yield (y / 3);
  return (x + y + z);
};

let it = foo(5);
console.log(it.next());   // => {value: 6, done: false}
console.log(it.next(12)); // => {value: 8, done: false}
console.log(it.next(13)); // => {value: 42, done: true}

为什么会产生这些值,接下进行逐行代码分析

  • 首先 Generator 函数和普通函数不同,它会返回一个迭代器
  • 当执行第一次 next 时,传参会忽略,并且函数暂停在 yield(x+1) 处,所以返回 5 + 1,得到 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果不传参,yield 永远返回 undefined 。 此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3,得到 8
  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13,x = 5,y = 24,相加得到 42

这样,经过上述的分析,大概就知道了怎么通过 Generator 函数来解决地狱回调的问题,可以把之前地狱回调的例子改写成如下代码

function *fetch() {
  yield ajax(url, () => {});
  yield ajax(url1, () => {});
  yield ajax(url2, () => {});
};

let it = fetch();
let result1 = it.next();
let result2 = it.next();
let result3 = it.next();

4、使用 Promise 解决地狱回调问题

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果在 then 中使用了 return,那么 return 的值会被 Promise.resolve() 包装。

---END---