聊聊promise系列(基礎)

laihuamin發表於2019-07-02

前言

本文將從淺到深的去剖析promise。由於內容較多,分為上下兩篇。

內容大綱

  • 非同步程式設計的解決方案(完成)
  • promise是什麼(完成)
  • promise的用法(完成)
  • promise的優點與缺點(完成)
  • promise的錯誤捕獲(待續)
  • promise的原理(待續)
  • promise的底層原始碼(待續)
  • 自己實現一個promise(待續)

非同步程式設計的解決方案

js是一門單執行緒的語言,所以其中會涉及到很多非同步的操作,非同步程式設計的解決方案有很多種,我們主要講一種最基礎的和這篇文章的主角(promise):

回撥函式

在非同步程式設計中,這種的應用範圍最廣,舉個定時器的例子:

setTimeout(() => {
    // ...
}, 1000);
複製程式碼

當然在業務中非同步請求也會用到很多,但當多個非同步操作需要序列操作的時候,就會有回撥地獄產生。

$.get(url, data1 => {
    console.log(data1)
    $.get(data1.url, data2 => {
        console.log(data2)
        $.get(data2.url, data3 => {
            console.log(data3)
        })
    })
})
複製程式碼

程式碼沒有美感,且不利於維護。當然,我們可以通過減少程式碼巢狀,模組化等手段來修復。但是並不如下面的解決方案優雅。

佼佼者——promise

const reqMethod = (url) => {
    return new Promise((reslove, reject) => {
        $.get(url, data => {
            if(data.success) {
                reslove();
            } else {
                reject();
            }
        })
    })
}

reqMethod(url).then((data1) => {
    return reqMethod(data1.url);
}).then((data2) => {
    return reqMethod(data2.url);
}).then((data3) => {
    return reqMethod(data3.url);
})
複製程式碼

這樣的實現方式,符合易於閱讀,因為每一步操作都是按照先後順序進行的。

promise是什麼

Promise從不同角度理解,有很多種含義。

promise的一種承諾

promise從字面意思理解,就是許諾和承諾的意思,對於一種承諾而言,有三種狀態:

1、承諾還未達成,還在糾結過程中(pending狀態)
2、承諾沒有實現,失言了(rejected狀態)
3、承諾實現了,就是成功的狀態(fulfilled狀態)
複製程式碼

Promise是一種標準的規範

在這裡不過多展開,大家可以去看Promise/A+ 規範或者ECMAscript規範;

Promise是ES6提供的一種物件

Promise是一個物件,是一個建構函式,ES6將其寫進了語言標準。統一了用法。最基礎的用法如下:

const promiseMethod = new Promise((resolve, reject) => {
    // some code
    if(/*非同步成功的條件*/) {
        resolve();
    } else {
        reject();
    }
})
複製程式碼

Promise的用法

new Promise

Promise是一個建構函式,最基礎的作用就是用new操作符生成一個例項物件

const promiseMethod = new Promise((resolve, reject) => {
    // some code
    if(/*非同步成功的條件*/) {
        resolve();
    } else {
        reject();
    }
})
複製程式碼

Promise可接受的引數是一個函式,resolvereject是該函式的兩個引數,由js引擎提供,不需要自己定義。

resolve的作用是將pending狀態變更為fulfilled狀態。

reject的作用是將pending狀態變更為rejected狀態。

Promise新建時就會立即執行,與何時呼叫無關,與結果也無關

舉個例子

const promiseOne = new Promise((resolve, reject) => {
    console.log('has finish');
    resolve();
})
複製程式碼

1、console.log在新建過程中就執行了。

2、promiseOne的結果已經固定下來了,無論何時呼叫,結果都不會發生改變。

Promise.prototype.then

then方法是被定義在Promise的原型上,作用是:新增Promise狀態改變後的回撥函式。

then方法接收兩個引數,第一個引數是resolve狀態執行的函式,第二個引數是reject執行的函式。

then方法返回的是一個新的Promise物件。

舉個例子:

const promiseOne = new Promise((resolve, reject) => {
    console.log('has finish');
    resolve();
})

const promiseTwo = new Promise((resolve, reject) => {
    console.log('has reject');
    reject();
})

const promiseThree = promiseOne.then(() => {
    console.log('成功的回撥')
}, () => {
    console.log('失敗的回撥')
})

promiseTwo.then(() => {
    console.log('成功的回撥')
}, () => {
    console.log('失敗的回撥')
})

console.log(promiseThree);

複製程式碼

輸出的結果:

has finish
has reject
<Promise>(pending)
成功的回撥
失敗的回撥
複製程式碼

Promise.prototype.catch

catch方法其實和then第二個回撥函式的別名,作用是用於儲物發生時的回撥處理。

catch捕獲兩類錯誤:

1、非同步操作時丟擲錯誤,導致狀態變為reject

2、then回撥過程中產生的錯誤。

舉個例子:

//第一種情況:非同步操作過程中報錯
const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});

// 第二種情況:then執行過程中報錯

