js異步編程
一般知道,js
腳步語言的執(zhí)行環(huán)境是單線程的痒谴,就是它會等一個任務(wù)完成,才會進行第二個任務(wù)铡羡,然后一直向下進行积蔚,這樣的執(zhí)行環(huán)境簡單,但是處理不了復(fù)雜的運用烦周,當(dāng)一個請求需要非常久的時候尽爆,下一個流程就會被擱淺,如果長時間得不到反饋读慎,進程就這樣的奔潰了漱贱。
為了解決這個硬性需求,Javascript語言提出了二種語言模式: 同步(Synchronous)和 異步 (Asynchronous)夭委。
異步的幾種常用方法
- 回調(diào)函數(shù)
- 訂閱和發(fā)布模式
- Promise
- generator
- async/await
回調(diào)函數(shù)方法
通過把一個函數(shù)(callback)作為參數(shù)傳入另一個函數(shù)幅狮,當(dāng)滿足一定條件的時候,就執(zhí)行callback函數(shù)株灸。
用法:
// 這里只是一個簡單的條件
function fn1(a, fn) {
if(a > 10 && fn instanceof Function) {
fn.call()
}
}
function fn2() {
console.log(' --- fn2 ----')
}
// 通過簡單的異步調(diào)用
function fn3(fn) {
setTimeout(() => {
console.log('--- fn3 ---')
fn.call()
},1000)
}
// 通過回調(diào)函數(shù)調(diào)用
fn1(12, fn2)
fn3(fn2)
通過回調(diào)函數(shù)的方式處理異步崇摄,是在異步早期的情況,其中jquery
中的很多都是通過callback來實現(xiàn)回調(diào)的慌烧。但是這種模式代碼編寫比較耦合逐抑,不利于代碼維護。
發(fā)布訂閱模式
pub/sub模式是js設(shè)計模式中的一種屹蚊,本身是借鑒于java的模式厕氨,但是在處理異步處理的時候非常有作用。通過一個信息中心EventCenter
來處理的監(jiān)聽(on
)和觸發(fā)(triggle
)汹粤∶可以參考樓主之前手寫的設(shè)計模式的文章最后一個設(shè)計模式。
function fn1() {
setTimeout(() => {
// 異步操作后得到數(shù)據(jù)data
let data = fetch(.....)
// 觸發(fā)信息中心的waterFull,并傳出data
Event.triggle('waterFull', data)
},2000)
}
fn1()
Event.on('waterFull', (data) => {
// 對得到的值進行進一步加工處理
console.log(data)
})
通過pub/sub模式玄括,我們可以在信息中心清楚的看到有多少信號來源冯丙,方便的集中管理,更加方便于模塊化的管理遭京,但是如果整個項目都使用pub/sub模式的話胃惜,流程就變得不太清晰了,數(shù)據(jù)的得到和數(shù)據(jù)的處理分開哪雕,對于后期的維護也是一個很大的問題船殉。
Promise
對于現(xiàn)在一個基本的前端人員,沒有說沒有聽過Promise
的斯嚎,如果你實在沒有看多promise利虫, 可以查看阮老師的es6文檔Promise挨厚。下面主要是通過具體的要求來實現(xiàn)promise,不會仔細(xì)的講解糠惫。
Promise
構(gòu)造函數(shù)成為承諾疫剃,它分為三種狀態(tài)resolve
, reject
, pending
,一旦狀態(tài)從pending
改為其它2個狀態(tài)之后,就不能修改了硼讽,就一個承諾一樣巢价。
Promise
接收2個參數(shù)resolve
, reject
,分別表示成功后執(zhí)行和失敗后執(zhí)行固阁,可以通過實例的then()
方法傳遞對于的函數(shù)壤躲。
// 返回一個Promise實例
const promise = new Promise((resolve, reject) => {
// some code 這里函數(shù)會立馬執(zhí)行
if(success) resolve(value)
else reject(err)
})
promise.then(/*成功*/(data) => { console.log(data) }).catch(/*失敗*/(err) => { console.log(err) })
這里看了之后,你可能會說备燃,這個和異步處理有什么聯(lián)系嗎碉克?你思考一下,當(dāng)一個異步操作后并齐,我們可以不去管它什么時候結(jié)束漏麦,什么時候出錯,就像一個人承諾了冀膝,我只需要按照他的承諾去當(dāng)這個事情已經(jīng)被處理好了唁奢,是不是方便很多,下面直接上手一個例子窝剖。
let promise = new Promise((resolve, reject) => {
let data = fetch('url') // 得到接口返回的數(shù)據(jù)
resolve(data)
})
promise.then(data => console.log(data));
// fetch自動返回一個promise
fetch('http://ons.me/tools/dropload/json.php?page=0&size=4').then(response => response.json()).then(data => console.log(data)) // 可以直接到控制臺看結(jié)果
//
我完全不用擔(dān)心它里面怎么實現(xiàn)了麻掸,反正它已經(jīng)承諾了會給我結(jié)果,我只需要通過then()
方法去接受赐纱,我需要得到的值就可以了脊奋。
Promise.resolve(value) value可以是三種值
- 單個值
- 一個
promsie
實例 - 一個
thenable
對象
Promise.resolve(value).then((value) => {})
處理一個請求依賴另一個請求的情況
如果一個請求的結(jié)果是下一個請求的參數(shù),如果我們使用原始的請求方法疙描,就是出現(xiàn)一個像右的箭頭的回調(diào)地獄诚隙。
一層層嵌套,非常的恐怖起胰,不利于維護久又。那么通過prmise怎么處理回調(diào)地獄呢?
function send(url) {
return new Promise((resolve, reject => {
ajax(data);
if('成功') resolve(data)
else reject
}))
}
send('url1').then(data => send('url2'))
.then(data => send('url3'))
.then(data => send('url4'))
.then(data => console.log(data)) //輸出最終的值
// 還有一個簡單的例子
Promise.resolve(1).then(val1 => val1+2).then(val2 => val2+3).then(val3 => console.log(val3)) //6
上面處理回調(diào)地獄是不是看著方便很多效五,代碼也簡單命令地消,依賴性也很強,后面我們會繼續(xù)通過async/await
繼續(xù)簡化畏妖。
處理多個請求并發(fā)的情況(不需要管服務(wù)器的返回順序)
Promise.all(arr)
接受一個promise實例的數(shù)組脉执,可以并發(fā)多個請求給服務(wù)器,但是并不能保證接受到的先后順序戒劫,這個取決于服務(wù)器的處理速度半夷。
// 現(xiàn)在有一個包含url的數(shù)組婆廊,需要并發(fā)請求給服務(wù)器 setPromise是一個包裝成promise的函數(shù),返回一個promsie實例
let urlArr = [url1, url2, url3]
Promise.all(urlArr.map(url => setPromise(url))).then(data => console.log(data))
// 會得到一個數(shù)組巫橄,包含了三個請求數(shù)據(jù)的數(shù)組淘邻。
處理多個請求并發(fā),并且需要保證返回數(shù)據(jù)的順序(運用場景比較少)
上面一個方法并不會保證請求返回的結(jié)果湘换,按照你發(fā)送的順序返回列荔,如果我想把完整的響應(yīng)的結(jié)果按照我
希望的順序返回給我,那應(yīng)該怎么辦呢枚尼?
let urlArr = [url1, url2, url3];
let totalData = []
// 遍歷一個數(shù)組,并對每一項都執(zhí)行對應(yīng)的函數(shù)砂吞,返回一個Promise.
urlArr.reduce((promise, url) => {
return promise.then(() => setPromise(url)).then(data => { totalData.push(data) })
}, Promise.resolve())
這樣署恍,會等待每一個請求完成后,并把得到的數(shù)據(jù)push
到totalData
中蜻直,就可以按照順序得到我們想要的值了盯质。當(dāng)然使用async/await
會更加的方便。之后我們會講解概而。
generator構(gòu)造器
generator是一個構(gòu)造器呼巷,generator
函數(shù)執(zhí)行并不會執(zhí)行函數(shù)體內(nèi)部部分,而是返回一個構(gòu)造器對象赎瑰,通過構(gòu)造器對象的next()
方法調(diào)用函數(shù)主體王悍,并且每當(dāng)遇到yield
都會暫停執(zhí)行,并返回一個對象餐曼。
function* gen() {
console.log(`---- start ---`)
yield 1
yield 2
return 3
}
let g = gen() // 這里執(zhí)行了generator函數(shù)压储,但是并沒有執(zhí)行下面
g.next() // console---- start --- return { value: 1; done: false }
g.next() // {value: 2; done : false}
g.next() // {value: 3; done: true}
g.next() // {value: undefined; done: true}
注意yield
本身是不會反悔內(nèi)容的,只是給構(gòu)造器對象返回了內(nèi)容源譬,如果想yield
表達(dá)式也返回內(nèi)容集惋,可以通過給下一個next()
傳遞參數(shù)。
function* gen() {
let a = yield 1
console.log(a)
yield 2
return 3
}
let g = gen();
// 這里先執(zhí)行yield 1 然后暫停函數(shù)
g.next() // {value: 1, done: false}
// 繼續(xù)執(zhí)行賦值表達(dá)式踩娘,并yield 1得到的值為 ggg
g.next('ggg') // console ggg {value: 2, done: false}
通過next()
傳遞參數(shù)刮刑,我們可以做到值向內(nèi)部傳遞,對于后面的異步處理很有幫助养渴。
generator異步運用
利用構(gòu)造器的暫停和繼續(xù)的功能雷绢,我們可以很好的處理異步請求,得到數(shù)據(jù)后再進行其他內(nèi)容厚脉。主要是運用yield
表達(dá)式返回一個promise
對象的原理习寸。
function* send() {
let data = yield fetch('https://suggest.taobao.com/sug?code=utf-8&q=%E6%89%8B%E6%9C%BA');
}
let objData;
// 調(diào)用
send().next().value.then( response => response.json()).then(data => objData = data)
這樣我們就得到了接口請求的數(shù)據(jù),相比于之前的promise函數(shù)的書寫是不是要簡單很多傻工。和同步是一樣的操作霞溪。
如果我們想內(nèi)部對得到的數(shù)據(jù)進行進一步的處理呢孵滞?
// 這里可以像寫同步代碼的一樣,除掉這個yield關(guān)鍵字
function* send() {
let data = yield fetch('https://suggest.taobao.com/sug?code=utf-8&q=%E6%89%8B%E6%9C%BA');
data.result.map(item => {
return item.push(11)
})
return data
}
let objData;
let gen = send()
// 調(diào)用 和promise一樣的調(diào)用鸯匹。
gen.next().value.then( response => response.json()).then(data => gen.next(data)).then(data => objData=data)
// 多個請求
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// 首先手動執(zhí)行
const g = gen()
g.next().value.then(data => {
// 將第一個接口的值傳入
g.next(data).value.then(data => {
// 將第二個接口的值傳入
g.next(data);
})
})
簡單的co模塊處理generator多個函數(shù)請求
從上面我的調(diào)用方法就可以看出坊饶,利用Promise + generator
的異步處理不斷地通過then()
方法處理數(shù)據(jù)。有沒有一個方式是我可以直接運行一個函數(shù)殴蓬,然后就可以得到我想要的值匿级。 例如:
function* send() {
let data = yield fetch('https://suggest.taobao.com/sug?code=utf-8&q=%E6%89%8B%E6%9C%BA');
return data
}
run(send) // 這樣調(diào)用就可以直接返回一個data數(shù)據(jù)
// TODO
function run(gen) {
const g = gen();
function next(data) {
let result = g.next(data);
// 如果執(zhí)行完了,就直接返回value
if(result.done) return result.value
result.value.then(data => {
// 回調(diào)執(zhí)行
next(data)
})
}
next()
}
網(wǎng)上已經(jīng)封裝了很多的方法染厅,例如常見的run
庫痘绎,co函數(shù)就是來處理這樣的處理方式。但是當(dāng)我們發(fā)送多個請求的時候肖粮,可能你會這樣寫:
function* send() {
var p1 =yield request( "http://some.url.1" );
var p2 =yield request( "http://some.url.2" );
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log(r3)
}
// 運行已經(jīng)實現(xiàn)好的run函數(shù)
run(send)
這樣寫是會發(fā)送請求孤页,但是并不是并發(fā)多個請求,而是等第一個請求p1之后涩馆,再進行第二個請求p2行施,在性能優(yōu)化方面是不利的,也不符合我們的要求魂那,怎么做到2個請求是獨立的蛾号,并且我們還可以通過得到2個請求的結(jié)果后,進行其他請求涯雅∠式幔或許我們可以這樣:
function* send() {
// 先并發(fā)進行請求
var p1 = request( "http://some.url.1" );
var p2 = request( "http://some.url.2" );
// 請求已經(jīng)發(fā)送了,我們可以讓得到的數(shù)據(jù)進行yield處理
const d1 = yield p1;
const d2 = yield p2;
var r3 = yield request(
"http://some.url.3/?v=" + d1 + "," + d2
);
}
這樣寫是不是和我們之前寫的Promise.all()
很像活逆?所以還可以改成這樣的:
function* send() {
// 先并發(fā)進行請求,然后等待解析數(shù)據(jù)
const [d1, d2] = yield Promise.all([
request( "http://some.url.1" ),
request( "http://some.url.2" )
])
var r3 = yield request(
"http://some.url.3/?v=" + d1 + "," + d2
);
}
async/await異步處理
ES7出現(xiàn)了async/await
進行異步的處理轻腺,使得異步操作就像同步代碼一樣簡單,方便了使用划乖,由于async/await
內(nèi)部封裝了generator
的 處理贬养,所有就很少有人用generator
來處理異步了,但是在異步的推動中generator
起到了很大的作用琴庵。
await: 后面接受一個promise實例
**async: 返回一個promise對象 **
一個簡單的異步請求
async function f() {
// 直接得到了接口返回的數(shù)據(jù)误算,在這里會等待接口返回數(shù)據(jù)。
let data = await fetch('').then(res => res.json())
console.log(data) // 接口數(shù)據(jù)
return data // 返回一個promise實例
}
async function h() {
let data = await Promise.resolve(22);
console.log(data); // 22
return data // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 22}
}
async function c() {
try {
let data = await Promise.reject(22);
console.log(11) // 不會執(zhí)行
} catch(e){
console.log(222) // 輸出 222
}
return 333 // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 333}
}
上面的例子是不是和generator中的異步請求很像迷殿?可以像同步一樣的編寫代碼儿礼,但是相比generator,await后面加上promise后直接返回相應(yīng)的數(shù)據(jù)庆寺,不像yield還需要從外部傳入蚊夫。
處理多個請求并發(fā)的情況(不需要管服務(wù)器的返回順序)
用async/await處理多個請求并發(fā),由于await后面需要添加Promise
實例懦尝,是不是腦袋里面一下子就想到了一個Promise.all()
// request返回一個promise對象
async function send() {
// 先并發(fā)進行請求,然后等待解析數(shù)據(jù)
const [d1, d2] = await Promise.all([
request( "http://some.url.1" ),
request( "http://some.url.2" )
])
}
你可能會很好奇知纷,為什么不需要像generator
那樣通過額外的函數(shù)來調(diào)用壤圃,因為async
已經(jīng)幫你想好了,內(nèi)部已經(jīng)調(diào)用了琅轧,是不是很爽伍绳?
處理多個請求并發(fā),并且需要保證返回數(shù)據(jù)的順序(運用場景比較少)
如果數(shù)據(jù)中沒有相互的聯(lián)系乍桂,但是又想一個個發(fā)送冲杀,可以這樣。
let patharr = [url1, url2, url3]
async function main2() {
let arrData = [];
// 利用for循環(huán)一次次的執(zhí)行
for(const url of pathArr) {
arrData.push(await request(url));
}
return arrData
}