promise及非同步程式設計async await

2023-05-14 18:01:05

前置說明

ECMAScript 6 新增了正式的 Promise(期約)參照型別,支援優雅地定義和組織非同步邏輯。接下來幾個版本增加了使用 async 和 await 關鍵字定義非同步函數的機制
JavaScript 是單執行緒事件迴圈模型。非同步行為是為了優化因計算量大而時間長的操作,只要你不想為等待某個非同步操作而阻塞執行緒執行,那麼任何時候都可以使用

同步與非同步

JS中同步任務會立即執行並放入呼叫棧中,而非同步任務會被放入事件佇列中,等待呼叫棧中的任務執行完畢後再被推入呼叫棧中執行。當非同步任務被推入呼叫棧中執行時,它就變成了同步任務。這種機制被稱為事件迴圈。
同步行為對應記憶體中順序執行的處理器指令。每條指令都會嚴格按照它們出現的順序來執行,而每條指令執行後也能立即獲得儲存在系統本地(如暫存器或系統記憶體)的資訊。這樣的執行流程容易分析程式在執行到程式碼任意位置時的狀態(比如變數的值)。
例如

let x = 3; 
x = x + 4;

在程式執行的每一步,都可以推斷出程式的狀態。這是因為後面的指令總是在前面的指令完成後才會執行。等到最後一條指定執行完畢,儲存在 x 的值就立即可以使用。

非同步行為類似於系統中斷,即當前程序外部的實體可以觸發程式碼執行。非同步程式碼不容易推斷
例如,在定時回撥中執行一次簡單的數學計算:

let x = 3; 
setTimeout(() => x = x + 4, 1000);

這段程式雖與上面同步程式碼執行的任務一樣,都是把兩個數加在一起,但這一次執行執行緒不知道 x 值何時會改變,因為這取決於回撥何時從訊息佇列出列並執行。雖然這個例子對應的低階程式碼最終跟前面的例子沒什麼區別,但第二個指令塊(加操作及賦值操作)是由系統計時器觸發的,這會生成一個入隊執行的中斷。到底什麼時候會觸發這個中斷,這對 JavaScript 執行時來說是一個黑盒

非同步程式設計主要包含以下幾類,非同步程式設計傳統的解決方案是回撥函數,這種傳統的方式容易導致「回撥地獄」,即函數多層巢狀,讓程式碼難以閱讀,維護困難,空間複雜度大大增加

  • fs 檔案操作
    require('fs').readFile('./index.html', (err,data)=>{})
    
  • 資料庫操作
  • AJAX
      $.get('/server', (data)=>{})
    
  • 定時器
    setTimeout(()=>{}, 2000);
    

Promise 物件

Promise屬於ES6規範, 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函數和事件——更合理和更強大。期約故意將非同步行為封裝起來,從而隔離外部的同步程式碼

  1. 從語法上來說: Promise 是一個建構函式
  2. 從功能上來說: promise 物件用來封裝一個非同步操作並可以獲取其成功/失敗的結果值
const p=new Promise(()=>{})
console.log(p);

Promise 的狀態及結果

Promise 建構函式: Promise (excutor)

  • executor 函數: 執行器 (resolve, reject) => {}
  • executor 會在 Promise 內部立即同步呼叫(即executor函數直接放入呼叫棧中,而不是訊息佇列,new Promise()執行時就會立即執行executor 函數),非同步操作在執行器中執行
let p = new Promise((resolve, reject) => {
    // 同步呼叫
    console.log(111);
});
console.log(222);

期約是一個有狀態的物件,Promise範例物件中的屬性PromiseState儲存著該Promise範例的狀態,可能處於如下 3 種狀態之一:

  • 待定(pending):期約的最初始狀態,在待定狀態下,期約可以落定(settled)為resolved狀態或reject狀態。無論落定為哪種狀態都是不可逆的。只要從待定轉換為解決或拒絕,期約的狀態就不再改變。
    例如使用一個空函數物件來應付一下直譯器:
let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending>

之所以說是應付直譯器,是因為如果不提供執行器函數,就會丟擲 SyntaxError。
無論 resolve()和 reject()中的哪個被呼叫,狀態轉換都不可復原了。於是繼續修改狀態會靜默失敗,如下所示:

