JS非同步程式設計之Promise

南波發表於2019-02-24

前言

《JS非同步程式設計之 callback》一文我們瞭解了“JS 是基於單執行緒事件迴圈”的概念構建的,回撥函式不會立即執行,由事件輪詢去檢測事件是否執行完畢,當執行完有結果後,將結果放入回撥函式的引數中,然後將回撥函式新增到事件佇列中等待被執行。

同時也講了回撥函式的問題:

一是“回撥地獄”,因為非同步回撥函式的特點:回撥函式是作為非同步函式的引數,一層一層巢狀,當巢狀過多,將使程式碼邏輯變得混亂,也無法做好錯誤捕捉和處理(只能在回撥函式內部 try catch)。

二是回撥的執行方式不符合自然語言的線性思維方式,不容易被理解。

三是控制反轉(控制權在其他人的程式碼上),假如非同步函式是別人提供的庫,我們把回撥函式傳進去,我們並不能知道非同步函式在呼叫回撥函式之外做了什麼事情。

func1(() => {
    func2(() => {
        func3(() => {
            func4(() => {
                try {
                    ...
                } catch (err){
                    ...
                }
            })
        });
    });
});
複製程式碼

一、Promise 原理

首先,Promise 中文翻譯為“承諾”, 是 JavaScript 的一種物件,表示承諾終將返回一個結果,無論成功還是失敗。

Promise 有三個狀態:等待中(pending),完成(fullfilled),失敗(rejected), Promise 的設計具有原子性,狀態一旦從 pending 狀態轉換為 fullfilled 狀態或者 rejected 狀態後,將不能被改變。

JS非同步程式設計之Promise

var promise1 = new Promise((resolve, reject) => {
    console.log("Promise 構造器會立即執行");
    setTimeout(function (){
        if(true) {
            resolve("完成");
        } else {
            reject("失敗");
        }
    }, 1000);
})
promise1
.then((result) => {
    // do something
    console.log(result);
    return 1
    
    // return Promise.resolve(1);  // 返回一個決議為成功的 promise 例項
    // return Promise.reject("error");  // 返回一個決議為拒絕的 Promise 例項
})
.then((result) => {
    // .then() 方法會返回一個 promise, 完成呼叫的引數為前一個 promise 的返回值或者決議值。
    // do other things
    console.log(result);
    throw new Error("錯誤")  // 丟擲錯誤是隱式拒絕
})
.catch((error) => {
    // 捕捉錯誤
    console.log(error)
})
.then(() => {
    // 還能繼續執行!
})
.finally(() => {
    // always do somethings
    console.log("finally!")
})
複製程式碼

二、Promise 的優勢

  1. 鏈式呼叫
    Promise 使用 then 方法後還會返回一個新的 Promise 物件,便於我們傳遞狀態資料,同時鏈式寫法接近於同步寫法,更符合線性思維。

  2. 錯誤捕捉
    相比回撥函式的錯誤無法在外部捕捉的問題,Promise 能夠為一連串的非同步呼叫提供錯誤處理。

  3. 控制反轉再反轉
    由於第三方提供的非同步函式,無法保證回撥函式如何被執行,但是 Promise 的特點,能夠保證非同步函式只能被 resolve 一次,以及始終以非同步的形式執行程式碼。

  4. 可以利用 Promise.all 和 Promise.race 來解決 Promise 始終未決議和並行 Promise 巢狀的問題

三、Promise 的不足

  1. 每個 .then() 都是一個獨立的作用域
    加入有很多個 .then() 方法,就會建立很多個獨立的作用域,那麼將只能通過外面包裹一層函式作用域的閉包來共享狀態資料

  2. 無法取消單個 .then()
    當 Promise 鏈中任意一個 .then() 方法中有語句執行錯誤後,儘管經過 catch 方法的錯誤處理,還是並不會中斷整個 Promise 鏈的執行。

  3. 無法得知進度
    由於 Promise 只能從 pending 到 fullfilled 或 rejected 狀態,無法得知 pending 階段的進度。

四、Promise 應用

// Promise 封裝 ajax
function fetch(method, url, data){
    return new Promise((resolve, reject) => {
        var xhr = new XMLHttpRequest();
        var method = method || "GET";
        var data = data || null;
        xhr.open(method, url, true);
        xhr.onreadystatechange = function() {
            if(xhr.status === 200 && xhr.readyState === 4){
                resolve(xhr.responseText);
            } else {
                reject(xhr.responseText);
            }
        }
        xhr.send(data);
        })
}

// 使用
fetch("GET", "/some/url.json", null)
.then(result => {
    console.log(result);
})

// 封裝 nodejs error first 風格回撥
function readFile(url) {
    return new Promise((resolve, reject) => {
       fs.readFile(url,'utf8', (err, data) => {
        if(err) {
            reject(err);
            return;
        }
        resolve(data)
        }) 
    })
}
複製程式碼

五、總結

Promise 是 ES6 提出的簡化非同步流程控制新規範,強調非同步任務的完成狀態且具有原子性,這使得我們的程式碼更容易追蹤和維護。Promise 在事件輪詢中屬於非同步事件佇列中的微任務,而微任務總是一次性全部執行,而巨集任務是每輪輪詢執行一個,此節內容參考我之前的文章《JS專題之事件迴圈》。

2019/02/24 @Manncoffee

相關文章