...

JavaScript回调函数&Promise总结「下」

文章分为上下两部分。上篇讲了为什么要使用回调函数和怎么写回调函数。
上一篇链接:JavaScript回调函数&Promise总结「上」
下篇讲优化回调函数。回调函数解决异步调用2个问题。

  • 顺序不确定。▶︎ 使用回调函数解决顺序 ▶︎ 副作用回调函数嵌套地狱问题
  • 返回结果无法取回。

1. 为什么要优化回调函数

1-1 解决异步返回值顺序不定的问题

异步操作,返回值的顺序是不知道的,至于按照的是什么顺序,那么就要看系统机制,系统机制这种飘忽不定的东西肯定是不行的,那么如何让返回值按照我们想要的顺序呢。

比如b()函数的操作需要a()函数的结果,但是a,b都是异步操作,那么如何让b拿到a的异步操作结果呢。
可以用回调函数嵌套的方法。下面上代码。▼

先做一个小 demo,异步读取3个 txt 文件。readFile()是异步操作的函数。

// 版本1 回调函数读取3个文件,顺序不保证  
const fs = require('fs')  
const path = require('path')  

fs.readFile(path.join(__dirname, "./1.txt"),'utf8',function(err,data1){  
    if(err){ throw err }  
    console.log(data1)  
})  

fs.readFile(path.join(__dirname, "./2.txt"),'utf8',function(err,data2){  
    if(err){ throw err }  
    console.log(data2)  
})  

fs.readFile(path.join(__dirname, "./3.txt"),'utf8',function(err,data3){  
    if(err){ throw err }  
    console.log(data3)  
})  

1-2 回调函数嵌套解决顺序问题

回调函数嵌套可以搞定顺序问题,保证1,2,3这样的输出

// 版本2 回调函数嵌套读取3个文件。保证1,2,3顺序。  
const fs = require('fs')  
const path = require('path')  

fs.readFile(path.join(__dirname, "./1.txt"),'utf8',function(err,data1){  
    if(err){ throw err }  
    console.log(data1) // 1.txt  

    fs.readFile(path.join(__dirname, "./2.txt"),'utf8',function(err,data2){  
        if(err){ throw err }  
        console.log(data2) // 2.txt  

        fs.readFile(path.join(__dirname, "./3.txt"),'utf8',function(err,data3){  
            if(err){ throw err }  
            console.log(data3) // 3.txt  
        })  
    })  
})  

1-3 回调嵌套地狱

调用1个返回值还好,如果是3个,或者n个呢。比如异步同时打开3个文件。如下。
readFile() 函数为异步操作。

这样一层一层的调用下去岂不是很恶心吗?

为了解决这个问题,就引申出来了 JavaScript 的一个很好用的专门解决回调函数的 API Promise

2. Promise

  • Promise 是 EcmaScript6 之后新增的构造函数。
  • new 之后容器一旦创建就立即执行代码
  • Promise并没有给你提供一个新的语法,只是对于原有语法的封装。
  • 默认状态是 pending,成功过就是resolve,错误就是reject。

直接写一下Promise怎么写吧。

// 版本3 Promise是什么引入  
new Promise(function(resolve, reject){  
    fs.readFile(path.join(__dirname, "./1.txt"),'utf8',(err, data)=>{  
        if(err){  
            reject(err) // 错误  
        } else {  
            resolve(data) // 成功  
            console.log(data)  
        }  
    })  
})  

如果把上面那个小demo按照Promise的写法版本是这样的。

// 版本4 开始使用Promise进行多个异步操作  
const fs = require('fs')  
const path = require('path')  
// p1  
let p1 = new Promise(function(resolve, reject){  
    fs.readFile(path.join(__dirname,"./1.txt"),"utf8",function(err, data1){  
        if(err){reject(err)} else {resolve(data1)}  
    })  
})  
// p2  
let p2 = new Promise(function(resolve, reject){  
    fs.readFile(path.join(__dirname,"./2.txt"),"utf8",function(err, data2){  
        if(err){reject(err)} else {resolve(data2)}  
    })  
})  
// p3  
let p3 = new Promise(function(resolve, reject){  
    fs.readFile(path.join(__dirname,"./3.txt"),"utf8",function(err, data3){  
        if(err){reject(err)} else {resolve(data3)}  
    })  
})  

如果优雅的顺序操作写法就要用到下面的then()链式编程。

2-1 then()

■ 基本用法

then() 的基本写法就是这样。
p1.then(function(data){结果},function(err){输出错误信息})

p1  
.then(function(data){  
    console.log(data)  
    return 'hello data' // ↓ 这里写什么 下面的then()链式编程返回什么  
},function(err){  
    console.log("err is ",err)  
})  
.then(function(data){  
    console.log(data) // ↑ hello data  
})  

// 如果是return什么就返回什么的话  
// 那么retunrn Promise 的实例对象 呢  
p1  
.then(function(data){  
    console.log(data)  
    return p2  
},function(err){  
    console.log("err is ",err)  
})  
.then(function(data){  
    console.log(data) // 这里返回的就是p2实例的结果  
})  