let p = new Promise((resolve, reject) => { 
 resolve();
 reject(); // 沒有效果
});
setTimeout(console.log, 0, p); // Promise <resolved>
  • 解決(resolved,有時候也稱為「兌現」,fulfilled):代表成功
  • 拒絕(rejected):代表失敗
    無論變為成功還是失敗, 都會有一個結果資料(這個資料儲存在Promise範例物件的PromiseResult屬性中),成功的結果資料一般稱為 value, 失敗的結果資料一般稱為 reason

期約主要有兩大用途:

  • 首先是抽象地表示一個非同步操作。期約的狀態代表期約是否完成。「待定」表示尚未開始或者正在執行中。「解決」表示已經成功完成,而「拒絕」則表示沒有成功完成
  • 在另外一些情況下,期約封裝的非同步操作會實際生成某個值,而程式期待期約狀態改變時可以存取這個值。相應地,如果期約被拒絕,程式就會期待期約狀態改變時可以拿到拒絕的理由。
    為了支援這兩種用例,每個期約只要狀態切換為解決,就會有一個私有的內部值(value)。類似地,每個期約只要狀態切換為拒絕,就會有一個私有的內部理由(reason)。無論是值還是理由,都是包含原始值或物件的不可修改的參照。二者都是可選的,而且預設值為 undefined。在期約到達某個落定狀態時執行的非同步程式碼始終會收到這個值或理由。
    由於期約的狀態是私有的,所以只能在內部進行操作。內部操作在期約的執行器函數中完成。執行器函數主要有兩項職責:初始化期約的非同步行為和控制狀態的最終轉換。其中,控制期約狀態的轉換是通過呼叫它的兩個函數引數實現的。這兩個函數引數通常都命名為 resolve()和 reject()。呼叫resolve()會把狀態切換為兌現,呼叫 reject()會把狀態切換為拒絕。另外,呼叫 reject()也會丟擲錯誤
let p1 = new Promise((resolve, reject) => resolve()); 
setTimeout(console.log, 0, p1); // Promise <resolved> 
let p2 = new Promise((resolve, reject) => reject()); 
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)

所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理
有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函數。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易

Promise方法

Promise.prototype.then()

Promise.prototype.then([onResolved|null],[onRejected|null])是為期約範例新增處理程式的主要方法。
引數:onResolved 處理程式和 onRejected 處理程式。這兩個引數都是可選的,如果提供的話,則會在期約分別進入「兌現」和「拒絕」狀態時執行。
返回值:一個新的Promise物件

可以指定多個回撥,當promise物件變為相應的狀態的時候就都會執行

let p = new Promise((resolve, reject) => {
    resolve('OK');
});
///指定回撥 - 1
p.then(value => {
    console.log(value);
});
//指定回撥 - 2
p.then(value => {
    alert(value);
});

問:then()返回的Promise物件的狀態是如何決定的呢?
① 如果丟擲異常, 新 promise 變為 rejected, reason 為丟擲的異常

let p = new Promise((resolve, reject) => {
    resolve('ok');
});
//執行 then 方法
let result = p.then(value => {
    console.log(value);
    //1. 丟擲錯誤
    throw '出了問題';
}, reason => {
    console.warn(reason);
});

② 如果返回的是非 promise 的任意值, 新 promise 變為 resolved, value 為返回的值

let p = new Promise((resolve, reject) => {
    resolve('ok');
});
//執行 then 方法
let result = p.then(value => {
    console.log(value);
	//2. 返回結果是非 Promise 型別的物件
    return 521;
}, reason => {
    console.warn(reason);
});
console.log(result);

③ 如果返回的是另一個新 promise, 此 promise 的結果就會成為新 promise 的結果

let p = new Promise((resolve, reject) => {
    resolve('ok');
});
//執行 then 方法
let result = p.then(value => {
    console.log(value);
    //3. 返回結果是 Promise 物件
    return new Promise((resolve, reject) => {
        // resolve('success');
        reject('error');
    });
}, reason => {
    console.warn(reason);
});
console.log(result);

不管呼叫的是onResolved還是onRejected函數,返回的新promise物件主要由返回值決定(丟擲錯誤的情況除外)

then()的鏈式呼叫

問:promise 如何串連多個操作任務?
(1) promise 的 then()返回一個新的 promise, 可以形成 then()的鏈式呼叫
(2) 通過 then 的鏈式呼叫串連多個同步/非同步任務

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('OK');
    }, 1000);
});
const result=p.then(value => {
    return new Promise((resolve, reject) => {
        resolve("success");
    });
}).then(value => {
    console.log(value);
}).then(value => {
    console.log(value);
})
console.log(result);

