Skip to content
Menu
CFC Studio
  • 实验室主页
  • CFC 招新简章
  • 友情链接
  • RSS订阅
CFC Studio

JS-解决回调地狱

Posted on 2019年12月14日 by godlanbo

JavaScript是单线程语言,但是js中有很多任务耗时比较长,比如ajax请求,如果都按照顺序进行,往往会出现浏览器无响应的情况,所以就需要异步的形式。我下面用Node.js中的读取文件来举例,node和JavaScript原理是差不多的。

先看看下面这个例子:

// a.json
// {
//   "next": "b.json",
//   "msg": "this is A"
// }
const fs = require('fs')
fs.readFile('a.json', (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(data.toString())
})

读取文件内容是个非常简单的异步操作,你得在文件内容读出来之后才能对文件内容进行操作。这是读取一个文件内容的异步过程,只有一个异步函数,回调函数读出的内容也没有拿出来给其他函数使用。

如果是几个异步函数连在一起呢?比如某一个读取文件的操作需要用到上一个读取文件读出来的内容。看下面这个例子:

// a.json
{
  "next": "b.json",
  "msg": "this is A"
}
// b.json
{
  "next": "c.json",
  "msg": "this is B"
}
// c.json
{
  "next": null,
  "msg": "this is C"
}

const fs = require('fs')
// 定义一个读取文件内容的函数
function getFileContent (filename, callback) {
  fs.readFile(filename, (err, data) => {
    if (err) {
      console.error(err)
      return
    }
    // callback是一个函数,参数是文件的内容
    callback(JSON.parse(data.toString()))
  })
}

getFileContent('a.json', aData => { // aData中拿到a文件的内容,在这个函数里
  console.log('a data is', aData)   // 才能调用aData.next访问b.json
  getFileContent(aData.next, bData => {  // 用前面拿到的a文件内容里的b文件名拿到b文件内容,下面继续
    console.log('b data is', bData)
    getFileContent(bData.next, cData => {
      console.log('c data is', cData)
    })
  })
})

看这里的异步过程产生的回调嵌套已经有好几层了,如果再多几个文件,再多几个异步过程,如山一般的回调就会在代码里形成回调地狱。

这个时候就轮到Promise来了。Promise 是异步编程的一种解决方案,它本身是一个对象,它代表了一个异步操作的最终完成的结果或者失败结果。

promise本身是一个构造函数,下面代码创造了一个Promise实例:

const promise = new Promise((resolve, reject) => {
	// some code
  if(/* 异步操作成功 */) {
  	resolve(value)
  } else { /* 异步操作失败(不是抛出错误,是异步条件不满足的情况下reject) */
    reject(error)
  }
})

promise接受两个参数,resolve, reject,这是两个js自带的函数,不用自己写。resolve的作用是将异步成功的内容传递出去,reject的作用相反,将异步失败时候的内容作为参数传递出去。

promise实例生成后,就可以调用它的then和catch方法,在then中,我们可以拿到resolve和reject中返回的内容,而then又返回一个新的promise实例,这就是它的强大之处,举个例子就好懂了,下面用promise对上面读文件进行重写:

const fs = require('fs')
// 获取文件的函数返回一个处理文件异步的promise
function getFileContent (filename) {
  const promise = new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if (err) {
        reject(err)
        return
      }
      resolve(JSON.parse(data.toString())) // 将读取出来的文件内容用resolve传递出去
    })
  })
  return promise
}
// 调用这个promise,在then里面拿到了a文件的内容
getFileContent('a.json').then(aData => {
  console.log('a Data ', aData)
  // 返回一个新的promise,这个promise处理读取b文件的异步
  return getFileContent(aData.next)
}).then(bData => {  // 返回的是一个promise,所以可以直接链式调用then,这个then会拿到b的内容
  console.log('b Data ', bData)
  // 返回一个新的promise,这个promise处理读取c文件的异步
  return getFileContent(bData.next)
}).then(cData => {  // 链式调用
  console.log('c Data ', cData)
})

使用promise进行重写后,我们发现,再多的文件只不过是多几个链式调用而已,嵌套控制在一层,解决了回调地狱这个问题。

在上面的链式调用里,后一个then会等待前一个promise的结果产生后调用,如果进行中报错,链式调用就会断掉,这时候就用到promise的另一个方法,catch:

promise
  .then(...)
  .then(...)
	.then(...)
  .catch(...) // catch可以处理前面所有then的报错,处理之后可以继续链式调用
	.then(...)

一遇到异常抛出,Promise 链就会停下来,直接调用链式中的 catch 处理程序来继续当前执行。这上面的代码看起来或许有点像下面这样:

try {
  let result1 = syncDoSomething1();
  let result2 = syncDoSomething2(result1);
  let result3 = syncDoSomething3(result2);
} catch(error) {
  handle(error) // 处理error
}

哦,这样似乎有点同步任务的意思了,正好,在新的规定里可以用新的async/await 语法将这种类似同步编程处理异步逻辑的代码体现出来:

async function foo() {
  try {
  	let result1 = await syncDoSomething1();
  	let result2 = await syncDoSomething2(result1);
  	let result3 = await syncDoSomething3(result2);
	} catch(error) {
  	handle(error) // 处理error
	}
}

在async定义的函数中,你可以使用await语法来处理promise,await执行到这里之后,会等待promise中的异步处理完成之后直接取出promise的值,然后再继续执行。我们用这种方法再来优化一下我们开头说的读取文件的例子:

async function readFileData() {
  const aData = await getFileContent('a.json')
  console.log('a Data ', aData)
  const bData = await getFileContent(aData.next)
  console.log('b Data ', bData)
  const cData = await getFileContent(bData.next)
  console.log('c Data ', cData)
}

是不是一下子就感觉这个代码非常简单了,就像是在写同步任务一样没有任何回调的顾虑,await会帮我们拿到promise的内容,直接用就可以了。到最后,什么回调地狱完全就没有了。

参考:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises

1 thought on “JS-解决回调地狱”

  1. ココロ说道:
    2019年12月29日 14:06

    readFileData 里的 await 调用也可以使用 promise.all 或者 bluebird.props 来管理 promise

    回复

发表评论 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

分类

  • CFC 周刊 (4)
  • CFC 技术 (29)
  • CFC 日常 (3)
  • 未分类 (15)
  • 活动通知 (3)

标签

ACM Android anime animeloop animeloop-cli APP Apple aria2 Array Blog CFC数据结构与算法训练指南 CoreData CQUT Don't Starve Hexo iBooks JavaScript macOS Matlab moeoverflow OpenCV Programming README RxJS SQLite SQLite3 Steam Swift Theme Web Xcode 主题模板 动漫 博客 反编译 妹子 循环 教程 数据库 游戏 算法 装逼 视频 重庆理工大学 饥荒

登录
©2023 CFC Studio | Powered by WordPress & Superb Themes