async 的本質是一個流程控制。其實在非同步程式設計中,還有一個更為經典的模型,叫做 Promise/Deferred 模型(當然還有更多相關解決方法,比如 eventproxy,co 等,到時候遇到在挖坑)
首先,我們思考一個典型的非同步程式設計模型,考慮這樣一個題目:讀取一個檔案,在控制檯輸出這個檔案內容
var fs = require('fs');
fs.readFile('1.txt', 'utf8', function (err, data) {
console.log(data);
});
複製程式碼
看起來很簡單,再進一步: 讀取兩個檔案,在控制檯輸出這兩個檔案內容
var fs = require('fs');
fs.readFile('1.txt', 'utf8', function (err, data) {
console.log(data);
fs.readFile('2.txt', 'utf8', function (err, data) {
console.log(data);
});
});//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼
要是讀取更多的檔案呢
var fs = require('fs');
fs.readFile('1.txt', 'utf8', function (err, data) {
fs.readFile('2.txt', 'utf8', function (err, data) {
fs.readFile('3.txt', 'utf8', function (err, data) {
fs.readFile('4.txt', 'utf8', function (err, data) {
// ...
});
});//歡迎加入全棧開發交流圈一起學習交流:864305860
});
});
複製程式碼
這就是傳說中的 callback hell,可以使用 async 來改善這段程式碼,但是在本例中我們要用 promise/defer 來改善它 promise 基本概念 首先它是一個物件,它和 javascript 普通的物件沒什麼區別,同時,它也是一種規範,跟非同步操作約定了統一的介面,表示一個非同步操作的最終結果,以同步的方式來寫程式碼,執行的操作是非同步的,但又保證程式執行的順序是同步的
-
- promise 只有三種狀態,未完成,完成 (fulfilled) 和失敗 (rejected)
-
- promise 的狀態可以由未完成轉換成完成,或者未完成轉換成失敗
-
- promise 的狀態轉換隻發生一次
promise 有一個 then 方法,then 方法可以接受 3 個函式作為引數。前兩個函式對應 promise 的兩種狀態 fulfilled, rejected 的回撥函式。第三個函式用於處理進度資訊 為了理解它,一些重要原理必須記牢:.then() 總是返回一個新的 promise,如下面程式碼:
var promise = readFile()
var promise2 = promise.then(readAnotherFile, console.error)
複製程式碼
這裡 then 的引數 readAnotherFile, console.error 是代表非同步操作成功後的動作 onFulfilled 或失敗後的動作 OnRejected,也就是說,讀取檔案成功後執行 readAnotherFile 函式,否則失敗列印記錄錯誤。這種實現是兩個中只有一種可能 也可以理解為:
promiseSomething().then(function (fulfilled) {
// 當 promise 狀態變成 fulfilled 時,呼叫此函式
}, function (rejected) {
// 當 promise 狀態變成 rejected 時,呼叫此函式
}, function (progress) {
// 當返回進度資訊時,呼叫此函式
});//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼
Promise 法則有兩部分必須分離:
-
- then() 總是返回一個新的 promise,每次你呼叫它,它不管回撥做什麼,因為 .then() 在回撥被呼叫之前已經給了你一個承諾 promise,回撥的行為隻影響承諾 promise 的實施,如果回撥返回一個值,那麼 promise 將使用那個值,如果這個值是一個 promise,返回這個 promise 實施後的值給這個值,如果回撥丟擲錯誤,promise 將拒絕錯誤
-
- 被 .then() 返回的 promise 是一個新的 promise ,它不同於那些 .then() 被呼叫的 promise,promise 長長的鏈條有時會好些隱藏這個事實,不管如何,每次 .then() 呼叫都會產生一個新的 promise,這裡必須注意的是你真正需要考慮的是你最後呼叫 .then() 可能代表失敗,那麼如果你不捕獲這種失敗,那麼容易導致你的錯誤 exception 消失
來看一個利用 q 來處理這種問題的簡單例子:
var Q = require('q');
var defer = Q.defer();
/**
* 獲取初始 promise
* @private
*/
function getInitialPromise() {
return defer.promise;
}//歡迎加入全棧開發交流圈一起學習交流:864305860
/**
* 為 promise 設定三種狀態的回撥函式
*/
getInitialPromise().then(function (success) {
console.log(success);
}, function (error) {
console.log(error);
}, function (progress) {
console.log(progress);
});
defer.notify('in progress'); // 控制檯列印 in progress
defer.resolve('resolve'); // 控制檯列印 resolve
defer.reject('reject'); // 沒有輸出。promise 的狀態只能改變一次
複製程式碼
promise 的傳遞 then 方法會返回一個 promise,在下面這個例子中,我們用 outputPromise 指向 then 返回的 promise。
var outputPromise = getInputPromise().then(function (fulfilled) {
}, function (rejected) {
});
複製程式碼
現在 outputPromise 就變成了受 function(fulfilled) 或者 function(rejected) 控制狀態的 promise 了。直白的意思就是:當 function(fulfilled) 或者 function(rejected) 返回一個值,比如一個字串,陣列,物件等等,那麼 outputPromise 的狀態就會變成 fulfilled。 在下面這個例子中,我們可以看到,當我們把 inputPromise 的狀態通過 defer.resovle() 變成 fulfilled 時,控制檯輸出 fulfilled. 當我們把 inputPromise 的狀態通過 defer.reject() 變成 rejected,控制檯輸出 rejected
var Q = require('q');
var defer = Q.defer();
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
* 通過 defer 獲得 promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)
* 當 inputPromise 狀態由未完成變成 rejected 時,呼叫 function(rejected)
* 將 then 返回的 promise 賦給 outputPromise
* function(fulfilled) 和 function(rejected) 通過返回字串將 outputPromise 的狀態由
* 未完成改變為 fulfilled
* @private
*///歡迎加入全棧開發交流圈一起學習交流:864305860
var outputPromise = getInputPromise().then(function (fulfilled) {
return 'fulfilled';
}, function (rejected) {
return 'rejected';
});
/**
* 當 outputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled),控制檯列印 'fulfilled: fulfilled'。
* 當 outputPromise 狀態由未完成變成 rejected, 呼叫 function(rejected), 控制檯列印 'rejected: rejected'。
*/
outputPromise.then(function (fulfilled) {
console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
console.log('rejected: ' + rejected);
});
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
* 將 inputPromise 的狀態由未完成變成 rejected
*/
defer.reject(); // 輸出 fulfilled: rejected
/**
* 將 inputPromise 的狀態由未完成變成 fulfilled
*/
//defer.resolve(); // 輸出 fulfilled: fulfilled
複製程式碼
當 function(fulfilled) 或者 function(rejected) 丟擲異常時,那麼 outputPromise 的狀態就會變成 rejected
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過 defer 獲得 promise
* @private
*///歡迎加入全棧開發交流圈一起學習交流:864305860
function getInputPromise() {
return defer.promise;
}
/**
* 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)
* 當 inputPromise 狀態由未完成變成 rejected 時,呼叫 function(rejected)
* 將 then 返回的 promise 賦給 outputPromise
* function(fulfilled) 和 function(rejected) 通過丟擲異常將 outputPromise 的狀態由
* 未完成改變為 reject
* @private
*/
var outputPromise = getInputPromise().then(function (fulfilled) {
throw new Error('fulfilled');
}, function (rejected) {
throw new Error('rejected');
});
/**
* 當 outputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)。
* 當 outputPromise 狀態由未完成變成 rejected, 呼叫 function(rejected)。
*/
outputPromise.then(function (fulfilled) {
console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
console.log('rejected: ' + rejected);
});
/**
* 將 inputPromise 的狀態由未完成變成 rejected
*/
defer.reject(); // 控制檯列印 rejected [Error:rejected]
//歡迎加入全棧開發交流圈一起學習交流:864305860
/**
* 將 inputPromise 的狀態由未完成變成 fulfilled
*/
//defer.resolve(); // 控制檯列印 rejected [Error:fulfilled]
複製程式碼
當 function(fulfilled) 或者 function(rejected) 返回一個 promise 時,outputPromise 就會成為這個新的 promise. 這樣做的意義在於聚合結果 (Q.all),管理延時,異常恢復等等 比如說我們想要讀取一個檔案的內容,然後把這些內容列印出來。可能會寫出這樣的程式碼:
// 錯誤的寫法
var outputPromise = getInputPromise().then(function (fulfilled) {
fs.readFile('test.txt', 'utf8', function (err, data) {
return data;
});//歡迎加入全棧開發交流圈一起學習交流:864305860
});
複製程式碼
然而這樣寫是錯誤的,因為 function(fulfilled) 並沒有返回任何值。需要下面的方式:
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過 defer 獲得promise
* @private
*///歡迎加入全棧開發交流圈一起學習交流:864305860
function getInputPromise() {
return defer.promise;
}
/**
* 當 inputPromise 狀態由未完成變成 fulfil時,呼叫 function(fulfilled)
* 當 inputPromise 狀態由未完成變成 rejected時,呼叫 function(rejected)
* 將 then 返回的 promise 賦給 outputPromise
* function(fulfilled) 將新的 promise 賦給 outputPromise
* 未完成改變為 reject
* @private
*///歡迎加入全棧開發交流圈一起學習交流:864305860
var outputPromise = getInputPromise().then(function (fulfilled) {
var myDefer = Q.defer();
fs.readFile('test.txt', 'utf8', function (err, data) {
if (!err && data) {
myDefer.resolve(data);
}
});//歡迎加入全棧開發交流圈一起學習交流:864305860
return myDefer.promise;
}, function (rejected) {
throw new Error('rejected');
});
/**
* 當 outputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled),控制檯列印 test.txt 檔案內容。
*
*///歡迎加入全棧開發交流圈一起學習交流:864305860
outputPromise.then(function (fulfilled) {
console.log(fulfilled);
}, function (rejected) {
console.log(rejected);
});
/**
* 將 inputPromise 的狀態由未完成變成 rejected
*/
//defer.reject();
/**
* 將 inputPromise 的狀態由未完成變成 fulfilled
*/
defer.resolve(); // 控制檯列印出 test.txt 的內容
複製程式碼
方法傳遞 方法傳遞有些類似於 Java 中的 try 和 catch。當一個異常沒有響應的捕獲時,這個異常會接著往下傳遞 方法傳遞的含義是當一個狀態沒有響應的回撥函式,就會沿著 then 往下找 沒有提供 function(rejected)
var outputPromise = getInputPromise().then(function (fulfilled) { })
複製程式碼
如果 inputPromise 的狀態由未完成變成 rejected, 此時對 rejected 的處理會由 outputPromise 來完成
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*///歡迎加入全棧開發交流圈一起學習交流:864305860
function getInputPromise() {
return defer.promise;
}
/**
* 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 function(fulfilled)
* 當 inputPromise 狀態由未完成變成 rejected 時,這個 rejected 會傳向 outputPromise
*/
var outputPromise = getInputPromise().then(function (fulfilled) {
return 'fulfilled'
});//歡迎加入全棧開發交流圈一起學習交流:864305860
outputPromise.then(function (fulfilled) {
console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
console.log('rejected: ' + rejected);
});
/**
* 將 inputPromise 的狀態由未完成變成 rejected
*/
defer.reject('inputpromise rejected'); // 控制檯列印 rejected: inputpromise rejected
/**
* 將 inputPromise的狀態由未完成變成fulfilled
*///歡迎加入全棧開發交流圈一起學習交流:864305860
//defer.resolve();
複製程式碼
沒有提供 function(fulfilled)
var outputPromise = getInputPromise().then(null, function (rejected) { })
複製程式碼
如果 inputPromise 的狀態由未完成變成 fulfilled, 此時對 fulfil 的處理會由 outputPromise 來完成
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}//歡迎加入全棧開發交流圈一起學習交流:864305860
/**
* 當 inputPromise 狀態由未完成變成 fulfil時,傳遞給 outputPromise
* 當 inputPromise 狀態由未完成變成 rejected時,呼叫 function(rejected)
* function(fulfilled) 將新的 promise 賦給 outputPromise
* 未完成改變為 reject
* @private
*/
var outputPromise = getInputPromise().then(null, function (rejected) {
return 'rejected';
}); //歡迎加入全棧開發交流圈一起學習交流:864305860
outputPromise.then(function (fulfilled) {
console.log('fulfilled: ' + fulfilled);
}, function (rejected) {
console.log('rejected: ' + rejected);
});
/**
* 將 inputPromise 的狀態由未完成變成 rejected
*/
// defer.reject('inputpromise rejected');
/**
* 將 inputPromise 的狀態由未完成變成fulfilled
*/
defer.resolve('inputpromise fulfilled'); // 控制檯列印fulfilled: inputpromise fulfilled
複製程式碼
可以使用 fail(function(error)) 來專門針對錯誤處理,而不是使用 then(null,function(error))
var outputPromise = getInputPromise().fail(function (error) { })
複製程式碼
看這個例子:
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當 inputPromise 狀態由未完成變成 fulfil 時,呼叫 then(function(fulfilled))
* 當 inputPromise 狀態由未完成變成 rejected 時,呼叫 fail(function(error))
* function(fulfilled) 將新的 promise 賦給 outputPromise
* 未完成改變為reject
* @private
*/
var outputPromise = getInputPromise().then(function (fulfilled) {
return fulfilled;
}).fail(function (error) {
console.log('fail: ' + error);
});//歡迎加入全棧開發交流圈一起學習交流:864305860
/**
* 將 inputPromise 的狀態由未完成變成 rejected
*/
defer.reject('inputpromise rejected');// 控制檯列印 fail: inputpromise rejected
/**
* 將 inputPromise 的狀態由未完成變成 fulfilled
*/
//defer.resolve('inputpromise fulfilled');
複製程式碼
可以使用 progress(function (progress)) 來專門針對進度資訊進行處理,而不是使用 then(function (success) { }, function (error) { }, function (progress) { })
var Q = require('q');
var defer = Q.defer();
/**
* 獲取初始 promise
* @private
*/
function getInitialPromise() {
return defer.promise;
}
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
* 為 promise 設定 progress 資訊處理函式
*/
var outputPromise = getInitialPromise().then(function (success) {
}).progress(function (progress) {
console.log(progress);
});
defer.notify(1);
defer.notify(2); // 控制檯列印 1,2
複製程式碼
promise 鏈 promise 鏈提供了一種讓函式順序執行的方法 函式順序執行是很重要的一個功能。比如知道使用者名稱,需要根據使用者名稱從資料庫中找到相應的使用者,然後將使用者資訊傳給下一個函式進行處理
var Q = require('q');
var defer = Q.defer();
// 一個模擬資料庫
var users = [{ 'name': 'andrew', 'passwd': 'password' }];
function getUsername() {
return defer.promise;
}
function getUser(username) {
var user;
users.forEach(function (element) {
if (element.name === username) {
user = element;
}
});//歡迎加入全棧開發交流圈一起學習交流:864305860
return user;
}
// promise 鏈
getUsername().then(function (username) {
return getUser(username);
}).then(function (user) {
console.log(user);
});
defer.resolve('andrew');
複製程式碼
我們通過兩個 then 達到讓函式順序執行的目的。 then 的數量其實是沒有限制的。當然,then 的數量過多,要手動把他們連結起來是很麻煩的。比如
foo(initialVal).then(bar).then(baz).then(qux)
複製程式碼
這時我們需要用程式碼來動態製造 promise 鏈
var funcs = [foo, bar, baz, qux]
var result = Q(initialVal)
funcs.forEach(function (func) {
result = result.then(func)
})
return result
複製程式碼
當然,我們可以再簡潔一點
var funcs = [foo, bar, baz, qux]
funcs.reduce(function (pre, current),Q(initialVal){
return pre.then(current)
})//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼
看一個具體的例子
function foo(result) {
console.log(result);
return result + result;
}
// 手動連結
Q('hello').then(foo).then(foo).then(foo);
// 控制檯輸出: hello
// hellohello
// hellohellohello
// 動態連結
var funcs = [foo, foo, foo];
var result = Q('hello');
funcs.forEach(function (func) {
result = result.then(func);
});
//歡迎加入全棧開發交流圈一起學習交流:864305860
// 精簡後的動態連結
funcs.reduce(function (prev, current) {
return prev.then(current);
}, Q('hello'));
複製程式碼
對於 promise 鏈,最重要的是需要理解為什麼這個鏈能夠順序執行。如果能夠理解這點,那麼以後自己寫 promise 鏈可以說是輕車熟路啊 promise 組合 回到我們一開始讀取檔案內容的例子。如果現在讓我們把它改寫成 promise 鏈,是不是很簡單呢?
var Q = require('q'),
fs = require('fs');
function printFileContent(fileName) {
return function () {
var defer = Q.defer();
fs.readFile(fileName, 'utf8', function (err, data) {
if (!err && data) {
console.log(data);
defer.resolve();
}
})
return defer.promise;
}
} //歡迎加入全棧開發交流圈一起學習交流:864305860
// 手動連結
printFileContent('sample01.txt')()
.then(printFileContent('sample02.txt'))
.then(printFileContent('sample03.txt'))
.then(printFileContent('sample04.txt')); // 控制檯順序列印 sample01 到 sample04 的內容
複製程式碼
我們會發現為什麼要他們順序執行呢,如果他們能夠並行執行不是更好嗎? 我們只需要在他們都執行完成之後,得到他們的執行結果就可以了 我們可以通過 Q.all([promise1,promise2...]) 將多個 promise 組合成一個 promise 返回。 注意:
-
- 當 all 裡面所有的 promise 都 fulfil 時,Q.all 返回的 promise 狀態變成 fulfil
-
- 當任意一個 promise 被 reject 時,Q.all 返回的 promise 狀態立即變成 reject
我們來把上面讀取檔案內容的例子改成並行執行吧
var Q = require('q');
var fs = require('fs');
/**//歡迎加入全棧開發交流圈一起學習交流:864305860
*讀取檔案內容
*@private
*/
function printFileContent(fileName) {
// Todo: 這段程式碼不夠簡潔。可以使用 Q.denodeify 來簡化
var defer = Q.defer();
fs.readFile(fileName, 'utf8', function (err, data) {
if (!err && data) {
console.log(data);
defer.resolve(fileName + ' success ');
} else {
defer.reject(fileName + ' fail ');
}
}) //歡迎加入全棧開發交流圈一起學習交流:864305860
return defer.promise;
}
Q.all([printFileContent('sample01.txt'), printFileContent('sample02.txt'), printFileContent('sample03.txt'), printFileContent('sample04.txt')])
.then(function (success) {
console.log(success);
}); // 控制檯列印各個檔案內容 順序不一定
複製程式碼
現在知道 Q.all 會在任意一個 promise 進入 reject 狀態後立即進入 reject 狀態。如果我們需要等到所有的 promise 都發生狀態後(有的 fulfil, 有的 reject),再轉換 Q.all 的狀態, 這時我們可以使用 Q.allSettled
var Q = require('q'),
fs = require('fs');
/**
*讀取檔案內容
*@private
*/
function printFileContent(fileName) {
// Todo: 這段程式碼不夠簡潔。可以使用Q.denodeify來簡化
var defer = Q.defer();
fs.readFile(fileName, 'utf8', function (err, data) {
if (!err && data) {
console.log(data);
defer.resolve(fileName + ' success ');
} else {
defer.reject(fileName + ' fail ');
}//歡迎加入全棧開發交流圈一起學習交流:864305860
})
return defer.promise;
}
Q.allSettled([printFileContent('nosuchfile.txt'),
printFileContent('sample02.txt'),
printFileContent('sample03.txt'),
printFileContent('sample04.txt')])
.then(function (results) {
results.forEach(
function (result) {
console.log(result.state);
}
);
});//歡迎加入全棧開發交流圈一起學習交流:864305860
複製程式碼
結束 promise 鏈 通常,對於一個 promise 鏈,有兩種結束的方式。第一種方式是返回最後一個 promise 如 return foo().then(bar); 第二種方式就是通過 done 來結束 promise 鏈 如 foo().then(bar).done() 為什麼需要通過 done 來結束一個 promise 鏈呢? 如果在我們的鏈中有錯誤沒有被處理,那麼在一個正確結束的 promise 鏈中,這個沒被處理的錯誤會通過異常丟擲
var Q = require('q');
function getPromise(msg, timeout, opt) {
var defer = Q.defer();
setTimeout(function () {
console.log(msg);
if (opt)
defer.reject(msg);
else
defer.resolve(msg);
}, timeout);
return defer.promise;
} //歡迎加入全棧開發交流圈一起學習交流:864305860
/**
* 沒有用 done() 結束的 promise 鏈
* 由於 getPromse('2',2000,'opt') 返回 rejected, getPromise('3',1000) 就沒有執行
* 然後這個異常並沒有任何提醒,是一個潛在的 bug
*/
getPromise('1', 3000)
.then(function () { return getPromise('2', 2000, 'opt') })
.then(function () { return getPromise('3', 1000) });
/**
* 用 done() 結束的 promise 鏈
* 有異常丟擲
*/
getPromise('1', 3000)
.then(function () { return getPromise('2', 2000, 'opt') })
.then(function () { return getPromise('3', 1000) })
.done();
複製程式碼
promise 代表一個非同步操作的最終結果。主要通過 promise 的 then 方法訂閱其最終結果的處理回撥函式,和訂閱因某原因無法成功獲取最終結果的處理回撥函式。
A 與 A+ 的不同點
- A+ 規範通過術語 thenable 來區分 promise 物件
- A+ 定義 onFulfilled/onRejectd 必須是作為函式來呼叫,而且呼叫過程必須是非同步的
- A+ 嚴格定義了 then 方法鏈式呼叫時,onFulfilled/onRejectd 的呼叫順序
結語
感謝您的觀看,如有不足之處,歡迎批評指正。
本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。 最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。