promise是ES6推出的一个新的对象,专门为了解决回调地狱的问题的。
回调函数:作为函数参数的函数,在函数执行完毕之后再执行,有同步回调和异步回调两种,同步回调:主函数等待回调函数执行完毕之后再执行,异步回调:主函数无需等到回调函数完成可继续执行,此时的回调函数被放置任务队列中。
我们都知道JS是单线程运行的,当遇到异步的任务时,它会将异步任务放置到队列中去,不会执行异步任务,异步任务只有到主程序执行完之后再去执行。举个常见的例子:
for(var i=0;i<5;i++){
setTimeOut(function(){
console.log(i)
},1000)
}
结果输入5个5,这时因为setTimeOut是一个异步操作,只有等主程序的for循环完毕之后才会输出,因此输出的i为5个5。
正是因为JS是一个单线程,所以JS推出了异步操作来解决JS在某些情况下的不适用 ,如循环等死的情况。
问题来了,既然主程序在执行完毕之后就会执行任务队列,异步操作都会被放置在任务队列里,那么什么时候知道该异步操作完成了呢?看下面的例子:
const fs =require('fs');
const path =require('path');
function getFile(){
fs.readFile(path.join(__dirname,'./file/1.txt'),'utf-8',(err,dataStr)=>{ //异步操作
if(err) throw err
console.log('异步操作发生')
return dataStr //返回1.txt的内容
});
};
var file1=getFile();
console.log(file1); //undefined
这里为什么会出现undefined呢?按照道理来说file1的值应该是1.txt的内容,但是,我们说了异步操作会放到任务队列里,所以主程序在执行异步操作语句时,会将此异步操作放到任务队列中,转而执行完主程序再执行任务队列,所以getFile中没有一个return,返回的值是undefined。
如何解决这个问题呢?我们可以创建一个异步回调函数,回调函数的特点是,作为函数参数的函数,能处理异步操作返回后的结果,当异步操作完成之后执行回调函数,主程序执行完之后,执行回调函数。
const fs =require('fs');
const path =require('path');
function getFile(unpath,callback){
fs.readFile(unpath,'utf-8',(err,dataStr)=>{ //异步操作
//当有错误时,就不执行后续的代码了
if(err) return callback(err.message)
return callback(dataStr) //返回1.txt的内容
});
};
getFile(path.join(__dirname,'./file/1.txt'),(dataStr)=>{ //这是一个回调函数
console.log(dataStr); //1.txt的内容
});
getFile中的第二个参数是一个回调函数,当异步操作有了结果之后,再执行回调函数(回调回调,回头调用的意思),但是此时的回调函数还没有被主函数执行到,仍然是在任务队列中,只有主程序执行完了才会去执行回调函数。不信我们看下面的例子
function b(){
setTimeout(function(){ //模拟异步操作
console.log("回调函数")},1000)
}
function a(callback){
callback();
console.log('主函数执行完毕');
}
a(b);
//输出:
// 主函数执行完毕
// 回调函数
相关的回调函数的解释:https://www.cnblogs.com/moxiaowohuwei/p/8438236.html
回调地狱:指在回调函数中嵌套回调函数,如此嵌套多层回调函数导致,代码不够整洁,容易出现错误等
看下面的例子:
const fs =require('fs');
const path =require('path');
//需求:读取1.txt后再读取2.txt再读取3.txt
function getFile(unpath,succd,errcd){
fs.readFile(unpath,'utf-8',(err,dataStr)=>{ //异步操作
//当有错误时,就不执行后续的代码了
if(err) return errcd(err.message)
return succd(dataStr) //返回内容
});
};
getFile(path.join(__dirname,'./file/1.txt'),function(data){
console.log('成功了:'+data); //1.txt的内容
getFile(path.join(__dirname,'./file/2.txt'),function(data){
console.log('成功了:'+data); //2.txt的内容
getFile(path.join(__dirname,'./file/3.txt'),function(data){
console.log('成功了:'+data); //3.txt的内容
},function(err){
console.log('失败了:'+err) //1.txt的错误处理
});
},function(err){
console.log('失败了:'+err) //2.txt的错误处理
});
},function(err){
console.log('失败了:'+err) //3.txt的错误处理
});
这里的先读取1再读取2再读取3,看着不难理解,但是如果是要读取10000个文件呢?那时候代码的混乱程度绝不一般。这就是回调地狱:在回调中调用回调,回调的回调再调用回调。。。。
promise的出现正式解决了回调地狱的问题,Promise代表的是一个异步操作对象
Promise创建之后会立即执行,所以要将promise对象封装在一个函数中,当函数被调用时才执行promise的构造函数
var promise=new Promise(function(){
fs.readFile(path.join(__dirname,'./file/1.txt'),'utf-8',function(err,data){
if(err) return err
console.log(data); //输出1.txt内容
})
});
//立即输出1.txt,哪怕我们没有调用
//使用一个函数将promise对象封装起来,当函数被调用时,才被调用
function getFile(){
var promise=new Promise(function(){
fs.readFile(path.join(__dirname,'./file/1.txt'),'utf-8',function(err,data){
if(err) return err
console.log(data); //输出1.txt内容
})
});
}
我们要使用Promise的话,要用到Promise对象的方法,那么就需要在封装Promise的函数中将Promise return出来:
function getFile(){
return new Promise(function(){
fs.readFile(path.join(__dirname,'./file/1.txt'),'utf-8',function(err,data){
if(err) return err
console.log(data)
})
})
}
在控制台输入:console.dir(Promise):
可以看到Promise有各种方法,比较重要的有:.then(编写回调函数)、.catach(捕获异常)、resolve(成功回调)、reject(失败回调)
我们要在Promise的构造函数中,使用resolve执行成功的回调函数,使用reject执行失败的回调函数。
function getFile(path){
return new Promise(function(resolve,reject){ //resolec和reject是形参
fs.readFile(path),'utf-8',function(err,data){
if(err) reject(err) //失败的操作,执行reject回调函数
resolve(data) //成功的操作
})
})
}
这样getFile是一个合格的promise对象了,在调用getFile时,我们还需要使用.then为异步操作指定resovle成功回调和reject失败回调函数,reject可以不写,但是resolve一定要写。
function getFile(path){
return new Promise(function(){
fs.readFile(path,'utf-8',function(err,data){
if(err) reject(err)
resolve(data)
})
})
}
getFile(path.join(__dirname,'./file/1.txt')).then(function(data){
console.log(data); //成功的回调,实现了resolve
},function(err){
console.log(err); //失败的回调,实现了reject
})
总结一下,创建Promise的步骤
-
要想正确的使用Promise,那么就要创建一个函数封装Promise对象,并return出去
-
在Promise的构造函数中,写入异步操作的语句,使用resolve()执行成功操作,reject()执行失败操作
-
封装Promise的函数.then(resolve,reject)编写成功和失败的回调函数,reject可以不写,一旦遇到失败就会停止执行,当你给每一个Promise写rejct的时候,那么就算遇到错误也可以继续执行
Promise解决回调地狱的问题:
不使用Promise:
const fs =require('fs');
const path =require('path');
//需求:读取1.txt后再读取2.txt再读取3.txt
function getFile(unpath,succd,errcd){
fs.readFile(unpath,'utf-8',(err,dataStr)=>{ //异步操作
//当有错误时,就不执行后续的代码了
if(err) return errcd(err.message)
return succd(dataStr) //返回内容
});
};
getFile(path.join(__dirname,'./file/1.txt'),function(data){
console.log('成功了:'+data); //1.txt的内容
getFile(path.join(__dirname,'./file/2.txt'),function(data){
console.log('成功了:'+data); //2.txt的内容
getFile(path.join(__dirname,'./file/3.txt'),function(data){
console.log('成功了:'+data); //3.txt的内容
},function(err){
console.log('失败了:'+err) //1.txt的错误处理
});
},function(err){
console.log('失败了:'+err) //2.txt的错误处理
});
},function(err){
console.log('失败了:'+err) //3.txt的错误处理
});
promise怎么解决回调地狱的呢?在.then中的resolve中return 下一次执行的回调,在此结果上再使用.then,当然了,要想再某一层抛出错误时继续执行,那么就再reject上也return下一次的的回调
// 需求:先读取1.txt再读取2.txt再读取3.txt
// Promise对象带有resolve(成功回调) reject(失败回调)
// Promise传入的构造函数
function getFile(path){
//返回一个prommise对象,这个函数getFile就是一个Promise对象
return new Promise(function(resolve,reject){ //这个Promise构造函数可以传入一个函数,这个函数有两个参数:resolve和reject
fs.readFile(path,'utf-8',function(err,data){ //异步操作
if(err) reject(err) //reject输出错误信息
resolve(data); //resolve输出错误信息
})
})
}
//.then是resolve和reject具体的回调函数,,then(resolve,reject)第二个回调可以省略
getFile(path.join(__dirname,'./file/1.txt')).then(function(data){
console.log(data) //读取1.txt
return getFile(path.join(__dirname,'./file/12.txt')) //第一个回调中return第2个Promise
}).then(function(data){ //这里的promise对象已经变成了第二个promise了
console.log(data); //读取2.txt
return getFile(path.join(__dirname,'./file/3.txt')) //第二个回调中return第3个Promise
}).then(function(data){ //这里的promise对象已经变成了第二个promise了
console.log(data); //读取3.txt
}).catch(function(err){ //捕获异常,一旦遇到异常就终止执行,转而处理异常
console.log(err.message);
})
console.log("主函数执行")
// 输出:
//主函数执行
//1.txt内容
//2.txt内容
//3.txt内容
getFile(path.join(__dirname,'./file/1.txt')).then---这是第一个Promise,根据传入的参数,作用是读取1.txt,接着,当读取1.txt成功之后,在resolve回调函数内部,我们return getFile(path.join(__dirname,'./file/2.txt')),那么此时的 getFile(path.join(__dirname,'./file/1.txt')).then相当于 getFile(path.join(__dirname,'./file/2.txt')),也就是执行第一个回调函数的结果就是第二个Promise的对象,如此一来,就能解决回调地狱了。而且最后还可以使用.catach来捕获异常,当异常一发生时,终止promise,转而处理异常。
如果我们希望异常出现时能够继续执行promise那么我们就可以为每一个promise指定一个reject处理异常
Promise执行过程:
-
先执行封装Promise的函数,当遇到异步操作时,将异步操作放入任务队列中,先执行主函数
-
主函数执行时遇到.then,定义了成功和失败的回调,继续执行主函数,等待异步操作完成
-
主函数执行完毕之后再去执行任务队列中的任务(此时异步操作完成了)
JQuery下Ajax的Promise实践
正常JQuery:
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input type="button" value="获取数据" id="btn">
<script src="./node_modules/jquery/dist/jquery.min.js"></script>
<script>
$(function () {
$('#btn').on('click', function () {
$.ajax({
url: './data.json', //请求地址
type: 'get', //请求类型
dataType: 'json', //请求文件类型
success:function(data){ //请求成功的回调
console.log(data);
}
})
})
});
</script>
</body>
</html>
打开Vscode,按照图下方法运行:
运行结果:
JQuery+Promise:
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input type="button" value="获取数据" id="btn">
<script src="./node_modules/jquery/dist/jquery.min.js"></script>
<script>
$(function () {
$('#btn').on('click', function () {
$.ajax({ //这个ajax就是一个异步操作,我们将它当作是一个promise对象
url: './data.json',
type: 'get',
dataType: 'json'
}).then(function(data){ //resolve成功回调
console.log(data)
})
})
});
</script>
</body>
</html>