p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}) // Promise (resolved): 'success'

p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}).then(value => {
console.log(value);
})// Promise (resolved): undefined 因為沒有返回值也沒有丟擲錯誤,所以值是undefined, 因為undefined不是Promise物件,所以返回的Promise狀態為resolved

p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}).then(value => {
console.log(value);
}).then(value => {
console.log(value);
})// Promise (resolved): undefined

中斷then()鏈
方法:在then()的回撥函數中返回一個 pendding 狀態的 promise 物件。因為pendding狀態的promise物件不會觸發onResolved()或onRejected()函數

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('OK');
    }, 1000);
});
p.then(value => {
    console.log(111);
    //有且只有一個方式
    return new Promise(() => {});// 
}).then(value => {
    console.log(222);
}).then(value => {
    console.log(333);
}).catch(reason => {
    console.warn(reason);
});

穿透:當沒有指定相應Promise狀態的回撥函數時,就可以跳過執行該then()
練習:分析輸出結果
1.

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        // resolve('OK');
        reject('Err');
    }, 10);
});
p.then(null,reason => {
    console.log(111);
}).then(null,reason => {
    console.log(222);
}).then(null,reason => {
    console.log(333);
}).catch(reason => {
    console.warn(reason);
});

分析:p.then(null,reason => {
console.log(111);
})的返回值是Promise (resolved):undefined, 後面沒有對應的onResolved的回撥函數,就跳過了執行,所以控制檯只輸出了111

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('OK');
        // reject('Err');
    }, 10);
});
// console.log(p.then(value => {
//     console.log(111);
// },reason=>{
//     throw '失敗啦!';
// })==p.then(value => {
//     console.log(111);
// },reason=>{
//     throw '失敗啦!';
// }).then(value => {
//     console.log(222);
// })); // false

console.log(p.then(value => {
    console.log(111);
},reason=>{
    throw '失敗啦!';
}).then(value => {
    console.log(222);
}).then(value => {
    console.log(333);
}));

p.then(value => {
    throw '失敗啦!';
}).then(value => {
    console.log(222);
}).then(value => {
    console.log(333);
}).catch(reason => {
    console.warn(reason);
});

p.then(value => {
throw '失敗啦!';
})返回Promise (rejected): '失敗啦!',後面沒有對應的onRejected()回撥函數,就跳過了執行,所以就直接執行catch()來捕獲錯誤資訊

Promise.prototype.catch()

Promise.prototype.catch(onRejected)
catch()基於then()做了一個單獨的封裝,只接收rejected狀態的回撥函數

給Promise物件設定回撥函數的方法有Promise.prototype.then()和Promise.prototype.catch(),注意Promise.prototype.catch()只能指定錯誤的回撥函數

Promise.resolve()

靜態方法,不是實體方法
期約並非一開始就必須處於待定狀態,然後通過執行器函數才能轉換為落定狀態。通過呼叫Promise.resolve()靜態方法,可以範例化一個解決的期約。
下面兩個期約範例實際上是一樣的:

let p1 = new Promise((resolve, reject) => resolve()); 
let p2 = Promise.resolve();

引數:成功的資料或 promise 物件
說明: 返回一個成功/失敗的 promise 物件

  • 引數為非Promise物件,則返回一個成功的Promise物件,成功的結果為該引數
let p1 = Promise.resolve(521);
console.log(p1);

let p = Promise.resolve(new Error('foo')); 
setTimeout(console.log, 0, p); // Promise <resolved>: Error: foo
  • 引數為Promise物件時,則返回該Promise物件
    對這個靜態方法而言,如果傳入的引數本身是一個期約,那它的行為就類似於一個空包裝。因此,Promise.resolve()可以說是一個冪等方法
let p = Promise.resolve(7); 
setTimeout(console.log, 0, p === Promise.resolve(p)); // true 
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true 
// 這個冪等性會保留傳入期約的狀態:
let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending> 
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending> 
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
let p2 = Promise.resolve(new Promise((resolve, reject) => {
    resolve('OK');
}));
console.log(p2);
p2.catch(reason => {
    console.log(reason);
})

