谈及回调地狱发生得情况和解决办法,就必须追溯到原生ajax请求。
先列出服务器提供的数据接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 服务器端接口 app.get( '/data1' , (req, res) => { res.send( 'hi' ) }) app.get( '/data2' , (req, res) => { res.send( 'hello' ) }) app.get( '/data3' , (req, res) => { res.send( 'nihao' ) }) // 启动监听 app.listen(3000, () => { console.log( 'running...' ) }) |
原生ajax请求步骤
1 2 3 4 5 6 7 8 9 10 | var xhr = new XMLHttpRequest(); xhr.open( 'get' , 'http://localhost:3000/data1' ); xhr.send( null ); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { // 获取后台数据 var ret = xhr.responseText; console.log(ret) } } |
又因为发送请求的以上代码需要经过反复复用,我们就需要将它封装为函数,以减少代码的冗余度。下面请看两个封装方法(都是错误的封装):
错误封装1:发生错误的原因是queryData函数本身没有返回值,会默认返回undefined,return ret是写在queryData的内层函数中的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function queryData(path) { var xhr = new XMLHttpRequest(); xhr.open( 'get' , 'http://localhost:3000/' +path); xhr.send( null ); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { // 获取后台数据 let ret = xhr.responseText; return ret; } } } let res = queryData(‘data1’); console.log(res); // 结果为:undefined |
这样很容易就让我们想到另一种封装方法——把ret在外层函数中返回。然而这就产生了另一种错误的封装效果。
错误封装2:这种情况下发生错误的原因是ajax请求时异步的,数据ret还没有修改成功时,就已经执行了queryData函数的return代码。这时ret的值并没有被修改,当然还是null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function queryData(path) { var xhr = new XMLHttpRequest() xhr.open( 'get' , 'http://localhost:3000/' +path) xhr.send( null ) var ret = null xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { // 获取后台数据 ret = xhr.responseText } } return ret } let res = queryData( 'data1' ) console.log(res) // 结果为:undefined |
要想执行异步操作代码的返回内容,就需要使用回调函数,下面介绍一种正确的封装方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function queryData(path, callback) { var xhr = new XMLHttpRequest(); xhr.open( 'get' , 'http://localhost:3000/' + path); xhr.send( null ); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { // 获取后台数据 var ret = xhr.responseText; callback(ret); } } } queryData( 'data1' , function (ret) { console.log(ret) // 结果为:hi }) |
但是,如果想要按顺序获取接口'data1'、'data2'、'data3'中的数据,就会进行下面的操作,也就造成了回调地狱的问题。
1 2 3 4 5 6 7 8 9 | queryData( 'data1' , function (ret) { console.log(ret) // 按顺序第一个输出为:hi queryData( 'data2' , function (ret) { console.log(ret) //按顺序第二个输出为:hello queryData( 'data3' , function (ret) { console.log(ret) // 按顺序第三个输出为:nihao }); }); }); |
promise方式
为了改造上面的回调地狱问题,诞生了promise。promise其实就是一种语法糖(代码形式发生改变、但是功能不变)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | function queryData(path) { return new Promise( function (resolve, reject) { // 需要在这里处理异步任务 var xhr = new XMLHttpRequest(); xhr.open( 'get' , 'http://localhost:3000/' + path); xhr.send( null ); xhr.onreadystatechange = function () { // 该函数何时触发?xhr.readyState状态发生变化时 if (xhr.readyState != 4) return ; if (xhr.readyState == 4 && xhr.status == 200) { // 获取后台数据 var ret = xhr.responseText; // 成功的情况 resolve(ret); } else { // 失败的情况 reject( '服务器错误' ); } } }) } queryData( 'data1' ) .then(ret=>{ console.log(ret) // 按顺序第一个输出为:hi // 这里返回的是Promise实例对象,下一个then由该对象调用 return queryData( 'data2' ); }) .then(ret=>{ console.log(ret); // 按顺序第二个输出为:hello return queryData( 'data3' ); }) .then(ret=>{ console.log(ret) // 按顺序第三个输出为:nihao }) |
对于上面代码中使用.then调用的情况,有几点说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 | queryData( 'data1' ) .then(ret=>{ console.log(ret) // 顺序输出第一个结果为:hi // 如果在then方法中没有返回Promise实例对象,那么下一个then由默认产生的Promise实例对象调用 }) .then(ret=>{ console.log( '-------------------' + ret) // 顺序输出第二个结果为:----------------------undefined // 如果在then中显式地返回一个具体数据,那么下一个then可以获取该数据 return 456; }) .then(ret=>{ console.log( '-------------------' + ret) // 顺序输出第三个结果为:----------------------456 }) |
上面的代码第二个.then中return的是456,为什么能继续调用.then方法呢?
这是因为return 456 实际上可以理解为下面的三种表示方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // Promise.resolve的作用:就是把数据转化为Promise实例对象 // 方式一: return Promise.resolve(456); // 方式二: return new Promise( function (resolve, reject) { resolve(99999); }) // 方式三: Promse.resolve = function (param) { return new Promise( function (resolve, reject) { resolve(param); }) } return Promise.resolve(88888); |
promise对象除了.then方法外还有两个方法可以通过 . 调用,其中.finally是ES7中新增的方法。
1 2 3 4 5 6 7 8 | . catch (ret=>{ // 发生错误时触发 console.log( 'error' ) }) .finally(ret=>{ // 无论结果成功还是失败都触发:一般用于释放一些资源 console.log( 'finally' ) }) |
虽然使用了promise对象,但是一路通过 . 调用方法进行下去,代码的可读性较差。
async和await
下面我们就提出解决回调地狱最好的一种方法,通过使用 async 和 await 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | function queryData(path) { return new Promise( function (resolve, reject) { // 需要在这里处理异步任务 var xhr = new XMLHttpRequest(); xhr.open( 'get' , 'http://localhost:3000/' + path); xhr.send( null ); xhr.onreadystatechange = function () { // 当readyState值不为0的时候直接返回 if (xhr.readyState != 4) return ; if (xhr.readyState == 4 && xhr.status == 200) { // 获取后台数据 var ret = xhr.responseText; // 成功的情况 resolve(ret); } else { // 失败的情况 reject( '服务器错误' ); } } }) } async function getAllData() { // await执行流程是顺序执行 let ret1 = await queryData( 'data1' ); let ret2 = await queryData( 'data2' ); let ret3 = await queryData( 'data3' ); console.log(ret1) console.log(ret2) console.log(ret3) } getAllData(); |
另外,有一点需要提起注意:async函数的返回值是Promise实例对象
1 2 3 4 5 6 7 8 9 10 | async function getAllData() { // await执行流程是顺序执行 let ret1 = await queryData( 'data1' ); return 'hello' ; } var ret = getAllData(); console.log(ret) // 这里输出一个promise对象,并且resolve的数据为hello ret.then(res=>{ console.log(res) // 这里输出结果为:hello }) |