promise介紹--基礎篇

前言

Promise,相信每個前端工程師都或多或少地在項目中都是用過,畢竟它早已不是一個新名詞。ES6中已經原生對它加以支持,在caniuse中搜索一下Promise,發現新版的chrome和firefox也已經支持。可是低版本的瀏覽器咱們可使用es6-promise這個polyfill庫來加以兼容。前端

暫且不談awaitasync,在Google或百度或360搜索等搜索引擎、或者在segmentfault等社區中,咱們能夠搜到一大把介紹promise的文章,畢竟它已經出現了很長時間,早已有不少大神分析講解過。es6

我也看了一些文章,可是感受都沒有達到想要的效果。因此決定本身開一個小系列文章學習講解一下promise的原理,以及實現,最後再談一談與之聯繫密切的Deferred對象。ajax

本文是該系列的第一篇文章,主要先讓你們對Promise有一個基本的認識。chrome

promise簡介

Promise的出現,本來是爲了解決回調地獄的問題。全部人在講解Promise時,都會以一個ajax請求爲例,此處咱們也用一個簡單的ajax的例子來帶你們看一下Promise是如何使用的。segmentfault

ajax請求的傳統寫法:數組

getData(method, url, successFun, failFun){
  var xmlHttp = new XMLHttpRequest();
  xmlHttp.open(method, url);
  xmlHttp.send();
  xmlHttp.onload = function () {
    if (this.status == 200 ) {
      successFun(this.response);
    } else {
      failFun(this.statusText);
    }
  };
  xmlHttp.onerror = function () {
    failFun(this.statusText);
  };
}

改成promise後的寫法:promise

getData(method, url){
  var promise = new Promise(function(resolve, reject){
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open(method, url);
    xmlHttp.send();
    xmlHttp.onload = function () {
      if (this.status == 200 ) {
        resolve(this.response);
      } else {
        reject(this.statusText);
      }
    };
    xmlHttp.onerror = function () {
      reject(this.statusText);
    };
  })
  return promise;
}

getData('get','www.xxx.com').then(successFun, failFun)

很顯然,咱們把異步中使用回調函數的場景改成了.then().catch()等函數鏈式調用的方式。基於promise咱們能夠把複雜的異步回調處理方式進行模塊化。瀏覽器

下面,咱們就來介紹一下Promise究竟是個什麼東西?它是如何作到的?前端工程師

Promise的原理分析

其實promise原理提及來並不難,它內部有三個狀態,分別是pendingfulfilledrejected 異步

pending是對象建立後的初始狀態,當對象fulfill(成功)時變爲fulfilled,當對象reject(失敗)時變爲rejected。且只能從pengding變爲fulfilledrejected ,而不能逆向或從fulfilled變爲rejected 、從rejected變爲fulfilled。如圖所示:

狀態變化

Promise實例方法介紹

Promise對象擁有兩個實例方法then()catch()

從前面的例子中能夠看到,成功和失敗的回調函數咱們是經過then()添加,在promise狀態改變時分別調用。promise構造函數中一般都是異步的,因此then方法每每都先於resolvereject方法執行。因此promise內部須要有一個存儲fulfill時調用函數的數組和一個存儲reject時調用函數的數組。

從上面的例子中咱們還能夠看到then方法能夠接收兩個參數,且一般都是函數(非函數時如何處理下一篇文章中會詳細介紹)。第一個參數會添加到fulfill時調用的數組中,第二個參數添加到reject時調用的數組中。當promise狀態fulfill時,會把resolve(value)中的value值傳給調用的函數中,同理,當promise狀態reject時,會把reject(reason)中的reason值傳給調用的函數。例:

var p = new Promise(function(resolve, reject){
    resolve(5)
}).then(function(value){
    console.log(value) //5
})

var p1 = new Promise(function(resolve, reject){
    reject(new Error('錯誤'))
}).then(function(value){
    console.log(value)
}, function(reason){
    console.log(reason) //Error: 錯誤(…)
})

then方法會返回一個新的promise,下面的例子中p == p1將返回false,說明p1是一個全新的對象。

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(function(value){
    console.log(value)
})
p == p1 // false

這也是爲何then是能夠鏈式調用的,它是在新的對象上添加成功或失敗的回調,這與jQuery中的鏈式調用不一樣。