let p2 = Promise.resolve(new Promise((resolve, reject) => {
    reject('Error');
}));
console.log(p2);
p2.catch(reason => {
    console.log(reason);
})

Promise.reject()

與 Promise.resolve()類似,Promise.reject()會範例化一個拒絕的期約並丟擲一個非同步錯誤(這個錯誤不能通過 try/catch 捕獲,而只能通過拒絕處理程式捕獲)。
下面的兩個期約範例實際上是一樣的:

let p1 = new Promise((resolve, reject) => reject()); 
let p2 = Promise.reject();

引數:失敗的理由或 promise 物件
說明: 返回一個失敗的 promise 物件

  • 引數為非Promise物件時,返回的失敗Promise物件的理由就為該引數值
let p1 = Promise.reject(521);
console.log(p1);

  • 引數為Promise物件時,返回的失敗Promise物件的理由就為該引數值(即失敗的理由就是該Promise物件)
let p2 = Promise.reject(new Promise((resolve, reject) => {
    resolve('OK');
}));
console.log(p2);

Promise.all()

引數: 包含 n 個 promise 的陣列
說明: 返回一個新的 promise, 只有所有的 promise 都成功該promise才成功,成功結果為所有promise成功結果組成的陣列。只要有一個promise失敗了該promise就失敗,失敗理由為第一個失敗的promise的失敗理由

  • 當所有promise都成功時,返回一個新的成功的promise,成功結果為所有promise成功結果組成的陣列
let p1 = new Promise((resolve, reject) => {
    resolve('OK');
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');
const result = Promise.all([p1, p2, p3]);
console.log(result);

  • 當有失敗的promise時該promise就失敗,失敗理由為第一個失敗的promise的失敗理由
let p1 = new Promise((resolve, reject) => {
    resolve('OK');
})
let p2 = Promise.reject('Error1');
let p3 = Promise.reject('Error2')
const result = Promise.all([p1, p2, p3]);
console.log(result);

Promise.race()

通過這種方式,可以檢測頁面中某個請求是否超時,並輸出相關的提示資訊。
引數: 包含 n 個 promise 的陣列
說明: 返回一個新的 promise, 該promise等於第一個完成的 promise(即第一個確定狀態的promise)

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('OK');
    }, 1000);
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');
//呼叫
const result = Promise.race([p1, p2, p3]);
console.log(result);

改變Promise物件狀態的方式

  1. promise回撥函數中改變
let p = new Promise((resolve, reject) => {
    //1. resolve 函數
    // resolve('ok'); // pending   => fulfilled (resolved)
    //2. reject 函數
    // reject("error");// pending  =>  rejected 
    //3. 丟擲錯誤
    // throw '出問題了';// pending  =>  rejected 
});
console.log(p);


2. 呼叫實體方法Promise.resolve()或Promise.reject()

基本用法

ES6 規定,Promise物件是一個建構函式,用來生成Promise範例

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

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

Promise建構函式接受一個函數作為引數,該函數的兩個引數分別是resolvereject。它們是兩個函數,由 JavaScript 引擎提供,不用自己部署

Promise範例生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函數。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

下面是一個抽獎範例

<body>
    <button id="btn">點選抽獎</button>
    <script>
        const btn=document.getElementById("btn")
        btn.onclick=function(){
            // 每次點選按鈕建立一個Promise範例物件
            const p =new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    let n =parseInt(Math.random()*101)//取值[1,100]的整數
                    if(n<30){
                        resolve(n)
                    }else{
                        reject(n)
                    }
                },10)
            })
            p.then((value)=>{
                alert("恭喜中獎!中獎數位為"+value);
            },(reason)=>{
                alert("再接再厲! 中獎數位為"+reason);
            })
        }
    </script>
</body>

載入圖片資源例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div></div>
    <script>
        function loadImageAsync(url) {
            var promise = 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;
            });
            return promise;
        }
        loadImageAsync("http://iwenwiki.com/api/vue-data/vue-data-1.png")
        .then(function(data){
            console.log(data);
            $("div").append(data)
        },function(error){
            $("div").html(error)
        })
    </script>
</body>
</html>

實時效果反饋

1. Promise的作用是什麼,下列描述正確的是:

A Promise 是非同步程式設計的一種解決方案,可以將非同步操作以同步操作的流程表達出來

