1. 程式人生 > >ES6 異步編程解決方案 之 Promise 對象

ES6 異步編程解決方案 之 Promise 對象

詳解 on() 基本 ack 地獄 down 場景 fill success

一、Promise 概述

  • Promise 對象是 ES6 提供的原生的內置對象

  • Promise 是異步編程的一種解決方案(異步代碼同步化),比傳統的解決方案——回調函數和事件——更合理和更強大

  • Promise 對象代表一個異步操作, 其不受外界影響,有三種狀態:
    • Pending(進行中、未完成的)
    • Resolved(已完成,又稱 Fulfilled)
    • Rejected(已失敗)

二、Promise 對象的 優缺點

1. 優點

  • 異步編程解決方案,避免了回調地獄(Callback Hell)問題
// 回調地獄

firstAsync(function(data){
    //處理得到的 data 數據
    //....
    secondAsync(function(data2){
        //處理得到的 data2 數據
        //....
        thirdAsync(function(data3){
              //處理得到的 data3 數據
              //....
        });
    });
});
// 使用 Promise 解決方案

firstAsync()
.then(function(data){
    //處理得到的 data 數據
    //....
    return secondAsync();
})
.then(function(data2){
    //處理得到的 data2 數據
    //....
    return thirdAsync();
})
.then(function(data3){
    //處理得到的 data3 數據
    //....
});

2. 缺點

  • Promise 對象,一旦新建它就會立即執行,無法中途取消
let Promise = new Promise(function(resolve, reject) {
  console.log(‘Promise‘);
  resolve();
});

Promise.then(function() {
  console.log(‘resolved.‘);
});

console.log(‘Hi!‘);

// Promise
// Hi!
// resolved
  • Promise 狀態一旦改變,就不會再變
const Promise = new Promise(function(resolve, reject) {
    resolve(‘ok‘);
    throw new Error(‘test‘);
});
Promise
    .then(function(value) { console.log(value) })
    .catch(function(error) { console.log(error) });

// 只輸出 ok
  • 當處於Pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)

三、Promise 的基本用法

  • ES6 規定,Promise對象是一個構造函數,用來生成Promise實例

  • 語法:

    • Promise 構造函數:接收一個函數參數,函數參數 中有兩個參數 resolvereject

    • 參數 resolvereject 都是函數(由 JavaScript 引擎提供,不用自己部署),在 異步操作成功時,手動執行 函數resolve在 異步操作失敗時,手動執行 函數reject

    • Promise實例生成以後,可以用 thencatch 方法分別指定 成功狀態、失敗狀態的回調函數

