Promise詳解

Promise
介紹:

  1. 用於異步計算
  2. 將異步操作隊列化,按照期望的順序執行,返回符合預期的結果
  3. 可以在對象之間傳遞和操作promise,幫助我們處理隊列

由於promise是控制異步操作的,所以先來介紹一下在promise之前異步操作的常見語法。

  1. 事件偵聽與響應
  2. 回調函數(例如ajax請求回調)

異步回調的問題:

  1. 回調地獄問題(一個回調嵌入一個回調,特別是一些數據庫操作和文件操作 , 難以維護)
  2. 無法正常使用 return 和 throw
  3. 異步回調的回調函數都是在一個新棧中,所以在現在的棧無法獲取到先前棧的信息。之前的棧也捕獲不到當前棧拋出的錯誤,所以在異步回調中無法正常使用try catch正常處理錯誤。
  4. 在異步回調中經常需要在外層回調中去定義一些變量給內層回調使用。
    talk is cheap , show me the code
const path = require('path');
const fs = require('fs');
//尋找最大文件的函數
function findLargest(dir,callback){
    fs.readdir(dir,function(err,files){
        if(err) return callback(err); //[錯誤使用回調來處理]
            let count = files.length; //獲取文件長度
            let errored = false; //是否錯誤
            let stats = [];
            //遍歷文件夾下的所有文件
            files.forEach(file => {
                fs.stat(path.join(dir,file),(err,stat) =>{
                    if(errored) return;
                    if(err){
                        errored = true;
                        return callback(err);
                    }
                    stats.push(stat);
                    if(--count === 0){
                        let largest = stats
                        .filter(function(stat){
                            console.log('-----');
                            console.log(stat.isFile());
                            return stat.isFile();
                        }) //先判斷是否是文件
                        .reduce(function(prev,next){
                            //判斷大小
                            if(prev.size > next.size) { return prev; }
                            return next;
                        });
                    callback(null,files[stats.indexOf(largest)])
                }
            })
        })
    })
}

findLargest('../blog/blogDemo/移動端滾動詳解demo',function(err,filename){
    if(err) return console.error(err);
    console.log('largest file was:',filename);
})

上面就是一個查找最大文件的例子,其中有許多回調帶來的問題。接下來我們先回歸主題,學習一些promise的使用,然後使用promise來改寫這個例子。

promise詳解

new Promise(
    /*實例化Promise時傳入一個執行器,也就是一個函數*/
    function(resolve,reject){
        //異步操作放在這裏

        resolve(); //處理成功,修改實例化的promise對象的狀態爲fulfilled
        reject(); //處理失敗,修改實例化的promise對象的狀態爲rejected
    }
)
.then(function A(){
    //成功之後的處理,即調用resolve()就執行A中的內容
},function B(){
    //失敗之後的處理,即調用reject()或者拋出了錯誤,就執行B中的內容
})

promise有三個狀態:

pending 【待定】初始狀態
fulfilled 【實現】操作成功
rejected 【否決】操作失敗

promise的狀態一發生改變,立馬調用.then()中的響應函數處理後續步驟,如果then()中返回了一個新的promise實例,則繼續循環下去。

promise常用的場景:

console.log('start');
new Promise(function(resolve,reject){
    setTimeout(function(){    //定時器模擬異步
        resolve('hello');    //修改promise狀態調用then中的第一個函數
    },2000);
}).then((value)=>{
    console.log(value);    //接收resolve傳來的值
    return new Promise(function(resolve){   //then()返回一個新的promise實例,後面可以繼續接then
        setTimeout(function(){
            resolve('world');       //修改新promise的狀態,去調用then
        },3000)
    })  
}).then((value)=>{
   console.log(value);
})

//輸出結果:
/*
    立即輸出   start
    兩秒輸出   hello
    再三秒     world
*/

上面我們在 then() 函數中返回的是一個新的promise,如果返回的不是一個新的promise會怎樣呢?依然是上面的代碼,稍作修改。

console.log('start');
new Promise(function(resolve,reject){
    setTimeout(function(){  
        resolve('hello');    
    },2000);
}).then((value)=>{
    console.log(value);  
    (function(){
        return new Promise(function(resolve){   
            setTimeout(function(){
                resolve('world');       
            },3000)
        })  
    })();  
    return false; 
}).then((value)=>{
   console.log(value);
})
/*
    結果:
       立即輸出   start
       兩秒輸出   hello
       三秒輸出   flase
*/