B Promise是同步程式設計的一種解決方案,可以將同步操作以非同步操作的流程表達出來

C Promise使得控制同步操作更加容易

D Promise還不是ES6的標準,目前是社群版本

答案

1=>A

Promise物件_Ajax實操

Promise封裝Ajax,讓網路請求的非同步操作變得更簡單

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        const getJSON = function (url) {
            const promise = new Promise(function (resolve, reject) {
                const handler = function () {
                    if (this.readyState !== 4) {
                        return;
                    }
                    if (this.status === 200) {
                        resolve(this.response);
                    } else {
                        reject(new Error(this.statusText));
                    }
                };
                const client = new XMLHttpRequest();
                client.open("GET", url);
                client.onreadystatechange = handler;
                client.responseType = "json";
                client.setRequestHeader("Accept", "application/json");
                client.send();
            });
            return promise;
        };
        		getJSON("http://iwenwiki.com/api/blueberrypai/getIndexBanner.php").then(function (json) {
            console.log(json);
        }, function (error) {
            console.error('出錯了', error);
        });
    </script>
</body>
</html>

util.promisefy()函數

傳入一個遵循常見的錯誤優先的回撥風格的函數(即以(err, value)=>{...}回撥作為最後一個引數),並返回一個返回值為promise物件的函數版本
「錯誤優先」是指回撥函數中,錯誤引數為回撥函數的第一個引數。node.js環境中fs模組中的非同步api大多是這種風格的
引數:函數
返回值:函數,返回的該函數的返回值是promise物件
將函數promise化的好處:函數promise化之後,函數的返回值就變成了promise物件,這樣就可以呼叫promise的實體方法then()或catch(), 利用它們的鏈式寫法,就能避免回撥函數多層巢狀

範例:呼叫util.promisefy()函數,將fs.readFile()函數promise化

//引入 util 模組
const util = require('util');
//引入 fs 模組
const fs = require('fs');
//promise化fs.readFile()函數
let mineReadFile = util.promisify(fs.readFile);
// promise化之後就可以呼叫promise範例的then()或catch()方法
mineReadFile('./resource/content.txt').then(value=>{
    console.log(value.toString());
});

封裝promisefy函數

在實際應用中,一個函數滿足這幾個條件,就可以被 promisify 化:

  • 該方法必須包含回撥函數
  • 回撥函數必須執行
  • 回到函數第一個引數代表 err 資訊,第二個引數代表成功返回的結果
// promisefy()返回一個函數,返回的該函數的返回值是Promise物件
// fn指要promise化的函數
const promisefy = (fn) => {
  // ‘...args’表示剩餘引數
  return function(...args){
    return new Promise((resolve,reject)=>{
      fn(...args,(err,data)=>{
        if(err) reject(err);
        resolve(data)
      })
    })
  }
}

範例(第14屆藍橋杯省賽第三期模擬題)
題目

下面就請你以 Node.js 中常用的讀取檔案操作為例,封裝一個 Promisefy 函數,將回撥形式呼叫的讀取檔案方法轉換成一個 Promise 的版本。
目錄結構如下:

  • index.js 是需要補充程式碼的 js 檔案。
  • test.md 供讀取的範例檔案。

請在 index.js 檔案中的補全程式碼,完成 promisefy 函數的封裝。將 fs 中的 readFile 方法 promise 化。也就是說 readFileSync 方法執行後,會返回一個 promise,可以呼叫 then 方法執行成功的回撥或失敗的回撥。
在控制檯執行:node index, 此時應列印出 true,即:回撥形式的 fs.readFile 方法讀取同個檔案的結果與 Promise 形式讀取結果一致。

參考答案

const fs = require('fs')
const path = require('path')
const textPath = path.join(__dirname, '/test.md')

// 讀取範例檔案
fs.readFile(textPath, 'utf8', (err, contrast) => {
  // 通過promisefy轉化為鏈式呼叫
  const readFileSync = promisefy(fs.readFile)

  readFileSync(textPath, 'utf8')
    .then((res) => {
      console.log(res === contrast) // 此處結果預期:true,即promise返回內容與前面讀取內容一致
    })
    .catch((err) => {})
})

const promisefy = (fn) => {
  // TODO 此處完成該函數的封裝
  // ‘...args’表示剩餘引數
  return function(...args){
    return new Promise((resolve,reject)=>{
      fn(...args,(err,data)=>{
        if(err) reject(err);
        resolve(data)
      })
    })
  }
}

