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>