天天看點

ES6文法之promise

    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):

ES6文法之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的步驟

  1. 要想正确的使用Promise,那麼就要建立一個函數封裝Promise對象,并return出去

  2. 在Promise的構造函數中,寫入異步操作的語句,使用resolve()執行成功操作,reject()執行失敗操作

  3. 封裝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執行過程:

  1. 先執行封裝Promise的函數,當遇到異步操作時,将異步操作放入任務隊列中,先執行主函數

  2. 主函數執行時遇到.then,定義了成功和失敗的回調,繼續執行主函數,等待異步操作完成

  3. 主函數執行完畢之後再去執行任務隊列中的任務(此時異步操作完成了)

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,按照圖下方法運作:

ES6文法之promise

運作結果:

ES6文法之promise

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>