const promise = new Promise(function(resolve, reject) {
    // 異步操作 code

    // 手動執行 resolve / reject 函數
    if (/* 異步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }
});

promise
  .then((value) => {})
  .catch((error) => {});
  • 異步編程解決方案: 將異步任務同步操作(一般情況下 Promise 對象 集結合 then、catch 使用)

    • 異步任務:在 創建 Promise 對象中 執行( Promise 對象中的代碼 會立即執行

    • 需要在異步任務之後執行的代碼:放到 then、catch 中

四、Promise 原型上的 方法

1. then 方法

  • 作用: 為 Promise 實例添加 狀態改變(成功、失敗) 時的回調函數

  • 語法: Promise.then(resolve, reject)
    • 參數:
      • 參數 resolve:異步操作成功時的回調函數

      • 參數 reject:異步操作失敗時的回調函數(可省略)

    • 返回值: 一個新的Promise實例(不是原來那個Promise實例),可采用鏈式寫法
getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

// 如上:第一個回調函數完成以後,會將返回結果作為參數,傳入第二個回調函數
  • 返回值 有可能是 含有異步操作的 Promise 對象, 這時之後的 then 方法中的函數,就會等待該 Promise 對象的狀態發生變化,才會被調用
const getPromise = () => {
    const Promise = new Promise(function (resolve, reject) {});

    return Promise;
}

getPromise().then(
    param => getPromise(param)
).then(
    comments => console.log("resolved: ", comments),
    err => console.log("rejected: ", err)
);

2. catch 方法

  • 作用: 為 Promise 實例添加 狀態變為 reject 時 的回調函數

  • 語法: Promise.catch(reject)

  • 等同於: Promise.then(null, reject)

// 示例如下(Promise拋出一個錯誤,就被catch方法指定的回調函數捕獲)

const Promise = new Promise(function(resolve, reject) {
  throw new Error(‘test‘);
});

Promise.catch(function(error) {
  console.log(error);
});
// Error: test
  • Promise 對象的錯誤具有“冒泡”性質: 會一直向後傳遞,直到被捕獲到為止
const getPromise = () => {
    const Promise = new Promise(function (resolve, reject) {});

    return Promise;
}

getPromise().then(function(post) {
    return getJSON(post.commentURL);
}).then(function(comments) {
    // some code
}).catch(function(error) {
    // 處理前面三個Promise產生的錯誤:無論是 getJSON產生錯誤,還是 then 產生的錯誤,都會由 catch 捕捉到
});
  • 最佳實踐: 一般來說,不要在then方法裏面定義 reject 狀態的回調函數
// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) {
    // success
  })
  .catch(function(err) {
    // error
  });

3. finally 方法

  • 作用: 為 Promise 實例添加 回調函數(不管狀態是 resolvereject),該回調函數都執行

  • 語法: promise.finally(() => {}) 不接受任何參數

  • 舉例應用:服務器使用 Promise 處理請求,然後使用 finally 方法關掉服務器

promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});

4. thencatchfinally 回調的執行順序

  • 以下原則 適用於: new Promise(...)Promise.resolve(...)Promise.reject(...)

    • 回調函數如果是 同步函數: 會在本輪事件循環的末尾執行(下一輪循環之前)

    • 回調函數如果是 異步函數: 會在本輪事件循環的末尾 將 該異步函數放入到事件循環隊列中,等待時間為 0s (也就是在下一輪循環的最後執行)

  • 示例如下:

// 回調函數 是 同步函數

setTimeout(function () {
    console.log(3);
}, 0);

const promise = new Promise(function (resolve, reject) {
    console.log(6);
    resolve();
});

setTimeout(function () {
    console.log(4);
}, 0);

promise.then(() => {
    console.log(2);
});

setTimeout(function () {
    console.log(5);
}, 0);

console.log(1);

// 6 1 2 3 4 5
// 回調函數 是 異步函數

setTimeout(function () {
    console.log(3);
}, 0);

const promise = new Promise(function (resolve, reject) {
    console.log(6);
    resolve();
});

setTimeout(function () {
    console.log(4);
}, 0);

promise.then(() => {
    setTimeout(() => {
        console.log(2);
    }, 0);
});

setTimeout(function () {
    console.log(5);
}, 0);

console.log(1);

// 6 1 3 4 5 2

5. 回顧:JS 的異常捕獲

  • 作用: 對於可能會出現錯誤的代碼,使用異常捕獲方式,可以使 JS 運行到錯誤代碼處,不終止代碼執行 且 拋出異常錯誤

  • 三種最佳實踐:
    • try...catch

    • try...finally

    • try...catch...finally

  • 詳解:
    • try 語句: 包含 可能出現錯誤的代碼

    • catch 語句: 包含 捕捉到錯誤,執行的代碼

    • finally 語句: 包含 無論有沒有錯誤,都要執行的代碼

  • 示例:

try {
    console.log(11);    // 可能會發生錯誤的代碼
}
catch (e) {
   throw e;     // 拋出錯誤異常,且 catch 代碼塊中 throw 之後的代碼 不會執行
}
finally {
    
}

五、多個異步 都 執行完成後,再執行回調:Promise.all()

  • 作用: 將多個異步函數 包裝成一個 內部並行執行的 Promise 實例

  • 關鍵: 包裝後的 Promise 實例,內部函數 並行執行;減少了執行時間

  • 語法: const promise = Promise.all([p1, p2, p3])
    • p1、p2、p3都是 Promise 實例;如果不是Promise 實例,就會先調用 Promise.resolve 方法,將參數轉為 Promise 實例

    • 參數可以不是數組,但 必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例

  • 使用場景: 多個異步 都 執行完成後,再執行回調

Promise
  .all([runAsync1(), runAsync2(), runAsync3()])
  .then((results) => {
      console.log(results);
  });
  
// 如上:三個異步操作 會 並行執行,等到它們都執行完後,才會執行 then 裏面的代碼;
// 三個異步操作的結果,組成數組 傳給了 then; 也就是 輸出上述 result 為 [異步1數據,異步2數據,異步3數據]
  • 包裝後的 Promise 的狀態(區別所在)
    • 只有p1、p2、p3的狀態都變成 fulfilled,p的狀態才會變成 fulfilled;此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數

    • 只要p1、p2、p3之中有一個被 rejected,p的狀態就變成rejected;此時第一個被 reject 的實例的返回值,會傳遞給p的回調函數

  • Promise 的狀態 決定執行 then 回調,還是執行 catch 回調
const p1 = new Promise((resolve, reject) => {
        resolve(‘hello‘);
    })
    .then(result => result)
    .catch(e => e);

const p2 = new Promise((resolve, reject) => {
        throw new Error(‘報錯了‘);
    })
    .then(result => result);

Promise.all([p1, p2])
    .then((result) => {
        console.log(‘then‘);
        console.log(result)
    })
    .catch((e) => {
        console.log(‘error‘)
        console.log(e);
    });

// ‘error‘, Error: 報錯了
const p1 = new Promise((resolve, reject) => {
        resolve(‘hello‘);
    })
    .then(result => result)
    .catch(e => e);

const p2 = new Promise((resolve, reject) => {
        throw new Error(‘報錯了‘);
    })
    .then(result => result)
    .catch(e => e);

Promise.all([p1, p2])
    .then((result) => {
        console.log(‘then‘);
        console.log(result)
    })
    .catch((e) => {
        console.log(‘error‘)
        console.log(e);
    });

// ‘then‘, ["hello", Error: 報錯了]
// 解釋:因為 p2 有自己的 catch, 錯誤才 p2 處被攔截

六、多個異步 中 有一個完成,就執行回調:Promise.race()

  • 作用: 將多個異步函數 包裝成一個 內部並行執行的 Promise 實例

  • 關鍵: 包裝後的 Promise 實例,內部函數 並行執行;減少了執行時間

  • 語法: const promise = Promise.race([p1, p2, p3])
    • p1、p2、p3都是 Promise 實例;如果不是Promise 實例,(如果是函數)就會先調用下面講到的 Promise.resolve 方法,將參數轉為 Promise 實例

    • 參數可以不是數組,但 必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例

  • 使用場景: 多個異步 中 有一個完成,就執行回調

Promise
  .race([runAsync1(), runAsync2(), runAsync3()])
  .then((results) => {
      console.log(results);
  });
  
// 如上:三個異步函數會並行執行,只要有一個執行完,就會執行 then;
// 率先執行完的異步函數的結果 作為參數 傳遞給 then 中的回調函數
// 請求超時的 demo

const p1 = myAjax(‘/resource-that-may-take-a-while‘);
const p2 = new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error(‘request timeout‘)), 5000)
    });

Promise.race([p1, p2])
    .then(response => console.log(response))
    .catch(error => console.log(error));
  • 包裝後的 Promise 的狀態(區別所在)
    • p1、p2、p3 中 率先 改變狀態的 Promise 的狀態值,直接影響 包裝後的Promise 的狀態;
    • 那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數

六、快速生成 Promise 對象 的方法

1. Promise.resolve()

  • 作用: 將現有對象轉為 Promise 對象(狀態為 resolve

  • 等價於:

Promise.resolve(‘foo‘);

// 等價於

new Promise((resolve, reject) => {
    resolve(‘foo‘);
});
  • 參數的幾種形式:
    • 參數是 具有then方法的對象: 轉為 Promise 對象,並立即執行對象的 then 方法 【重點區分】

    • 參數是 Promise 實例: 那麽Promise.resolve將不做任何修改、原封不動地返回這個實例

    • 不帶有任何參數: 最快速生成 Promise 對象

// 參數是 具有then方法的對象

let thenable = {
    then: function (resolve, reject) {
        console.log(‘then‘);
        resolve(42);
    }
};

Promise.resolve(thenable);  //  輸出 then
// 不帶參數,快速生成 Promise 對象

Promise.resolve();

2. Promise.reject()

  • 作用: 返回一個新的 Promise 實例,該實例的狀態為 rejected

  • 等價於:

Promise.reject(‘出錯了‘);

// 等價於

new Promise((resolve, reject) => {
    reject(‘報錯了‘);
});
  • 參數: 無論參數是什麽都會原封不動的作為 錯誤理由 【重點區分】

七、Promise 的應用

  • Promise 實現 異步加載圖片
// Promise 實現 單張圖片 異步加載(預加載)

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

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

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

        image.src = url;
    });
}

loadImageAsync(‘./1png‘)
    .then((img) => {
        console.log(img); // 圖片標簽
    })
// Promise 實現 多張圖片 異步加載(預加載)

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

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

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

        image.src = url;
    });
}

function getImages(source) {
    const imgs = [];

    source.forEach(url => {
        loadImageAsync(url)
            .then((img) => {
                imgs.push(img);
            })
    });

    return imgs;
}

console.log(getImages([‘1.png‘, ‘2.png‘]));
// ES5 實現圖片異步加載

function getAsyncLoadImages(source) {
    const imgs = [];

    source.forEach(function (url, i) {
        imgs[i] = new Image();
        imgs[i].onload = function () {
            // console.log(imgs[i]);
        }

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

        imgs[i].src = url;
    })

    return imgs;
}

const result = getAsyncLoadImages([‘1.png‘, ‘2.png‘]);
console.log(result);

ES6 異步編程解決方案 之 Promise 對象