那麼新對象的狀態是基於什麼改變的呢?是否是說若是p的狀態fulfill,後面的then建立的新對象都會成功;或者說若是p的狀態reject,後面的then建立的新對象都會失敗?

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(function(value){
    console.log(value)   // 5
}).then(function(value){
    console.log('fulfill ' + value)   // fulfill undefined
}, function(reason){
    console.log('reject ' + reason)   
})

上面的例子會打印出5和"fulfill undefined"說明它的狀態變爲成功。那若是咱們在p1then方法中拋出異常呢?

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(function(value){
    console.log(value)   // 5
    throw new Error('test')
}).then(function(value){
    console.log('fulfill ' + value)
}, function(reason){
    console.log('reject ' + reason)   // reject Error: test
})

理所固然,新對象確定會失敗。

反過來若是p失敗了,會是什麼樣的呢?

var p = new Promise(function(resolve, reject){
    reject(5)
})
var p1 = p.then(undefined, function(value){
    console.log(value)   // 5
}).then(function(value){
    console.log('fulfill ' + value)   // fulfill undefined
}, function(reason){
    console.log('reject ' + reason)
})

說明新對象狀態不會受到前一個對象狀態的影響。

再來看以下代碼:

var p = new Promise(function(resolve, reject){
    reject(5)
})
var p1 = p.then(function(value){
    console.log(value) 
})
var p2 = p1.then(function(value){
    console.log('fulfill ' + value)
}, function(reason){
    console.log('reject ' + reason)   // reject 5
})

咱們發現p1的狀態變爲rejected,從而觸發了then方法第二個參數的函數。這彷佛與咱們以前提到的有差別啊,p1的狀態受到了p的狀態的影響。

再來看一個例子:

var p = new Promise(function(resolve, reject){
    resolve(5)
})
var p1 = p.then(undefined, function(value){
    console.log(value) 
})
var p2 = p1.then(function(value){
    console.log('fulfill ' + value)   // fulfill 5
}, function(reason){
    console.log('reject ' + reason)   
})

細心的人可能會發現,該例子中then第一個參數是undefined,且value值5被傳到了p1成功時的回調函數中。上面那個例子中then的第二個參數是undefined,一樣reason值也傳到了p1失敗時的回調函數中。這是因當對應的參數不爲函數時,會將前一promise的狀態和值傳遞下去。

promise含有一個實例方法catch,從名字上咱們就看得出來,它和異常有千絲萬縷的關係。其實catch(onReject)方法等價於then(undefined, onReject),也就是說以下兩種狀況是等效的。

new Promise(function(resolve, reject){
    reject(new Error('error'))
}).then(undefined, function(reason){
    console.log(reason) // Error: error(…)
})

new Promise(function(resolve, reject){
    reject(new Error('error'))
}).catch(function(reason){
    console.log(reason) // Error: error(…)
})

咱們提到參數不爲函數時會把值和狀態傳遞下去。因此咱們能夠在多個then以後添加一個catch方法,這樣前面只要reject或拋出異常,都會被最後的catch方法處理。

new Promise(function(resolve, reject){
    resolve(5)
}).then(function(value){
    taskA()
}).then(function(value){
    taskB()
}).then(function(value){
    taskC()
}).catch(function(reason){
    console.log(reason)
})

Promise的靜態方法

Promise還有四個靜態方法,分別是resolverejectallrace,下面咱們一一介紹。

除了經過new Promise()的方式,咱們還有兩種建立Promise對象的方法:

Promise.resolve() 它至關於建立了一個當即resolve的對象。以下兩段代碼做用相同:

Promise.resolve(5)

new Promise(function(resolve){
    resolve(5)
})

它使得promise對象直接resolve,並把5傳到後面then添加的成功函數中。

Promise.resolve(5).then(function(value){
    console.log(value) // 5
})

Promise.reject() 很明顯它至關於建立了一個當即reject的對象。以下兩段代碼做用相同:

Promise.reject(new Error('error'))

new Promise(function(resolve, reject){
    reject(new Error('error'))
})

它使得promise對象直接reject,並把error傳到後面catch添加的函數中。

Promise.reject(new Error('error')).catch(function(reason){
    console.log(reason) // Error: error(…)
})

Promise.all() 它接收一個promise對象組成的數組做爲參數,並返回一個新的promise對象。

當數組中全部的對象都resolve時,新對象狀態變爲fulfilled,全部對象的resolvevalue依次添加組成一個新的數組,並以新的數組做爲新對象resolvevalue,例:

Promise.all([Promise.resolve(5), 
  Promise.resolve(6), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value)  // fulfill [5, 6, 7]
}, function(reason){
    console.log('reject',reason)
})

當數組中有一個對象reject時,新對象狀態變爲rejected,並以當前對象rejectreason做爲新對象rejectreason

Promise.all([Promise.resolve(5), 
  Promise.reject(new Error('error')), 
  Promise.resolve(7),
  Promise.reject(new Error('other error'))
  ]).then(function(value){
    console.log('fulfill', value)
}, function(reason){
    console.log('reject', reason)  // reject Error: error(…)
})

那當數組中,傳入了非promise對象會如何呢?

Promise.all([Promise.resolve(5), 
  6,
  true,
  'test',
  undefined,
  null,
  {a:1},
  function(){},
  Promise.resolve(7)
  ]).then(function(value){
    console.log('fulfill', value)  // fulfill [5, 6, true, "test", undefined, null, Object, function, 7]
}, function(reason){
    console.log('reject', reason)
})

咱們發現,當傳入的值爲數字、boolean、字符串、undefined、null、{a:1}、function(){}等非promise對象時,會依次把它們添加到新對象resolve時傳遞的數組中。

那數組中的多個對象是同時調用,仍是一個接一個的依次調用呢?咱們再看個例子

function timeout(time) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(time);
        }, time);
    });
}
console.time('promise')
Promise.all([
    timeout(10),
    timeout(60),
    timeout(100)
]).then(function (values) {
    console.log(values); [10, 60, 100]
    console.timeEnd('promise');   // 107ms 
});

由此咱們能夠看出,傳入的多個對象幾乎是同時執行的,由於總的時間略大於用時最長的一個對象resolve的時間。

Promise.race() 它一樣接收一個promise對象組成的數組做爲參數,並返回一個新的promise對象。

Promise.all()不一樣,它是在數組中有一個對象(最先改變狀態)resolvereject時,就改變自身的狀態,並執行響應的回調。

Promise.race([Promise.resolve(5), 
  Promise.reject(new Error('error')), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value)  // fulfill 5
}, function(reason){
    console.log('reject',reason)
})

Promise.race([Promise.reject(new Error('error')), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value) 
}, function(reason){
    console.log('reject',reason) //reject Error: error(…)
})

且當數組中有非異步Promise對象或有數字、boolean、字符串、undefined、null、{a:1}、function(){}等非Promise對象時,都會直接以該值resolve

Promise.race([new Promise((resolve)=>{
    setTimeout(()=>{
        resolve(1)
    },100)}),
  Promise.resolve(5), 
  "test",
  Promise.reject(new Error('error')), 
  Promise.resolve(7)]).then(function(value){
    console.log('fulfill', value)  // fulfill 5
}, function(reason){
    console.log('reject',reason)
})
// fulfill 5

數組中第一個元素是異步的Promise,第二個是非異步Promise,會當即改變狀態,因此新對象會當即改變狀態並把5傳遞給成功時的回調函數。

那麼問題又來了,既然數組中第一個元素成功或失敗就會改變新對象的狀態,那數組中後面的對象是否會執行呢?

function timeout(time) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            console.log(time)
            resolve(time);
        }, time);
    });
}
console.time('promise')
Promise.race([
    timeout(10),
    timeout(60),
    timeout(100)
]).then(function (values) {
    console.log(values); [10, 60, 100]
    console.timeEnd('promise');   // 107ms
});

// 結果依次爲
// 10
// 10
// promise: 11.1ms
// 60
// 100

說明即便新對象的狀態改變,數組中後面的promise對象還會執行完畢,其實Promise.all()中即便前面reject了,全部的對象也都會執行完畢。規範中,promise對象執行是不能夠中斷的。

補充

promise對象即便立馬改變狀態,它也是異步執行的。以下所示:

Promise.resolve(5).then(function(value){
  console.log('後打出來', value)
});
console.log('先打出來')

// 結果依次爲
// 先打出來
// 後打出來 5

但還有一個有意思的例子,以下:

setTimeout(function(){console.log(4)},0);
new Promise(function(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,命名4是先添加到異步隊列中的,爲何結果不是1 2 3 4 5呢?這個涉及到Event loop,後面我會單獨講一下。

相關文章
相關標籤/搜索