1. 程式人生 > >一篇文章徹底搞懂es6 Promise

一篇文章徹底搞懂es6 Promise

app .org image status end .json apple objects 同步

前言

Promise,用於解決回調地獄帶來的問題,將異步操作以同步的操作編程表達出來,避免了層層嵌套的回調函數。

既然是用來解決回調地獄的問題,那首先來看下什麽是回調地獄

var sayhello = function(callback){
    setTimeout(function(){
        console.log("hello");
        return callback(null);
    },1000);
}
sayhello(function(err){
    console.log("xiaomi");
});
console.log("mobile phone");

輸出結果 
mobile phone
hello
Xiaomi

看上面這段代碼,假如我們需要對輸出內容的順序進行調整,例如依次打印xiaomi apple huawei ,那麽我們之前的做法是怎麽樣的

var sayhello = function(name, callback){
    setTimeout(function(){
        console.log("hello");
        console.log(name);
        return callback(null);
    },1000);
}
sayhello("xiaomi", function(err){
    sayhello("apple", function(err){
        sayhello("huawei", function(err){
            console.log("end");
        });
    });
});
console.log("mobile phone");

問題很明顯,代碼層層嵌套,看起來十分的混亂,如果層級代碼更多更是難以維護

因此Promise的出現使得我們可以用同步的方式來操作異步代碼,解決以上問題

初識promise

var getUserInfo = function() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            var user = {name: ‘Kerry Wu‘, age: 31};
            resolve(user);
        }, 3000);
    });
};
this.getUserInfo().then(function(userInfo) {
    console.log(‘userInfo‘,userInfo);//{ name: ‘Hanmeimei‘, age: 31 }
});

我們通過new關鍵詞實例化一個Promise對象並返回該對象,然後使用.then的形式獲取Promise返回的內容

這裏需要註意的是,new Promise 實例化是一個同步的過程,而.then是一個異步的過程,關於同步異步執行順序 ,先執行同步在執行異步代碼

Promise狀態

1、Pending 進行中 / Resolved 已成功 / Rejected 已失敗

resove 將未完成變成已完成 pending => resolved
reject 將未完成變成已失敗 pending => rejected

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

2、then 與 catch
then方法接收兩個函數參數,第一個表示resove 已成功的回調,第二個表示reject 已失敗的回調

用法如下:

var p = new Promise(function(resolve, reject){ ... })
p.then(function(){}, function(){})
p.then().catch();

then,前一個then的返回結果,可以再後一then的回調中獲取,如:

var p3 = ()=> new Promise((resolve, reject)=>{
     resolve(‘{"name":"jack", "age":28}‘)
});

p3()
   .then(res => JSON.parse(res))
       .then(data => Object.assign(data, {name:‘rose‘}))
   .then(data => console.log(data))
// 輸出:{name: "rose", age: 28}

異步加載圖片

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    var image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error(‘Could not load image at ‘ + url));
    };

    image.src = url;
  });
}

loadImagesAsync(‘//img.static.com/xxx.jpg‘).then(function(img){
    //加載成功 顯示圖片
}, function(err){
   //加載失敗 提示失敗
})

異步加載數據
使用promise包裝一個異步請,返回一個promise對象,使用then和catch的方式對返回結果進行處理

var getJSON = function(url){
  return new Promise((resolve, reject)=>{
             var client = new XMLHttpRequest();
          client.open(‘GET‘, url);
          client.onreadystatechange = callback;
               client.send();

          function callback(){
                  if(this.readyState !== 4) return;

                 if(this.status === 200){
                          resolve(this.response)
                  }else{
                          reject(new Error(this.statusText))
                     }
           }
   })
}

getJSON(‘/api/getList‘).then(function(data){
   //獲取請求的數據
}, function(err){
     //請求失敗錯誤處理
});

catch
p.catch()用於處理promise中rejected狀態的回調,與p.then(resolveFn, rejectFn)中 rejectFn的作用相同

var p = new Promise(function(resolve, reject){ ... });
p.then(function(){}, function(){});
//等同於
p.then(function(){}).catch(function(){});

reject(‘error’) 與 throw new Error(‘…’) 都能被catch捕獲

new Promise((resolve, reject) => {
        throw new Error(‘some error1‘);
}).catch(err =>  console.log(err.message))