const promise = new Promise((resolve, reject) => {
    resolve();
})

promise.then(() => {
    throw new Error('test');
}).catch(function(error) {
  console.log(error);
});
複製程式碼

關於catchthen第二個引數的區別:

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });
複製程式碼

第二種寫法的優點在於,then執行過程中的報錯,catch一樣能捕獲,優於第一種寫法。

更深入的錯誤捕獲我們單獨放一章講。

Promise.prototype.finally

finally方法作用在於不管promise返回的狀態是什麼,它都會在執行。

finally不接受任何引數。

promise
  .then(function(data) {
    // success
  })
  .catch(function(err) {
    // error
  }).finally(() => {
      
  });
複製程式碼

finally中執行的狀態與promise的結果無關,而且在方法中無法得知promise的執行結果。

Promise.all

用法:

Promise.all([p1, p2, p3]);
複製程式碼

作用:是將多個promise示例封裝成一個promise例項。結果只有以下兩種情形:

  • 全部成功

所有promise都成功,總的promise才會成功

const p1 = new Promise((resolve, reject) => {
    resolve('hello');
})
const p2 = new Promise((resolve, reject) => {
    resolve('hello');
})
Promise.all([p1, p2]).then(() => {
    console.log('全部成功')
}).catch(() => {
    console.log('全部失敗')
})

// 全部成功
複製程式碼
  • 全部失敗

只要有一個promise的狀態從pending變成reject就是失敗。

const p1 = new Promise((resolve, reject) => {
    resolve();
})
const p2 = new Promise((resolve, reject) => {
    reject();
})
Promise.all([p1, p2]).then(() => {
    console.log('全部成功')
}).catch(() => {
    console.log('全部失敗')
})
複製程式碼

Promise的時間如何計算?

const newDate = new Date();
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, 500)
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, 1000)
})
Promise.all([p1, p2]).then(() => {
    const now = new Date();
    let time = now.getTime() - newDate.getTime();
    console.log(time);
})
// 1001
複製程式碼

按最長的那個為準。

Promise.race

用法:

Promise.race([p1, p2, p3]);
複製程式碼

作用:是將多個promise示例封裝成一個promise例項。

Promise.race與Promise.all的區別

Promise.race的狀態取決於最先完成的promise的狀態。

舉個例子,我們需要對一個請求做5秒的timeout,就可以用Promise.race

const reqMethod = (url) => {
    return new Promise((reslove, reject) => {
        $.get(url, data => {
            reslove();
        })
    })
}

const timeout = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('timeout')
    }, 5000)
})
Promise.race([reqMethod(xxx),timeout]).then(() => {
    console.log('請求成功')
}).catch(() => {
    console.log('timeout')
})
複製程式碼

Promise.resolve

作用:需要將現有的物件轉化為promise物件。

Promise.resolve({});
//等價於
new Promise(resolve => resolve({}));
複製程式碼

Promise.resolve會根據傳入的不同引數做不同的處理

  • 傳入一個promise物件

不做任何操作,原封不動的返回該物件。

  • 引數是一個thenable物件

立即執行thenable物件中的then方法,然後返回一個resolved狀態的promise

let thenable = {
    then: function(resolve, reject) {
        resolve('success')
    }
}

const p = Promise.resolve(thenable);
p.then((e) => {
    console.log(e);
})
複製程式碼
  • 不是物件

直接返回一個resolved狀態的promise。並將引數帶給回撥函式。

const p = Promise.resolve('引數');

p.then((e) => {
    console.log(e)
})
複製程式碼
  • 不傳引數

直接返回一個resolved狀態的promise

Promise.reject

其作用是返回一個新的promise例項,狀態直接為reject

Promise.reject('error');
//等價於
new Promise((resolve, reject) => reject('error'));
複製程式碼

不同於Promise.resolve,其引數會原封不動的作為reject的理由。

舉個例子:

const p = Promise.reject('error');
p.catch((e) => {
    console.log(e)
})
// error
複製程式碼

Promise的優缺點

Promise的特點

  • 狀態不受外界影響,只滿足於結果
1、promise代表一個非同步操作,一共有三種狀態:pending、fulfilled和rejected。
2、promise的結果只服從於非同步操作的結果,成功進入fulfilled狀態,失敗進入rejected狀態。
複製程式碼
  • 狀態變更之後不會在改變
1、promise狀態變化只有兩種可能,一種從pending到fulfilled,或者是從pending到reject。
2、當狀態變更完時,狀態將不在發生改變。
複製程式碼

Promise的優點

  • 鏈式寫法可讀性比回撥函式的寫法更優雅。
  • 與回撥函式相比更加方便錯誤處理。
  • 與事件體系相比,一次性返回結果,更加適用於一次性返回的結果。

Promise的缺點

  • 不能取消執行過程。
  • 不能讀取非同步過程中的進度。
  • 生成之後物件結果不會改變。

結語

本文對promise的用法進行了詳解,之後會更新兩篇深入一點的文章:

  • promise錯誤捕捉
  • 聊聊promise系列(深入)

相關文章