回调地狱以及Promise解决方案
一.前言:
本案例我将介绍回调函数的基本概念与应用,并一步一步的演示回调地狱是怎么产生的,最后是怎么解决回调地狱问题的
二.回调函数是怎么产生的
首先我这里有个需求:打印变量str
function getDate(){
function processData(){
var str="hello world";
}
}
正常的思维可能会这样打印
function getDate(){
function processData(){
var str="hello world";
return str;
}
processData();
}
var res=getDate();
console.log(res);//undefined
但是实际的结果是undefined,为什么呢?
这里涉及到一个概念:异步
什么是异步?
简单理解就是:两件事情同时进行,如边听歌边写作业
既然是两件事情同时在进行,那么就必然会出现:
- 各自结束的时间,二者结束的时间可能相同也可能不同
- 各自结束得到的结果
好了,上面的例子为什么不能打印出str
的?
getDate()
与processData()
是两个不同函数,只是嵌套在一起而已,暂且不管他们结束时间是否一致,但是肯定的是console.log(res)
打印的是getDate()
的返回值,但是getDate()
并没有返回值,所以结果是undefined
那怎么解决,好办
给getData()
一个返回值
function getDate(){
function processData(){
var str="hello world";
return str;
}
return processData();
}
var res=getDate();
console.log(res);//hello world
到了这里,我们确实是实现了打印str
的值,但是,如果要获取异步函数执行的结果,是不是首要考虑return
呢?
下面例子,我的需求是打印add()
执行后的结果
function add(x, y) {
console.log(1)
var res=setTimeout(function () {
console.log(2);
var result = x + y;
return result;
}, 1000)
console.log(3);
return res;
}
console.log(add(20,30));
//结果
1
//
3
//
1
//
2
看到运行结果,是不是很惊讶打印add(20,30)
执行的结果是1而不是50?
那么上例中的结果是怎么得来的,上面的代码到底是怎么执行的?
首先需要明确的一点是,我要打印add(20,30)
的结果,而它的结果就是函数的返回值res
执行函数add(20,30)
时,先执行代码
console.log(1)
然后执行
console.log(3)
然后执行
console.log(add(20,30))
然后执行
定时器里面的代码...console.log(2)...
为什么是这个顺序,以后我会再重新写一篇文章详细介绍
已知定时器函数里面所要执行的是一个异步操作,也许你会认为将异步函数的结果result
返回并赋值给res
,再把res
返回,这样就可以得到add(20,30)
执行的结果了,但是并没有,为什么?
因为实际上res
是系统记录的一个setTimeout
的ID,以后可以通过这个ID取消定时器,那问题来了?定时器里面的匿名函数的结果返回去哪了?
没有变量接收定时器匿名函数返回的结果,也无法接收
到这了,很明显我们无法打印add(20,30)
的结果res
到此,是时候介绍我们的主角了
像上面两个例子的需求在实际的开发中还是会经常遇到的,这时,就需要我们去探索一种有效的解决方法,那就是:回调函数
三.什么是回调函数
回调函数是作为参数传给另一个函数的函数
对于回调函数概念的解释就和闭包一样,并没有唯一解释,但是在实践中总结出来的最接近我们理解的说法是有的,简单的来说,回调函数除了是一个函数外,还是别的函数的一个参数
四.回调函数的基础模型
回调函数的基本模型没有固定的写法,借用上面简单的例子来写一个启发模型:打印sum
function getDate(callback){
function processData(){
var sum=0;
for(var i=0;i<4;i++){
sum++;
}
callback(sum);//调用外部的函数,把里面的值带出去,实现外部访问
}
processData();
}
getDate((data)=>{
console.log(data);
});
五.回调函数的应用
一般情况下,把函数作为参数的目的就是为了获取函数内部的异步操作结果
六.回调地域是怎么产生的
我在同一个目录下分别新建了三个文件:
aa.txt
bb.txt
cc.txt
test.js
其中,aa.txt,bb.txt,cc.txt的内容分别为aaaa,bbbb,cccc
下面是test.js
let path=require("path");
let fs=require('fs');
fs.readFile(path.join(__dirname,"./aa.txt"),"utf8",function(err,data){
if(err) throw err
console.log(data)
})
执行代码,得到aaaa
下面修改代码,使用回调函数来打印
let path=require("path");
let fs=require('fs');
function getFileByPath(fpath,callback){
fs.readFile(fpath,"utf8",function(err,data){
if(err) return callback(err.message)
callback(data)
})
}
strurl=path.join(__dirname,"./aa.txt");
getFileByPath(strurl,(data)=>{
console.log(data);
})
执行代码,得到aaaa
由于上面打印err,data,都是共用同一个callback,为了代码的层次分明,对他们分别使用回调函数,下面修改代码
function getFileByPath(fpath,succb,errcb){
fs.readFile(fpath,'utf8',(err,data)=>{
if(err) return errcb(err.message)
succb(data)
})
}
getFileByPath(path.join(__dirname,'aa.txt'),(data)=>{
console.log(data)
},
(err)=>{
console.log(err)
})
执行代码,得到aaaa
下面我要将aa.txt,bb.txt,cc.txt三个文件的内容依次读取出来,注意是"依次",下面修改代码
fs.readFile(path.join(__dirname,"aa.txt"),"utf8",function(err,data){
console.log(data)
})
fs.readFile(path.join(__dirname,"bb.txt"),"utf8",function(err,data){
console.log(data)
})
fs.readFile(path.join(__dirname,"cc.txt"),"utf8",function(err,data){
console.log(data)
})
重复执行代码,观察结果
从结果可以看出,在重复执行多次代码,发现得到的结果中有出现读取顺序并不是我们预想的结果,这个问题也是由异步引起的,是因为三个读取文件的函数结束时间不一定相同
因此人们采用了这种方案来解决这个问题
fs.readFile(path.join(__dirname,"aa.txt"),"utf8",function(err,data){
console.log(data)
fs.readFile(path.join(__dirname,"bb.txt"),"utf8",function(err,data){
console.log(data)
fs.readFile(path.join(__dirname,"cc.txt"),"utf8",function(err,data){
console.log(data)
})
})
})
这个结构就保证了函数执行的顺序,但是如果我们要读取很多的文件,那不是要这样一直嵌套下去?
这就是回调地域问题
七.回调地域写法的缺点
- 代码层次不够分明,视觉观感不好
- 嵌套的代码中如果有其中一个报错,那么下面的就不执行了,致命缺点
八.解决回调地狱
为了解决回调地狱的写法带来的缺点,可以采用Promise解决方案
下面用Promise来读取单个文件
let p=new Promise(function(res,rej){
fs.readFile(path.join(__dirname,'aa.txt'),'utf8',function(err,data){
if(err) rej(err.message)
else res(data)
})
})
p.then((data)=>{
console.log(data)//aaaa
})
下面将aa.txt,bb.txt,cc.txt三个文件的内容依次读取出来
function fn(fpath){
return new Promise(function(res,rej){
fs.readFile(fpath,"utf8",function(err,data){
res(data)
})
})
}
fn(path.join(__dirname,"aa.txt"))
.then((data)=>{
console.log(data)
return fn(path.join(__dirname,"bb.txt"))
})
.then((data)=>{
console.log(data)
return fn(path.join(__dirname,"cc.txt"))
})
.then((data)=>{
console.log(data)
})
九.什么是Promise
特别声明
以下内容参考或引用自:
1.Promise的含义
Promise 是异步编程的一种解决方案,所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果,这个事件通常是一个异步操作,从语法上说,Promise 是一个对象,而这个对象是一个构造方法,从它可以获取异步操作的消息。
// 下面的代码可以直接运行在浏览器的控制台中(Chrome浏览器)
> typeof Promise
"function" // 可以看出这是一个构造函数
> Promise
function Promise() { [native code] } // ES6的原生支持
2.Promise的特点
(1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
注意,为了行文方便,本章后面的resolved
统一只指fulfilled
状态,不包含rejected
状态。有了Promise
对象,就可以将异步操作以"同步操作"的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
3.Promise实例化
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
注意:理解这些状态的变化,可以结合生命周期的概念进行理解
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise
对象传出的值作为参数。
好了,我这里对于Promise相关知识的介绍就这么多,Promise更多的知识点可以到上面我给的那两个参考的链接进行学习
3.Promise的.then()
方法
一旦创建一个Promise对象之后,我们就可以使用then方法来进行链式的调用,而且我们可以把每一次的结果都返还给下一个then方法,然后在下一个then方法中对这个值进行处理。每一个then方法中都可以再次新创建一个Promise对象,然后返还给下一个then方法处理。
下面,回头看看我们为了解决回调地狱的是怎么应用Promise的?
let p=new Promise(function(res,rej){
fs.readFile(path.join(__dirname,'aa.txt'),'utf8',function(err,data){
if(err) rej(err.message)
else res(data)
})
})
p.then((data)=>{
console.log(data)//aaaa
})
-
生成实例化对象P的时候,Promise构造方法中传进一个函数,函数有两个参数,分别是
res
和rej
-
函数里面做一个异步动作,读取文件aa.txt
-
如果读取失败,
rej(err.message)
将异步动作失败结果作为参数传递出去 -
否则
res(data)
将异步动作成功结果作为参数传递出去
下面将aa.txt,bb.txt,cc.txt三个文件的内容依次读取出来
function fn(fpath){
return new Promise(function(res,rej){
fs.readFile(fpath,"utf8",function(err,data){
res(data)
})
})
}
fn(path.join(__dirname,"aa.txt"))
.then((data)=>{
console.log(data)
return fn(path.join(__dirname,"bb.txt"))
})
.then((data)=>{
console.log(data)
return fn(path.join(__dirname,"cc.txt"))
})
.then((data)=>{
console.log(data)
})
- 直接调用
fn()
,并传入参数执行异步动作,返回promise
实例, - 用
.then()
函数并传入回调函数,接收上一步传出来的值,并返回一个新的动作 - 以此类推直到最后一步