玩轉(zhuǎn)js異步編程

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可以是三種值
  1. 單個值
  2. 一個promsie實例
  3. 一個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ù)pushtotalData中蜻直,就可以按照順序得到我們想要的值了盯质。當(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
} 

異步好文

圖解async/awati

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睹酌,一起剝皮案震驚了整個濱河市权谁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌憋沿,老刑警劉巖闯传,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卤妒,居然都是意外死亡,警方通過查閱死者的電腦和手機字币,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門则披,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洗出,你說我怎么就攤上這事士复。” “怎么了翩活?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵阱洪,是天一觀的道長。 經(jīng)常有香客問我菠镇,道長冗荸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任利耍,我火速辦了婚禮蚌本,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隘梨。我一直安慰自己程癌,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布轴猎。 她就那樣靜靜地躺著嵌莉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捻脖。 梳的紋絲不亂的頭發(fā)上锐峭,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天中鼠,我揣著相機與錄音,去河邊找鬼只祠。 笑死兜蠕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抛寝。 我是一名探鬼主播熊杨,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼盗舰!你這毒婦竟也來了晶府?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤钻趋,失蹤者是張志新(化名)和其女友劉穎川陆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛮位,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡较沪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了失仁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尸曼。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萄焦,靈堂內(nèi)的尸體忽然破棺而出控轿,到底是詐尸還是另有隱情,我是刑警寧澤拂封,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布茬射,位于F島的核電站,受9級特大地震影響冒签,放射性物質(zhì)發(fā)生泄漏在抛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一萧恕、第九天 我趴在偏房一處隱蔽的房頂上張望霜定。 院中可真熱鬧,春花似錦廊鸥、人聲如沸望浩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磨德。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間典挑,已是汗流浹背酥宴。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留您觉,地道東北人拙寡。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像琳水,于是被迫代替她去往敵國和親肆糕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 異步編程對JavaScript語言太重要在孝。Javascript語言的執(zhí)行環(huán)境是“單線程”的诚啃,如果沒有異步編程,根本...
    呼呼哥閱讀 7,301評論 5 22
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持私沮,譯者再次奉上一點點福利:阿里云產(chǎn)品券始赎,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大...
    HetfieldJoe閱讀 6,374評論 9 19
  • 弄懂js異步 講異步之前仔燕,我們必須掌握一個基礎(chǔ)知識-event-loop造垛。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,706評論 0 5
  • 本文首發(fā)在個人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云閱讀 1,679評論 0 3
  • 一.非阻塞和異步 借用知乎用戶嚴(yán)肅的回答在此總結(jié)下,同步和異步是針對消息通信機制晰搀,同步代表一個client發(fā)出一個...
    Daniel_adu閱讀 1,816評論 0 8