JS的非同步
JS語言的執行環境是“單執行緒”的,即指一次只能完成一件任務;如果有多個任務,那麼必須排隊,前面一個任務完成,再執行後一個任務,以此類推。這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。
為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。
"同步模式"就是上一段的模式,後一個任務等待前一個任務結束,然後再執行,程式的執行順序與任務的排列順序是一致的、同步的;"非同步模式"則完全不同,每一個任務有一個或多個回撥函式(callback),前一個任務結束後,不是執行後一個任務,而是執行回撥函式,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的、非同步的。
"非同步模式"非常重要。在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在伺服器端,"非同步模式"甚至是唯一的模式,因為執行環境是單執行緒的,如果允許同步執行所有http請求,伺服器效能會急劇下降,很快就會失去響應。
常用的非同步程式設計模式
- 回撥函式
即f1,f2兩個函式,f2要等待f1執行結果後執行,即 f1(f2) - 事件驅動的方式
f1.on('done', f2); (JQ寫法,f1完成時,trigger("done")則執行f2) - 釋出-訂閱 模式
- Promise物件實現
Promise物件
本文著重講ES6的Promise物件的定義和用法 阮一峰老師的ES6詳解 - Promise物件 相信大家在學習ES6的過程中都或多或少的學習過阮老師的ES6教程,那麼這裡簡單舉一些例子講述Promise物件的特點和使用方法
基礎使用方法
ES6提供Promise建構函式,我們創造一個Promise例項,Promise建構函式接收一個函式作為引數,這個傳入的函式有兩個引數,分別是兩個函式 resolve
和reject
作用是,resolve
將Promise的狀態由未成功變為成功,將非同步操作的結果作為引數傳遞過去;相似的是reject
則將狀態由未失敗轉變為失敗,在非同步操作失敗時呼叫,將非同步操作報出的錯誤作為引數傳遞過去。
例項建立完成後,可以使用then
方法分別指定成功或失敗的回撥函式,比起f1(f2(f3))的層層巢狀的回撥函式寫法,鏈式呼叫的寫法更為美觀易讀
let promise = new Promise((resolve, reject)=>{
reject("拒絕了");
});
promise.then((data)=>{
console.log('success' + data);
}, (error)=>{
console.log(error)
});
執行結果:"拒絕了"
複製程式碼
Promise的特點
- 物件不受外界影響,初始狀態為pending(等待中),結果的狀態為resolve和reject,只有非同步操作的結果決定這一狀態
- 狀態只能由pending變為另外兩種的其中一種,且改變後不可逆也不可再度修改,
即pending -> resolved 或 pending -> reject
let promise = new Promise((resolve, reject)=>{
reject("拒絕了");
resolve("又通過了");
});
promise.then((data)=>{
console.log('success' + data);
}, (error)=>{
console.log(error)
});
執行結果: "拒絕了"
複製程式碼
上述程式碼不會再執行resolve的方法
then方法的規則
then
方法下一次的輸入需要上一次的輸出- 如果一個promise執行完後 返回的還是一個promise,會把這個promise 的執行結果,傳遞給下一次
then
中 - 如果
then
中返回的不是Promise物件而是一個普通值,則會將這個結果作為下次then的成功的結果 - 如果當前
then
中失敗了 會走下一個then
的失敗 - 如果返回的是undefined 不管當前是成功還是失敗 都會走下一次的成功
- catch是錯誤沒有處理的情況下才會走
then
中不寫方法則值會穿透,傳入下一個then
中
用node fs模組讀取檔案的流程來測試 我們建立一個讀取檔案的方法,在Promise中定義如果讀取成功則展示檔案的內容,否則報出錯誤
let fs = require('fs');
function read(file, encoding) {
return new Promise((resolve, reject)=>{
fs.readFile(filePath, encodeing, (err, data)=> {
if (err) reject(err);
resolve(data);
});
})
}
複製程式碼
由於想看到多次連貫回撥,我們專門設定3個txt檔案,其中1號檔案的內容為2號檔案的檔名,2號檔案的內容為3號檔案的檔名,3號中展示最終內容
執行程式碼如下:
read('1.promise/readme.txt', 'utf8').then((data)=>{
console.log(data)
});
複製程式碼
讀取一個檔案的列印結果為,readme2.txt
我們改造這個程式碼,新增多個回撥,在最後一個之前的所有then中都return出當前返回的promise物件
read('readme.txt', 'utf8').then((data)=>{
return read(data, 'utf8');
}).then((data)=>{
return read(data, 'utf8')
}).then((data)=>{
console.log(data);
});
最終輸出 readme3.txt的內容
複製程式碼
再對下一步then進行新的處理,我們對readme3.txt的內容進行加工並返回
read('readme.txt', 'utf8').then((data)=>{
return read(data, 'utf8');
}).then((data)=>{
return read(data, 'utf8')
}).then(data=>{
return data.split('').reverse().join(); // 這一步返回的是一個普通值,普通值在下一個then會作為resolve處理
}).then(null,data=>{ // 特意不對成功做處理,放過這一個值,進入下一步
throw new Error('出錯') // 由於上一步返回的是普通值,走成功回撥,不會走到這裡
}).then(data=>{
console.log(data) // 最終會列印出來上上步的普通值
});
複製程式碼
這裡我們將內容處理後,則將一個普通值傳給了下次的then,但是由於下一個then沒有處理成功方法(null)
這個普通值會繼續傳入下一個then,最終會作為成功值列印出來。最後我們看一下對錯誤的處理,在處理完readme3.txt的結果後,我們將這個值傳入下一個then中,令其作為一個檔名開啟,然而此時已經找不到這個不存在的檔案了,那麼在最後一步就會列印出報錯的結果
read('readme.txt', 'utf8').then((data)=>{
return read(data, 'utf8');
}).then((data)=>{
return read(data, 'utf8')
}).then(data=>{
return data.split('').reverse().join();
}).then(null,data=>{
throw new Error('出錯')
}).then(data=>{
return read(data, 'utf8')
}).then(null,(err)=>{
console.log(err)
});
複製程式碼
結果:
Promises A+ (Promises Aplus)
Promises Aplus規範即規定了Promise的原理,原始碼規範等,通過這個規範,我們可以自己實現一個基於PromiseA+規範的Promise類庫,下面我們展示一下原始碼的實現
/**
* Promise 實現 遵循promise/A+規範
* 官方站: https://promisesaplus.com/
* Promise/A+規範譯文:
* https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
*/
// promise 三個狀態
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(excutor) {
let self = this; // 快取當前promise例項物件
self.status = PENDING; // 初始狀態
self.value = undefined; // fulfilled狀態時 返回的資訊
self.reason = undefined; // rejected狀態時 拒絕的原因
self.onFulfilledCallbacks = []; // 儲存fulfilled狀態對應的onFulfilled函式
self.onRejectedCallbacks = []; // 儲存rejected狀態對應的onRejected函式
function resolve(value) { // value成功態時接收的終值
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 為什麼resolve 加setTimeout?
// 2.2.4規範 onFulfilled 和 onRejected 只允許在 execution context 棧僅包含平臺程式碼時執行.
// 這裡的平臺程式碼指的是引擎、環境以及 promise 的實施程式碼。實踐中要確保 onFulfilled 和 onRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。
setTimeout(() => {
// 呼叫resolve 回撥對應onFulfilled函式
if (self.status === PENDING) {
// 只能由pedning狀態 => fulfilled狀態 (避免呼叫多次resolve reject)
self.status = FULFILLED;
self.value = value;
self.onFulfilledCallbacks.forEach(cb => cb(self.value));
}
});
}
function reject(reason) { // reason為失敗態時接收的原因
setTimeout(() => {
// 呼叫reject 回撥對應onRejected函式
if (self.status === PENDING) {
// 只能由pedning狀態 => rejected狀態 (避免呼叫多次resolve reject)
self.status = REJECTED;
self.reason = reason;
self.onRejectedCallbacks.forEach(cb => cb(self.reason));
}
});
}
// 捕獲在excutor執行器中丟擲的異常
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
複製程式碼
這一部分程式碼我們對resolve和reject進行了判斷處理,接著我們構造then
方法
/**
* [註冊fulfilled狀態/rejected狀態對應的回撥函式]
* @param {function} onFulfilled fulfilled狀態時 執行的函式
* @param {function} onRejected rejected狀態時 執行的函式
* @return {function} promise2 返回一個新的promise物件
*/
Promise.prototype.then = function (onFulfilled, onRejected) {
// 成功和失敗的回撥 是可選引數
// onFulfilled成功的回撥 onRejected失敗的回撥
let self = this;
let promise2;
// 需要每次呼叫then時都返回一個新的promise
promise2 = new Promise((resolve, reject) => {
// 成功態
if (self.status === 'resolved') {
setTimeout(()=>{
try {
// 當執行成功回撥的時候 可能會出現異常,那就用這個異常作為promise2的錯誤的結果
let x = onFulfilled(self.value);
//執行完當前成功回撥後返回結果可能是promise
resolvePromise(promise2,x,resolve,reject);
} catch (e) {
reject(e);
}
},0)
}
// 失敗態
if (self.status === 'rejected') {
setTimeout(()=>{
try {
let x = onRejected(self.reason);
resolvePromise(promise2,x,resolve,reject);
} catch (e) {
reject(e);
}
},0)
}
if (self.status === 'pending') {
// 等待態時,當一部呼叫resolve/reject時,將onFullfilled/onReject收集暫存到集合中
self.onResolvedCallbacks.push(() => {
setTimeout(()=>{
try {
let x = onFulfilled(self.value);
resolvePromise(promise2,x,resolve,reject);
} catch (e) {
reject(e);
}
},0)
});
self.onRejectedCallbacks.push(() => {
setTimeout(()=>{
try {
let x = onRejected(self.reason);
resolvePromise(promise2,x,resolve,reject);
} catch (e) {
reject(e);
}
},0)
});
}
});
return promise2
}
// 其中規範要求對回撥中增加setTimeout處理
複製程式碼
可以看到resolve和reject都有一個處理新promise的方法resolvePromise,對其進行封裝,達到處理不同情況的目的
/**
* 對resolve 進行改造增強 針對resolve中不同值情況 進行處理
* @param {promise} promise2 promise1.then方法返回的新的promise物件
* @param {[type]} x promise1中onFulfilled的返回值
* @param {[type]} resolve promise2的resolve方法
* @param {[type]} reject promise2的reject方法
*/
function resolvePromise(promise2,x,resolve,reject){
if(promise2 === x){ // 如果從onFullfilled中返回的x就是promise2,就會導致迴圈引用報錯
return reject(new TypeError('Chaining cycle'));
}
let called; // 宣告避免多次使用
// x型別判斷 如果是物件或者函式
if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
// 判斷是否是thenable物件
try{
let then = x.then;
if(typeof then === 'function'){
then.call(x,y=>{
if(called) return;
called = true;
resolvePromise(promise2,y,resolve,reject);
},err=>{
if(called) return;
called = true;
reject(err);
});
}else{
// 說明是普通物件/函式
resolve(x);
}
}catch(e){
if(called) return;
called = true;
reject(e);
}
}else{
resolve(x);
}
}
複製程式碼
以上基本實現了Promise的基本方法,根據Promise的用法,補充一些類上的方法
// 用於promise方法鏈時 捕獲前面onFulfilled/onRejected丟擲的異常
Promise.reject = function(reason){
return new Promise((resolve,reject)=>{
reject(reason);
})
}
Promise.resolve = function(value){
return new Promise((resolve,reject)=>{
resolve(value);
})
}
Promise.prototype.catch = function(onRejected){
// 預設不寫成功
return this.then(null,onRejected);
};
/**
* Promise.all Promise進行並行處理
* 引數: promise物件組成的陣列作為引數
* 返回值: 返回一個Promise例項
* 當這個陣列裡的所有promise物件全部變為resolve狀態的時候,才會resolve。
*/
Promise.all = function(promises){
return new Promise((resolve,reject)=>{
let arr = [];
let i = 0;
function processData(index,data){
arr[index] = data;
if(++i == promises.length){
resolve(arr);
}
}
for(let i = 0;i<promises.length;i++){
promises[i].then(data=>{ // data是成功的結果
processData(i,data);
},reject);
}
})
}
/**
* Promise.race
* 引數: 接收 promise物件組成的陣列作為引數
* 返回值: 返回一個Promise例項
* 只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理(取決於哪一個更快)
*/
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i = 0;i<promises.length;i++){
promises[i].then(resolve,reject);
}
})
}
複製程式碼
最後我們匯出方法
module.exports = Promise;
複製程式碼
至此一個符合PromiseA+規範的自己寫的原始碼庫完成了,可以測試使用這個庫替代Promise,以測試是否有邏輯錯誤等,或者可以使用
npm install promises-aplus-tests -g
promises-aplus-test 檔名
複製程式碼
外掛來測試該原始碼是否符合PromiseA+規範
希望這篇文章能幫到你,以上