根據上面的運行結果來看,如果在一個then()中沒有返回一個新的promise,則return 什麼下一個then就接受什麼,在上面的實例代碼中return的是false,下一個then中接受到的value就是false,如果then中沒有return,則默認return的是undefined.

注意:then中return Promise必須是在then函數的作用域中return,不能在其他函數作用域中return,無效。上面的例子中return Promise就是在一個立即執行函數中返回的,所以無效。

.then()中包含.then()的嵌套情況
then()的嵌套會先將內部的then()執行完畢再繼續執行外部的then();在多個then嵌套時建議將其展開,將then()放在同一級,這樣代碼更清晰。

console.log('start');
new Promise((resolve,reject)=>{
    setTimeout(function(){
        console.log('step');
        resolve(110);
    },1000)
})
.then((value)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            console.log('step1');
            resolve(value);
        },1000)
    })
    .then((value)=>{
        console.log('step 1-1');
        return value;
    })
    .then((value)=>{
        console.log('step 1-2');
        return value;
    })
})
.then((value)=>{
    console.log(value);
    console.log('step 2');
})
/*
 start
 step
 step1
 step 1-1
 step 1-2
 110
 step 2
*/

//展開之後的代碼
console.log('start');
new Promise((resolve,reject)=>{
    setTimeout(function(){
        console.log('step');
        resolve(110);
    },1000)
})
.then((value)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            console.log('step1');
            resolve(value);
        },1000)
    })
})
.then((value)=>{
        console.log('step 1-1');
        return value;
    })
.then((value)=>{
    console.log('step 1-2');
    return value;
})
.then((value)=>{
    console.log(value);
    console.log('step 2');
})

錯誤處理
promise處理錯誤有兩種方式,一種是發現錯誤執行then中的第二個回調函數來處理錯誤,一種是.catch()來處理錯誤

注意:拋出ERROR時,只有在執行函數的頂層拋出後面的catch纔會接受到。這裏如果在setTimeout中拋出錯誤,catch和then中的錯誤處理函數是接受不到的

//1.根據錯誤執行then中第二個回調來處理錯誤
new Promise((resolve,reject)=>{
    setTimeout(function(){
        //只要出現了錯誤或者調用了reject,就可以在then的第二個函數中獲取到
        reject('err');
    },1000)
}).then((value)=>{
    console.log(value);
},
//出錯之後的執行函數
(err)=>{
    console.log('出錯了');
    console.log(err);
})
/*
    出錯了
    err
*/

//2.根據catch來獲取錯誤,拋出err或者執行reject()都會在catch中獲取到錯誤信息
new Promise((resolve,reject)=>{
    //只要出現了錯誤,就可以在then的第二個函數中獲取到
    setTimeout(function(){
        reject('一個錯誤');
    },1000)
}).then((value)=>{
    console.log(value);
}).catch(err=>{
    console.log('錯誤信息:'+err);
})
/*
    錯誤信息:一個錯誤
*/

更推薦使用catch的方式進行處理錯誤,因爲catch能獲取到之前所有then中出現的錯誤

catch和then的連用
如果每一步都有可能出現錯誤,那麼就可能出現catch後面接上then的情況。上代碼

new Promise((resolve,reject)=>{
    resolve();
})
.then(value=>{
    console.log('done 1');
    throw new Error('done 1 error');
})
.catch(err=>{
    console.log('錯誤信息1:'+err);
})
.then(value=>{
    console.log('done 2');
})
.catch(err=>{
    console.log('錯誤信息2:'+err);
})
/*
 done 1
 錯誤信息1:Error: done 1 error
 done 2

 說明catch後面會繼續執行thencatch返回的也是一個promise實例
*/
new Promise((resolve,reject)=>{
    resolve();
})
.then(value=>{
    console.log('done 1');
    throw new Error('done 1 error');
})
.catch(err=>{
    console.log('錯誤信息1:'+err);
    throw new Error('catch error');
})
.then(value=>{
    console.log('done 2');
})
.catch(err=>{
    console.log('錯誤信息2:'+err);
})
/*
 done 1
 錯誤信息1:Error: done 1 error
 錯誤信息2:Error: catch error

 如果在catch中也拋出了錯誤,則後面的then的第一個函數不會執行,因爲返回的promise狀態已經爲rejected了
*/

總的來說,catch之後可以接then,catch也是返回的一個promise對象。如果catch中出現錯誤,則promise狀態修改成reject,否則爲fullfilled狀態