module.exports = promisefy // 請勿刪除該行程式碼

封裝Promise類實戰

點選檢視程式碼
// 在非同步任務中不能丟擲錯誤,即使是內建的Promise也捕獲不到

class Promise{
    constructor(executor){
        // 設定預設的PromiseState和PromiseResult
        // 因為Promise()建構函式是以範例的方式呼叫(new運運算元),所以this指向範例
        this.PromiseState='pendding'
        this.PromiseResult=null
        // 將callback物件定義到範例內部,用來儲存後面通過then()或catch()指定的onResolved()和onRejected()函數,這是考慮到executor中通過非同步任務改變promise範例狀態和多個then()指定了多個onResolved()或onRejected()回撥的情況
        this.callbacks=[];
        // 而resolve()和reject()是以函數形式呼叫的,this為window物件,所以這裡用self儲存指向範例的this
        const self=this;
        function resolve(data){
            // 該判斷為了避免狀態重複改變
            if(self.PromiseState!=='pendding') return;
            self.PromiseState='fulfilled'//或'resolved'
            self.PromiseResult=data
            // 注意要在Promise範例狀態改變並且資料賦值之後呼叫onResolved
            // 注意在呼叫前要判斷有沒有定義onResolved()函數,所以就看callbacks陣列第一個元素有沒有onResolved屬性
            // if(self.callbacks[0].onResolved){
                // 通過setTimeout將then中的回撥設定成非同步, 因為內建的Promise的實體方法then()中的回撥是非同步的

                setTimeout(()=>{
                    self.callbacks.forEach(item => {
                        item.onResolved(data)
                    })
                });
            // }
        };
        function reject(data){
            // 該判斷為了避免狀態重複改變
            if(self.PromiseState!=='pendding') return;
            self.PromiseState='rejected'
            self.PromiseResult=data
            // 注意要在Promise範例狀態改變並且資料賦值之後呼叫onRejected
            // 注意在呼叫前要判斷有沒有定義onRejected()函數,所以就看callbacks陣列第一個元素有沒有onRejected屬性
            // if(self.callbacks[0].onRejected){
                // 通過setTimeout將then中的回撥設定成非同步, 因為內建的Promise的實體方法then()中的回撥是非同步的

                setTimeout(()=>{
                    self.callbacks.forEach(item => {
                        item.onRejected(data)
                    })
                })
                
            // }
        };
        try{
            // executor在傳入時就能確定是個函數,所以這裡不需要再定義executor, 只是呼叫即可
            executor(resolve,reject)
        }catch(e){
            reject(e)
        }
    }

    //新增 then 方法
    then(onResolved, onRejected){
        self=this
        // 考慮異常穿透情況
        if(typeof onRejected!=='function'){
            onRejected=reason=>{
                throw reason;
            }
        }
        if(typeof onResolved!=='function'){
            onResolved=value=>value // (value)=>{return value}的簡寫形式
        }
        // 
        
        return new Promise((resolve,reject)=>{
            // 這個callback()函數得放到返回的Promise範例裡,否則會報錯,說resolve和reject未定義
            function callback(type){
                try{
                    // 因為callback()是以函數形式呼叫的,此時this指向window物件,所以此處引數不能為this.PromiseResult
                    let result=type(self.PromiseResult)
                    if(result instanceof Promise){
                        result.then(v=>{
                            resolve(v)
                        },e=>{
                            reject(e)
                        })
                    }else{// 丟擲錯誤時result是undefined還是null? 不進入該分支應該
                        resolve(result)
                    }
                }catch(e){//考慮丟擲錯誤的情況
                    reject(e)
                }
            }
            // 考慮executor中同步任務改變promise範例狀態
            
            if(this.PromiseState==='fulfilled'){
                // 通過setTimeout將then中的回撥設定成非同步, 因為內建的Promise的實體方法then()中的回撥是非同步的
                setTimeout(()=>{
                    callback(onResolved)
                })
            }
            
            if(this.PromiseState==='rejected'){
                // 通過setTimeout將then中的回撥設定成非同步, 因為內建的Promise的實體方法then()中的回撥是非同步的

                setTimeout(()=>{
                    callback(onRejected)
                })
            }
            // 

            // 考慮executor中非同步任務改變promise範例狀態
            if(this.PromiseState==='pendding'){
                this.callbacks.push({
                    onResolved:function(){
                        callback(onResolved)
                    },
                    onRejected:function(){
                        callback(onRejected)
                    }
                })
            }
        })
    }
    // 定義Promise.prototype.catch()
    catch(onRejected){
        return this.then(undefined,onRejected)
    }
    // 定義Promise.resolve()靜態方法,注意靜態方法要加關鍵詞static
    static resolve(value){
        return new Promise((resolve,reject)=>{
            if(value instanceof Promise){
                value.then(
                    v=>resolve(v),
                    e=>reject(e))
            }else{
                resolve(value)
            }
        })
    }
    // 定義Promise.reject()靜態方法
    static reject(reason){
        return new Promise((resolve,reject)=>{
            reject(reason)
        })
    }
    // 定義Promise.all()靜態方法
    static all(promises){
        let arr=[]
        let count=0;
        return new Promise((resolve,reject)=>{
            for(let i in promises){
                promises[i].then(v=>{
                    arr[i]=v;
                    count++;
                    if(count===promises.length){
                        resolve(arr)
                    }
                },r=>{
                    reject(r)
                })
            }
            // 為什麼這個判斷不能放在這
            // if(count===promises.length){
            //     resolve(arr)
            // }
        })
    }
    // 定義Promise.race()靜態方法
    static race(promises){
        return new Promise((resolve,reject)=>{
            for(let i in promises){
                promises[i].then(
                    v=>resolve(v),
                    r=>reject(r))
            }
        })
    }
}

