本文主要说一下回调函数、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) })
其他还有内置中间件和第三方中间件等,可自行了解。
他们试图把你埋了,
但你要记得你是种子。
——墨西哥谚语
评论
Only wanna input on few general things, The website pattern is perfect, the subject matter is rattling great. “I have seen the future and it doesn’t work.” by Robert Fulford.
https://www.tdsky.com/hire-a-hacker-for-iphone/
I got good info from your blog
https://youtu.be/VKUPvmAMtQo