本文主要说一下回调函数、Promise以及中间件的概念与用法。

 

异步操作与回调函数

在实际开发中有些操作会耗费一些时间来获得结果,例如文件读写、网络请求、延时函数等等,这种时候就会采取异步操作避免阻塞等待,而如果需要获取异步操作的结果,必须通过回调函数。

var fs = require('fs')

console.log(1)

new Promise(function () {
  console.log(2)
  fs.readFile('./test.js', 'utf8', function (err,data) {
    if (err) {
      console.log(3)
    } else {
      console.log(4)
    }
  })
  console.log(5)
})

console.log(6)

当异步操作成功时,err为null,data为操作结果;当异步操作失败时,err为报错信息,data为undefined。

 

Promise避免回调地狱

当有多个异步任务同时进行时,无法保证每个任务的顺序,当其中一个异步任务依赖于另一个异步任务的结果时,就不得不写成嵌套式结构,层层嵌套既不美观也难以维护,这种情况就被称为“回调地狱”。

 

Promise使用

而为了避免回调地狱,在EcmaScript6中,新增了Promise这个API,它是一个容纳异步任务的容器。

var fs = require('fs')

console.log(1)

new Promise(function () {
  console.log(2)
  fs.readFile('./test.js', 'utf8', function (err,data) {
    if (err) {
      console.log(3)
    } else {
      console.log(4)
    }
  })
  console.log(5)
})

console.log(6)

最终会出现的结果顺序是12564。

从上面的顺序可以看出,Promise虽然是异步任务的容器,但其本身是同步任务,一旦创建立即开始运行,因此Promise一般作为函数的返回值,在需要调用时执行。

Promise容器默认是Pending状态,而运行后可能出现以下两种状态。

  • 通过resolve(data)改变成fulfilled状态,返回结果
  • 通过reject(err)改变成Rejected状态,返回异常。
var fs = require('fs')

var P1 = () => new Promise(function (resolve, reject) {
  fs.readFile('./a.js', 'utf8', function (err,data) {
    if (err) {
      reject(err)
    } else {
      resolve(data)
    }
  })
})

// 对结果进行处理,第二个异常回调可以省略
P1().then(function (data) {
  console.log(data)
}, function (err) {
  console.log('failed')
})

// 或者使用then来处理正常结果,使用catch来处理异常
P1().then(function (data) {
  console.log(err.message)
})
.catch(function (err) {
  console.log(err.message)
})

需要注意的是Promise的状态改变之后才会执行then方法。

 

then的实质

上面演示了使用then来处理异步回调的用法,下面则要说明then最烧脑的地方——then的返回值也是Promise对象,并且是未运行的Pending状态,可继续链式连接then执行。

then返回的Promise对象的状态可以在浏览器中断点调试状态进行查看,此处不再赘述,可查看此教程

const p = () => new Promise((resolve,reject) => {
  resolve('123')
})

p().then((value)=>{
    console.log(value)   //123
  },(reason)=>{
    console.log(reason)
})
.then((value)=>{
    console.log('test')  //test
    console.log(value)   //undefined
  },(reason)=>{
    console.log(reason)
})

从上面的结果可以看出,第一个then并未resolve,但仍然触发了第二个then,说明第一个then返回的Promise对象在不调用resolve()的情况下,会在运行完后默认自动resolve且不返回结果(因为then中没有resolve()和reject()方法)。

而在then中也可以通过return返回结果,而此时then的返回值将是一个fulfilled的Promise对象,并触发下一个then的正常处理方法。

const p = () => new Promise((resolve,reject) => {
  resolve('123')
})

p().then((value)=>{
    console.log(value) //123
    return(value)
  },(reason)=>{
    console.log(reason)
})
.then((value)=>{
    console.log('test') //test
    console.log(value)  //123
  },(reason)=>{
  console.log(reason)
})

而在then中运行的代码如果报错,则当前then的返回值将是一个rejected的Promise对象,并且触发下一个then的异常处理方法。

const p = () => new Promise((resolve,reject) => {
  reject('123')
})

p().then((value)=>{
    console.log(value)
  },(reason)=>{
    console.log(rs)
})
.then((value)=>{
    console.log(value)
  },(reason)=>{
    console.log('test')          //test
    console.log(reason.message)  //rs is not defined
})

总结一下,一个new Promise必须运行并修改状态后才能触发第一个then,而then得到的返回值promise对象则运行后自动修改状态并触发下一个then,直到整条then链式结束。

现在来看catch方法,其实就是that的语法糖,catch(do(err))等价于that(undefined, do(err)),而finally(do(data))等价于that(do(data), do(err))。

那么下面来看一个例子,在浏览器中运行以下代码会得到不一样的结果:

fetch('/').then((res)=>res.text()).then(data=>console.log(data.length))
// 29450
fetch('/').then((res)=>res.text().length)
// undefined

fetch命令的返回值是一个Promise对象,而res.text()返回值也是一个Promise对象,Promise对象没有length属性,因此第二种方式自然是错误的。

 

Promise处理回调地狱

而如果要实现顺序调用,则可以根据链式执行,在P1成功的回调函数中返回P2,再由P2向下运行,如下。

var fs = require('fs')

var P1 = () => new Promise(function (resolve, reject) {
  fs.readFile('./a.js', 'utf8', function (err,data) {
    if (err) {
      reject(err)
    } else {
      resolve(data)
    }
  })
})

var P2 = () => new Promise(function (resolve, reject) {
  fs.readFile('./app.js', 'utf8', function (err,data) {
    if (err) {
      reject(err)
    } else {
      resolve(data)
    }
  })
})

// P1.then()执行成功时返回P2的对象,再链式P2.then()
P1().then(function (data) {
  console.log(data)
  return P2()
}, function (err) {
  console.log('failed')
})
.then(function (data) {
  console.log(data)
})

 

async&await

async&await其实是Promise&then的语法糖,能够更加直观地处理回调地狱。

基本使用方式如下。

var fs = require('fs')

var P1 = () => new Promise(function (resolve, reject) {
  fs.readFile('./a.js', 'utf8', function (err,data) {
    if (err) {
      reject(err)
    } else {
      resolve(data)
    }
  })
})

async function Q1() {
  var res = await P1()
  console.log(res)
}

Q1()

在处理回调地狱时的运行过程如下

var P1 = (count) => new Promise(function (resolve, reject) {
  console.log(1)
  setTimeout(function(){
    console.log(count)
    resolve(count+1)
  },1000)
  console.log(2)
})

async function Q1() {
  console.log(3)
  var res1 = await P1(100)
  console.log(4)
  var res2 = await P1(res1)
  console.log(5)
}

Q1()
console.log(6)

最终结果是立即输出3、1、2、6,经过1秒后输出100、4、1、2,再过1秒后输出101、5。

从顺序可以看出async修饰的方法就是顺序调用链本身,而且不阻塞方法后方代码的运行,方法内部则会等待异步任务的结果,并且按照顺序执行异步任务。

但是这种async&await方式有一个缺点就是await只能获取resolve传出的值结果,如果调用链中出现错误就只能通过try&catch获得。

async function Q1() {
  try{
    var res1 = await P1(100)
    var res2 = await P1(res1)
  }catch(e){
    console.log(e)
  }
}

 

中间件

中间件的本质就是一个请求处理方法,以express为例,把用户从请求到响应的整个过程分发到多个中间件中去处理,从而提高代码的灵活性和动态扩展性。

 

匹配中间件

app.use(function(req,res,next){
  console.log(1)
  next()
})

app.use(function(req,res,next){
  console.log(2)
})

app.use(function(req,res,next){
  console.log(3)
})

上面这种情况是全局匹配,所有访问都会进入app.use进行处理,而默认情况下匹配成功一次后就不会再尝试匹配下面的两个app.use,除非有next()传递到下一个app.use。因此,上面的结果是1、2,第三个app.use甚至后面的app.get等都会失效。

因为会匹配所有请求,因此一般用来处理404错误,位置放在其他路由之后。

当匹配多次时,多次处理的是同一个请求和响应,可以理解为链式执行。

app.use('/a',function(req,res,next){
  console.log(2)
  next()
})
app.use('/b',function(req,res,next){
  console.log(3)
})

这种情况下是限定匹配,例如app.use(‘/a’)代表匹配以”/a/”开头的请求,例如127.0.0.1/a/sdf,结果为2,匹配成功一次后也不会再匹配后面的中间件,因此next()的使用是要注意的。

而app.get以及app.post等其实就是特殊的app.use,称为路由级别中间件,区别只是匹配完整路径并且限定请求方式而已。

 

全局错误处理中间件

默认情况下会在单个匹配中间件中处理错误,但也可以通过next将错误发送到全局错误处理中间件进行统一处理,因此位置也应该放在其他路由之后。

app.get('/', function (req, res, next) {
  fs.readFile('test',function (err, data) {
    if (err) {
      // 默认情况下的处理报错
      // return res.status(500).send('Server Error')
      // 传送到全局错误
      next(err)
    }
  })
})
app.use(function (err, req, req, next) {
  console.log(err.message)
})

其他还有内置中间件和第三方中间件等,可自行了解。

 


他们试图把你埋了,

但你要记得你是种子。

——墨西哥谚语