Promise.all()
將多個Promise批量執行,所有的Promise都完畢之後返回一個新的Promise。

  1. 接收一個數組作爲參數
  2. 數組中可以是Promise實例,也可以是別的值,只有Promise會等待狀態的改變
  3. 所有子Promise完成,則該Promise完成,並且返回值是參數數組中所有Promise實例的結果組成的數組
  4. 有任何一個Promise失敗,則該Promise失敗,返回值是第一個失敗的Promise的結果
console.log('here we go');
Promise.all([1,2,3])
    .then(all=>{
        console.log('1: ' + all); 
        return Promise.all([function(){
            console.log('ooxx');
        },'xxoo',false])
    })
    .then(all=>{
        console.log('2: ' + all);
        let p1 = new Promise(resolve=>{
            setTimeout(function(){
                resolve('I\'m p1');
            },1500)
        });
        let p2 = new Promise(resolve=>{
            setTimeout(function(){
                resolve('I\'m p2');
            },2000)
        });
        return Promise.all([p1,p2]);
    })
    .then(all=>{
        console.log('3: '+all);
        let p1 = new Promise((resolve,reject)=>{
            setTimeout(function(){
                resolve('P1');
            },1000)
        })
        let p2 = new Promise((resolve,reject)=>{
            setTimeout(function(){
                reject('P2');
            },3000)
        })
        let p3 = new Promise((resolve,reject)=>{
            setTimeout(function(){
                reject('P3');
            },2000)
        })
        return Promise.all([p1,p2,p3]);
    })
    .then(all=>{
        console.log('all: ' + all);
    })
    .catch(err=>{
        console.log('Catch:' + err);
    })
    /*
         here we go
         1: 1,2,3
         2: function(){
            console.log('ooxx');
            },xxoo,false
         3: I'm p1,I'm p2    
         Catch:P3      

         證明了上面的四點。
    */

Promise.race()
和Promise.all()差不多,區別就是傳入的數組中有一個Promise完成了則整個Promise完成了。

let p1 = new Promise(resolve=>{
    setTimeout(function(){
        resolve('p1');
    },10000);
})
let p2 = new Promise(resolve=>{
    setTimeout(function(){
        resolve('p2');
    },1000);
})
Promise.race([p1,p2])
.then((value)=>{
    console.log(value);
})
/*
    p1     1s之後輸出
    。。    等待十秒後代碼纔算執行完畢
*/

常見用法:

將定時器和異步操作放在一起,如果定時器先觸發,則認爲超時,告知用戶。

let p1 = new Promise(resolve=>{
    $.ajax({
        success:function(result){
            resolve(result);
        }
    }); //異步操作
})
let p2 = new Promise(resolve=>{
    setTimeout(function(){
        resolve('timeout');
    },10000);
})
Promise.race([p1,p2])
.then(value=>{
    if(value === 'timeout'){
        alert('請求超時');
    }
})

將回調包裝爲Promise
好處:1.可讀性好 2. 返回的結果可以放在任意Promise隊列

// 將nodejs中fs模塊的readDir和readFile方法包裝爲Promise
//FileSystem.js
const fs = require('fs');
module.exports = {
    readDir: function(path,options){
        return new Promise(resolve=>{
            fs.readdir(path,options,(err,files)=>{
               if(err){
                   throw err;
               }
               resolve(files);
            })
        })
    },
    readFile: function(path,options){
        return new Promise(resolve=>{
            fs.readFile(path,options,(err,content)=>{
                if(err) throw err;
                resolve(content);
            })
        })
    }
}

//test.js
const fs = require('./FileSystem');
fs.readFile('./test.txt','utf-8')
    .then(content=>{
       console.log(content);
    })

尋找最大文件Promise改版

const fs = require('fs');
const path = require('path');
const FileSystem = require('./FileSystem'); //用上面封裝的FileSystem

function findLargest(dir) {

    return FileSystem
        .readDir(dir, 'utf-8')
        .then(files => {
            return Promise.all(files.map(file=>{
                return new Promise(resolve =>{
                    fs.stat(path.join(dir,file),(err,stat)=>{
                        if err throw err;
                        if(stat.isDirectory()){
                            return resolve({
                                size: 0
                            });
                        }
                        stat.file = file;
                        resolve(stat);
                    });
                });
            }));
        })
        .then( stats =>{
            let biggest = stats.reduce((memo,stat)=>{
                if(memo.size < stat.size){
                    return stat;
                }
                return memo;
            });
            return biggest.file;
        })
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章