// 等同於
new Promise((resolve, reject) => {
   reject(‘some error2‘)
}).catch(err => console.log(err))

捕獲異常
promise對象的錯誤,具有 冒泡 性質,會一直向後傳遞,直到被捕獲
推薦使用 catch 代替then(null, rejectFn)中的rejectFn,catch可以捕獲前面then函數返回的錯誤信息,也更接近同步的寫法

// bad
new Promise(function(resolve, reject){}).then(resolveFn, rejectFn)

// good
new Promise(function(resolve, reject){}).then(resoveFn).catch(rejectFn)

Promise all與race

Promise.all([]) 與 Promise.race([])

  • 接收一個數組做為參數,參數中的每個元素為promise實例,
  • 如果元素不是promise實例,則會調用Promise.resolve()轉換為promise的實例
  • 將多個promise對象包裝為一個新的promise對象

1、Promise.all()

Promise的all方法提供了並行執行異步操作的能力,並且在所有異步操作執行完後才執行回調

當p1、p2、p3的狀態全部為resolved時,才能將p的狀態改為resolved
當p1、p2、p3其中一個狀態變成rejected時,就會將p的狀態變成rejected

var p = Promise.all([Promise.resolve(‘1‘), Promise.resolve(‘2‘), Promise.resolve(‘3‘)]);
p.then(data => console.log(data)) //["1", "2", "3"]

var p1 = Promise.all([Promise.resolve(‘1‘), Promise.reject(‘2‘), Promise.resolve(‘3‘)]);
p1.then(data => console.log(data)).catch(err => console.log(err)) // 2

Promise.all用的最多一般是我們在請求網絡數據時,比如需要同時請求多個接口,我們可以合並多個請求一次處理

function getURL(URL) {
            return new Promise(function (resolve, reject) {
                var req = new XMLHttpRequest();
                req.open(‘GET‘, URL, true);
                req.onload = function () {
                    if (req.status === 200) {
                        resolve(req.responseText);
                    } else {
                        reject(new Error(req.statusText));
                    }
                };
                req.onerror = function () {
                    reject(new Error(req.statusText));
                };
                req.send();
            });
        }
        function getComment() {
            return getURL(‘http://azu.github.io/promises-book/json/comment.json‘).then(res=>JSON.parse(res));
        }
        function getPeople() {
            return getURL(‘http://azu.github.io/promises-book/json/people.json‘).then(res=>JSON.parse(res));
        }
        // 合並請求
        Promise.all([getComment(), getPeople()]).then(function (value) {
            console.log(value);
        }).catch(function(error){
            console.log(error);
        });

Promise resolve和reject

Promise.resolve() 與 Promise.reject()

Promise.resolve(‘foo‘)
// 等價於
new Promise(resolve => resolve(‘foo‘))

最後

記得以前在面試的時候,被問了一道很有意思的面試題,主要是考察promise和settimeout執行順序

setTimeout(function () {
    console.log(1)
}, 0);
new Promise(function executor(resolve) {
        resolve();
}).then(function () {
    console.log(2);
});

如上代碼,為什麽運行結果是2,1而不是1,2?
不是setTimeout先加入任務隊列嗎?

解答:
1、從規範上來講,setTimeout有一個4ms的最短時間,也就是說不管你設定多少,反正最少都要間隔4ms才運行裏面的回調(當然,瀏覽器有沒有遵守這個規範是另外一回事兒)。而Promise的異步沒有這個問題。
2、從具體實現上來說,這倆的異步隊列不一樣,Promise所在的那個異步隊列優先級要高一些。

還有一道差不多的

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()

為什麽輸出結果是 1,2,3,5,4 而非 1,2,3,4,5 ?

解答:
1、Promise.then 是異步執行的,而創建Promise實例( executor )是同步執行的。
2、setTimeout 的異步和 Promise.then 的異步不太一樣不在同一個隊列中,setTimeout(fn, 0)在下一輪“事件循環”開始時執行,Promise.then()在本輪“事件循環”結束時執行。因此then 函數先輸出,settimeout後輸出。

這裏涉及到js事件循環、任務隊列的東西,了解更多 https://www.cnblogs.com/hity-tt/p/6733062.html

參考閱讀

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/0014345008539155e93fc16046d4bb7854943814c4f9dc2000
http://coderlt.coding.me/2016/07/17/ES6-promise/

一篇文章徹底搞懂es6 Promise