餘爲前端菜鳥,感姿式水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣爲引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。前端
本系列代碼及文檔均在 此處node
繼續啃老本...讓人又愛又恨的異步c++
同步和異步git
function sync(){
const doA = '12'
const doB = '34'
}
function async(){
ajax('/api/doC1', (res) => {
doC2(res)
})
}
複製代碼
同步很好理解,任務一個個執行,doA之後才能doB。github
異步任務能夠理解爲分兩個階段,doC的前一階段是發出請求,後一階段是在請求結束後的將來時刻處理。web
二者各有優劣,同步任務會致使阻塞,異步任務須要由有機制實現先後兩部分的分離,使得主線程可以在這間歇內繼續工做而不浪費時間等待。ajax
以瀏覽器爲例大體過程:shell
主線程調用web api,經過工做線程發起請求,而後主線程繼續處理別的任務(這是part1)。工做線程執行完了異步任務之後往事件隊列裏註冊回調,等待主線程空閒後去隊列中取出到主線程執行棧中執行(這是part2)。編程
併發和並行api
簡單描述:併發是交替作不一樣事情,並行是同時作不一樣事情。
咱們能夠經過多線程去處理併發,但說到底CPU只是在快速切換上下文來實現快速的處理。而並行則是利用多核,同時處理多個任務。
單線程和多線程
咱們總說js是單線程的,node是單線程的,其實這樣的說法並不完美。所謂單線程指的是js引擎解釋和執行js代碼的線程是一個,也便是咱們常說的主線程。
又好比對於咱們熟悉的node,I/O操做實際上都是經過線程池來完成的,js->調用c++函數->libuv方法->I/O操做執行->完畢後js線程繼續執行後續。
ajax('/a', (res) => {
ajax('/b, (res) => {
// ...
})
})
複製代碼
醜陋的callback形式,再也不多說
Promise
誕於社區,初爲異步編程之解決方案,後有ES6將其寫入語言標準,終成今人所言之 Promise
對象// 接收以resolve和reject方法爲參數的函數
const pr = new Promise((resolve, reject) => {
// do sth
resolve(1) // pending -> resolved
reject(new Error()) // pending -> rejected
})
複製代碼
pr.then((value) => {
// onresolved cb
}, (err) => {
// onrejected cb
})
複製代碼
Promise.prototype.then
採用鏈式寫法,返回一個新的Promise,上一個回調的返回做爲參數傳遞到下一個回調
Promise.prototype.catch
其實是.then(null, rejection)
的別名
一樣支持鏈式寫法,最後一個catch能夠catch到前面任一個Promise跑拋出的未catch的error
Promise.all
參數需具備Iterator接口,返回爲多個Promise實例
var p = Promise.all([p1, p2, p3]);
複製代碼
p1, p2, p3均resolve後p才resolve,任一個reject則p就reject。
若內部有catch,則外部catch捕獲不到異常。
Promise.race
// 若5秒未返回則拋錯
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));
複製代碼
第一個狀態改變的Promise會引發p狀態改變。
Promise.resolve/reject
Promise.resolve('1')
Promise.resolve({ then: function() {
console.log(123)
} })
複製代碼
Promise.prototype.finally
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
複製代碼
不管如何都會執行最後的cb
Promise爲咱們提供了優於callback嵌套的異步選擇,但實際上仍是基於回調來實現的。
簡單的Promise實現代碼能夠看這裏 github
基本概念
function * gen() {
const a = yield 1;
return 2
}
const m = gen() // gen{<suspended>}
m.next() // {value: 1, done: false}
m.next() // {value: 2, done: true}
m.next() // {value: undefined, done: true}
m // gen {<closed>}
複製代碼
{ value, done }
對象。value
屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值,done
屬性是一個布爾值,表示是否遍歷結束{ value: undefined, done: true }
,Generator的內部屬性[[GeneratorStatus]]
變爲closed狀態yield
Generator.prototype.next()
經過傳入參數爲Generator函數內部注入不一樣的值來調整函數接下來的行爲
// 這裏利用參數實現了重置
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
// 傳遞的參數會被賦值給i(yield後的表達式的值(i))
// 而後執行var reset = i賦值給reset
g.next(true) // { value: 0, done: false }
複製代碼
Generator.prototype.throw()
Generator.prototype.return()
try ... finally
存在時,return會在finally執行完後執行,最後的返回結果是return方法的參數,這以後Generator運行結束,下次訪問會獲得{value: undefined, done: true}
try ... finally
不存在時,直接執行return,後續和上一條一致以上三種方法都是讓Generator恢復執行,並用語句替換yield表達式
yield*
在一個Generator內部直接調用另外一個Generator是沒用的,若是須要在一個Generator內部yield另外一個Generator對象的成員,則須要使用yield*
function* inner() {
yield 'a'
// yield outer() // 返回一個遍歷器對象
yield* outer() // 返回一個遍歷器對象的內部值
yield 'd'
}
function* outer() {
yield 'b'
yield 'c'
}
let s = inner()
for (let i of s) {
console.log(i)
} // a b c d
複製代碼
yield*
後跟一個遍歷器對象(全部實現了iterator的數據結構實際上均可以被yield*
遍歷)
被代理的Generator函數若是有return,return的值會被for...of忽略,因此next不會返回,可是實際上能夠向外部Generetor內部返回一個值,以下:
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
複製代碼
舉個🌰
// 處理嵌套數組
function* Tree(tree){
if(Array.isArray(tree)){
for(let i=0;i<tree.length;i++) {
yield* Tree(tree[i])
}
} else {
yield tree
}
}
let ss = [[1,2],[3,4,5],6,[7]]
for (let i of Tree(ss)) {
console.log(i)
} // 1 2 3 4 5 6 7
// 理解for ...of 其實是一個while循環
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
複製代碼
做爲對象的屬性的Generator函數
寫法很清奇
let obj = {
* sss() {
// ...
}
}
let obj = ={
sss: function* () {
// ...
}
}
複製代碼
Generator函數的this
Generator函數返回的是遍歷器對象,會繼承prototype的方法,可是因爲返回的不是this,因此會出現:
function* ss () {
this.a = 1
}
let f = ss()
f.a // undefined
複製代碼
想要在內部的this綁定遍歷器對象?
function * ss() {
this.a = 1
yield this.b = 2;
yield this.c = 3;
}
let f = ss.call(ss.prototype)
// f.__proto__ === ss.prototype
f.next()
f.next()
f.a // 1
f.b // 2
f.c // 3
複製代碼
舉個🌰
// 利用暫停狀態的特性
let clock = function* () {
while(true) {
console.log('tick')
yield
console.log('tock')
yield
}
}
複製代碼
異步操做的同步化表達
// Generator函數
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
// ajax請求函數,回調函數中要將response傳給next方法
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
// 須要第一次執行next方法,返回yield後的表達式,觸發異步請求,跳到request函數中執行
var it = main();
it.next();
複製代碼
控制流管理
// 同步steps
let steps = [step1Func, step2Func, step3Func];
function *iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
// 異步後續討論
複製代碼
TO BE CONTINUED
回到最初提到的異步:將異步任務看作兩個階段,第一階段如今執行,第二階段在將來執行,這裏就須要將任務 暫停
。而前面說到的Generator彷佛剛好提供了這麼一個當口,暫停
結束後第二階段開啓不就對應下一個next調用嘛!
想像我有一個異步操做,我能夠經過Generator的next方法傳入操做須要的參數,第二階段執行完後返回值的value又能夠向外輸出,maybe Generator真的能夠做爲異步操做的容器?
協程A執行->協程A暫停,執行權轉交給協程B->一段時間後執行權交還A->A恢復執行
// yield是異步兩個階段的分割線
function* asyncJob() {
// ...其餘代碼
var f = yield readFile(fileA);
// ...其餘代碼
}
複製代碼
參數的求值策略
js中的Thunk函數
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
複製代碼
看起來只是換了個樣子,好像並無什麼用Generator看起來很美妙,可是next調用方式看起來很麻煩,如何實現自執行呢?
Generator函數自動執行
function* gen() {
yield a // 表達式a
yield 2
}
let g = gen()
let res = g.next()
while(!res.done) {
console.log(res.value)
res = g.next() // 表達式b
}
複製代碼
可是,這不適合異步操做。若是必須保證前一步執行完,才能執行後一步,上面的自動執行就不可行。
next方法是同步的,執行時必須馬上返回值,yield後是同步操做固然沒問題,是異步操做時就不能夠了。處理方式就是返回一個Thunk函數或者Promise對象。此時value值爲該函數/對象,done值仍是按規矩辦事。
var g = gen();
var r1 = g.next();
// 重複傳入一個回調函數
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
複製代碼
Thunk函數的自動流程管理
思路:
Generator函數中yield 異步Thunk函數,經過yield將控制權轉交給Thunk函數,而後在Thunk函數的回調函數中調用Generator的next方法,將控制權交回給Generator。此時,異步操做確保完成,開啓下一個任務。
Generator是一個異步操做的容器,實現自動執行須要一個機制,這個機制的關鍵是控制權的交替,在異步操做有告終果之後自動交回控制權,而回調函數執行正是這麼個時間點。
// Generator函數的執行器
function run(fn) {
let gen = fn()
// 傳給Thunk函數的回調函數
function cb(err, data) {
// 控制權交給Generator,獲取下一個yield表達式(異步任務)
let result = gen.next(data)
// 沒任務了,返回
if (result.done) return
// 控制權交給Thunk函數,傳入回調
result.value(cb)
}
cb()
}
// Generator函數
function* g() {
let f1 = yield readFileThunk('/a')
let f2 = yield readFileThunk('/b')
let f3 = yield readFileThunk('/c')
}
// Thunk函數readFileThunk
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
// 自動執行
run(g)
複製代碼
說明
基於Promise的執行器
function run(fn) {
let gen = fn()
function cb(data) {
// 將上一個任務返回的data做爲參數傳給next方法,控制權交回到Generator
// 這裏將result變量引用{value, done}對象
// 不要和Generator中的`let result = yield xxx`搞混
let result = gen.next(data)
if (result.done) return result.value
result.value.then(function(data){
// resolved以後會執行cb(data)
// 開啓下一次循環,實現自動執行
cb(data)
})
}
cb()
}
複製代碼
源碼分析
其實和上面的實現相似
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1) // 除第一個參數外的全部參數
// 返回一個Promise對象
return new Promise(function(resolve, reject) {
// 若是是Generator函數,執行獲取遍歷器對象gen
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// 第一次執行遍歷器對象gen的next方法獲取第一個任務
onFulfilled();
// 每次異步任務執行完,resolved之後會調用,控制權又交還給Generator
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 獲取{value,done}對象,控制權在這裏暫時交給異步任務,執行yield後的異步任務
} catch (e) {
return reject(e);
}
next(ret); // 進入next方法
}
// 同理可得
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
// 關鍵
function next(ret) {
// 遍歷執行完異步任務後,置爲resolved,並將最後value值返回
if (ret.done) return resolve(ret.value);
// 獲取下一個異步任務,並轉爲Promise對象
var value = toPromise.call(ctx, ret.value);
// 異步任務結束後會調用onFulfilled方法(在這裏爲yield後的異步任務設置then的回調參數)
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
})
}
複製代碼
其實仍是同樣,爲Promise對象then方法指定回調函數,在異步任務完成後觸發回調函數,在回調函數中執行Generator的next方法,進入下一個異步任務,實現自動執行。
舉個🌰
'use strict';
const fs = require('fs');
const co =require('co');
function read(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, 'utf8', function(err, res) {
if (err) {
return reject(err);
}
return resolve(res);
});
});
}
co(function *() {
return yield read('./a.js');
}).then(function(res){
console.log(res);
});
複製代碼
比較
function* asyncReadFile () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
複製代碼
看起來只是寫法的替換,實際上有這樣的區別
用法
返回的Promise
async命令及其後的Promise
相互獨立的異步任務能夠改造下讓其併發執行(Promise.all)
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
複製代碼
await 與 for ... of
應該還在提案階段吧
for await (const item of list) {
console.log(item)
}
複製代碼
async function logInOrder(urls) {
// 併發讀取遠程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序輸出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
複製代碼
目前瞭解到的異步解決方案大概就這樣,Promise是主流,Generator做爲容器,配合async await語法糖提供了看起來彷佛更加優雅的寫法,但實際上由於一切都是Promise,同步任務也會被包裝成異步任務執行,我的感受仍是有不足之處的。
雖發表於此,卻畢竟爲一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。