■ 完整案例

按照上面的思路 then() 的写法进行对上面那个小demo的改进

// 版本5 then()进行优化异步操作  

const fs = require('fs')  
const path = require('path')  
let p1 = new Promise(function(resolve,reject){  
    fs.readFile(path.join(__dirname,"./1.txt"),"utf8",function(err,data){  
        if(err){reject(err)} else {resolve(data)}  
    })  
})  
let p2 = new Promise(function(resolve,reject){  
    fs.readFile(path.join(__dirname,"./2.txt"),"utf8",function(err,data){  
        if(err){reject(err)} else {resolve(data)}  
    })  
})  
let p3 = new Promise(function(resolve,reject){  
    fs.readFile(path.join(__dirname,"./3.txt"),"utf8",function(err,data){  
        if(err){reject(err)} else {resolve(data)}  
    })  
})  

p1  
.then(function(data){  
    console.log('START')  
    console.log(data)  
    return p2   
},function(err){console.log('err is ',err)})  
.then(function(data){ //这里传入的data其实就是return p2里  
    console.log(data)  
    return p3  
})  
.then(function(data){  
    console.log(data)  
    console.log('END')  
})  

■ 继续封装

上面写的已经ok了,new Promise那个部分代码太多,那么就封装一下。

// 版本6 封装new Promise()  
const fs = require('fs')  
const path = require('path')  
function pReadFile(filePath){  
    return  new Promise(function(resolve,reject){  
        fs.readFile(path.join(__dirname,filePath),"utf8",function(err,data){  
            if(err){reject(err)} else {resolve(data)}  
        })  
    })  
}  
pReadFile("./1.txt")  
.then(function(data){  
    console.log("START")  
    console.log(data)  
    return pReadFile("./2.txt")  
})  
.then(function(data){  
    console.log(data)  
    return pReadFile("./3.txt")  
})  
.then(function(data){  
    console.log(data)  
    console.log("END")  
})  

2-2 async

上面的 Promise 虽然解决了回调函数嵌套地狱的问题,但是看起来还是很臃肿。
EcmaScript 7 引入了 async 关键字转换为异步函数的功能。是解决异步编程的终极方案。

■ 基本写法

普通函数前面加上 async 就变成了异步函数
MDN参考:async_function

// 普通函数  
const fn = () =>{}  

// 异步函数  
const fn = async () => {}  
//写成这样也对  
async function fn (){  
}  

其实 async 异步函数默认的返回的是一个 Promise 对象

async function fn () {}  
console.log(fn()) // Promise { undefined }  

// 异步函数的返回值就是在原有的函数基础上在外面包裹了一层Promise对象  
async function fn () { return 'hello async'}  
console.log(fn()) // Promise { 'hello async' }  

return 返回的 Promise 对象中 return 关键字就代替了 resolve() 方法

■ 普通函数 PK 异步函数

也就是说,异步函数内部,省了你 new Promise() 这个步骤了

// 普通函数Promise变异步操作  
function fn(){  
    return new Promise(function(){  
        console.log("hello fn")  
    })  
}  
fn().then(function data(){  
    console.log(data)  
}) // hello fn  

// async异步函数直接写  
 async function fn(){  
     return "hello async"  
 }  
 fn().then(function(data){  
     console.log(data)  
 }) //hello async  

2-3 await

利用异步函数捕获异步操作错误的用法是这样写的

 // ERROR 写法  
 async function fn (){  
     throw "ERROR"  
     return "hello async"  
 }  
 fn().then(function(data){  
     console.log(data)  
 }).catch(function(err){  
     console.log(err)  
 }) // ERROR  

throw 执行到错误地方停止执行并且返回 err 到下面进行输出。
这样写也太麻烦了,于是 await 又登场了,他有几个需要注意的地方。

  • await 关键字只出现在异步函数中,其他函数没用。
  • await 后面只能接上Promise 的 API,其他没用。
  • await 可以暂停异步函数向下执行,直到 Promise 返回结果

■ 基本写法

async function p1 (){return "p1"}  
async function p2 (){return "p2"}  
async function p3 (){return "p3"}  

async function run(){  
    let r1 = await p1()  
    let r2 = await p2()  
    let r3 = await p3()  
    console.log(r1)  
    console.log(r2)  
    console.log(r3)  
}  
run()   // p1 p2 p3  

剩下的以后再写。累了。

总结

从异步操作 ▶︎ 回调函数 ▶︎ Promise 整体思路

想一个函数不按照顺序来执行

异步操作

需要异步return的结果 因为异步结果在函数之前就return出来了,出来undefined

需要回调函数

回调函数副作用回调嵌套地狱 ( *`ω´)!!

Promise的API来拯救

API里有then()解决回调地狱脏不垃圾的写法

使用async更加优雅

最终大法await

函数内部异步操作的结果需要回调函数才能拿到!这就是使用回调函数的意义。
后面的一堆Promise的使用,都是为了解决回调函数地狱这个副作用所服务的。

共有评论(0)

登陆即可评论哦