本文主要说一下回调函数、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) })
其他还有内置中间件和第三方中间件等,可自行了解。
他们试图把你埋了,
但你要记得你是种子。
——墨西哥谚语
评论
还没有任何评论,你来说两句吧!