Async 函數

async 英文單詞的意思是非同步,雖然它是 ES8 中新增加的一個關鍵字,但它的本質是一種語法糖寫法(語法糖是一種簡化後的程式碼寫法,它能方便程式設計師的程式碼開發),async 通常寫在一個函數的前面,表示這是一個非同步請求的函數,將返回一個 Promise 物件,並可以通過 then 方法取到函數中的返回值
使用 async 關鍵字可以讓函數具有非同步特徵,但總體上其程式碼仍然是同步求值的。而在引數或閉包方面,非同步函數仍然具有普通 JavaScript 函數的正常行為。非同步函數的返回值會被 Promise.resolve()包裝成一個期約物件。非同步函數始終返回期約物件。
ES2017 標準引入了 async 函數,使得非同步操作變得更加方便
async函數可以將非同步操作變為同步操作

  1. 函數的返回值為 promise 物件
  2. promise 物件的結果由 async 函數執行的返回值決定
    • 如果返回值是一個非Promise型別的資料,相當於執行了Promise.resolve(), 則返回的Promise範例狀態為fulfilled
    • 如果返回值是一個Promise物件,相當於執行了Promise.resolve(返回值),則返回的Promise範例等效於該Promise物件
async function main(){
    // return 521; // Promise<fulfilled>:521
    return new Promise((resolve, reject) => {
        // resolve('OK'); // Promise<fulfilled>:'OK'
        reject('Error'); // Promise<rejected>:'Error'
    });
    //3. 丟擲異常
    // throw "Oh NO"; // Promise<rejected>:"Oh NO"
}
let result = main();
console.log(result);

範例程式碼

function print(){
    setTimeout(() =>{
        console.log("定時器");
    },1000)
    console.log("Hello");
}

print()

基本語法

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

非同步應用

function ajax(url){
    return new Promise(function(resolve,reject){
        $.getJSON(url,function(result){
            resolve(result)
        },function(error){
            reject(error) 
        })
    })
}

async function getInfo(){
    let ids = await ajax("http://iwenwiki.com/api/generator/list.php")
    let names = await ajax("http://iwenwiki.com/api/generator/id.php?id="+ids[0])
    let infos = await ajax("http://iwenwiki.com/api/generator/name.php?name=" + names.name)
    console.log(infos);
}

getInfo();

實時效果反饋

1. Async 是什麼:

A Async是完成網路請求,如Ajax一樣

B Async的作用是完成非同步網路請求,如Ajax一樣

C Async使得非同步操作變得更加方便

D Async是新的網路請求解決方案

答案

1=>C

await 表示式

