回调函数一旦成了魔

1,126 阅读10分钟

回调地狱以及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

特别声明

以下内容参考或引用自:

知乎未来科技专栏-ES6 Promise

阮一峰-ECMAScript 6 入门

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构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 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构造方法中传进一个函数,函数有两个参数,分别是resrej

  • 函数里面做一个异步动作,读取文件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()函数并传入回调函数,接收上一步传出来的值,并返回一个新的动作
  • 以此类推直到最后一步