await 可以理解為 async wait 的簡寫,表示等待非同步執行完成。await 後可以返回任意的表示式,如果是正常內容,則直接執行,如果是非同步請求,必須等待請求完成後,才會執行下面的程式碼

  1. await 右側的表示式一般為 promise 物件, 但也可以是其它的值
  2. 如果表示式是 promise 物件, await 返回的是 promise 成功的值, promise物件狀態是rejected時,需要捕獲錯誤來檢視錯誤理由
  3. 如果表示式是其它值, 直接將此值作為 await 的返回值

注意

  1. await 必須寫在 async 函數中, 但 async 函數中可以沒有 await
  2. 如果 await 的 promise 失敗了, 就會丟擲異常, 需要通過 try...catch 捕獲處理
// 函數 p 返回的是一個 Promise 物件,在物件中,延時 2 秒,執行成功回撥函數,相當於模擬一次非同步請求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函數執行時,將函數的實參值 v ,作為執行成功回撥函數的返回值。
      resolve(v);
    }, 2000);
  });
}

// 一個用於正常輸出內容的函數
function log() {
  console.log("2.正在操作");
}

async function fn() {
  console.log("1.開始");
  await log();
  let p1 = await p("3.非同步請求");
  console.log(p1);
  console.log("4.結束");
}
fn();


根據頁面效果,原始碼解析如下:

  • fn 函數執行後,首先,會按照程式碼執行流程,先輸出「1.開始」。
  • 其次,對於沒有非同步請求的內容,在 await 後面都將會正常輸出,因此,再輸出「2.正在操作」。
  • 如果 await 後面是非同步請求,那麼,必須等待請求完成並獲取結果後,才會向下執行。
  • 根據上述分析,由於 方法 p 是一個非同步請求,因此,必須等待它執行完成後,並將返回值賦給變數 p1,再執行向下程式碼。
  • 所以,最後的執行順序是,先輸出 「3.非同步請求」,再輸出 "4.結束",在 async 函數中的執行順序,如下圖所示。

async function main(){
    let p = new Promise((resolve, reject) => {
        // resolve('OK');
        reject('Error');
    })
    //1. 右側為promise的情況
    // let res = await p;
    //2. 右側為其他型別的資料, 但這種情況不常見
    // let res2 = await 20;
    //3. 如果promise是失敗的狀態,需要捕獲錯誤來檢視錯誤理由
    try{
        let res3 = await p;
        console.log(res3);
    }catch(e){
        console.log(e);
    }
}
main();

async與await結合實踐

1.txt

觀書有感
       -朱熹
半畝方塘一鑑開
天光雲影共徘徊。

2.txt

問渠那得清如許?
為有源頭活水來。

3.txt


---------
中華古詩詞
const fs = require('fs')
const util= require('util')
// 將fs.readFile promise化,即將非同步函數轉換成同步的形式(只是轉換形式,實質還是非同步)
const myReadFile=util.promisify(fs.readFile)
// 傳統的讀多個檔案,並將檔案內容串聯起來輸出。缺點是會有多個回撥巢狀
// fs.readFile('./resource/1.txt',(err,data1)=>{
//     if(err) throw err;
//     fs.readFile('./resource/2.txt',(err,data2)=>{
//         if(err) throw err;
//         fs.readFile('./resource/2.txt',(err,data3)=>{
//             if(err) throw err;
//             console.log(data1+data2+data3);
//         })
//     })
// })

// 結合使用async和await, 將非同步任務以同步的形式呼叫,減少巢狀,結構更清晰
async function main(){
    try{
        const data1=await myReadFile('./resource/1.txt')
        const data2=await myReadFile('./resource/2.txt')
        const data3=await myReadFile('./resource/3.txt')
        console.log(data1+data2+data3);
    }catch(e){
        console.log(e);
    }
}
main()

async與await結合傳送AJAX請求

//axios是基於Promise封裝的AJAX請求庫
function sendAJAX(url){
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.responseType = 'json';
        xhr.open("GET", url);
        xhr.send();
        //處理結果
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4){
                //判斷成功
                if(xhr.status >= 200 && xhr.status < 300){
                    //成功的結果
                    resolve(xhr.response);
                }else{
                    reject(xhr.status);
                }
            }
        }
    });
}

//段子介面地址 https://api.apiopen.top/getJoke
let btn = document.querySelector('#btn');

btn.addEventListener('click',async function(){
    //獲取段子資訊
    let duanzi = await sendAJAX('https://api.apiopen.top/getJoke');
    console.log(duanzi);
});