摘要:接下來(lái)介紹下異步編程六種方法。六生成器函數(shù)是提供的一種異步編程解決方案,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同,最大的特點(diǎn)就是可以控制函數(shù)的執(zhí)行。參考文章前端面試之道異步編程的種方法你不知道的中卷函數(shù)的含義和用法替代的個(gè)理由
前言
我們知道Javascript語(yǔ)言的執(zhí)行環(huán)境是"單線程"。也就是指一次只能完成一件任務(wù)。如果有多個(gè)任務(wù),就必須排隊(duì),前面一個(gè)任務(wù)完成,再執(zhí)行后面一個(gè)任務(wù)。
這種模式雖然實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,執(zhí)行環(huán)境相對(duì)單純,但是只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行。常見(jiàn)的瀏覽器無(wú)響應(yīng)(假死),往往就是因?yàn)槟骋欢蜫avascript代碼長(zhǎng)時(shí)間運(yùn)行(比如死循環(huán)),導(dǎo)致整個(gè)頁(yè)面卡在這個(gè)地方,其他任務(wù)無(wú)法執(zhí)行。
為了解決這個(gè)問(wèn)題,Javascript語(yǔ)言將任務(wù)的執(zhí)行模式分成兩種:同步和異步。本文主要介紹異步編程幾種辦法,并通過(guò)比較,得到最佳異步編程的解決方案!
想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客
一、同步與異步我們可以通俗理解為異步就是一個(gè)任務(wù)分成兩段,先執(zhí)行第一段,然后轉(zhuǎn)而執(zhí)行其他任務(wù),等做好了準(zhǔn)備,再回過(guò)頭執(zhí)行第二段。排在異步任務(wù)后面的代碼,不用等待異步任務(wù)結(jié)束會(huì)馬上運(yùn)行,也就是說(shuō),異步任務(wù)不具有”堵塞“效應(yīng)。比如,有一個(gè)任務(wù)是讀取文件進(jìn)行處理,異步的執(zhí)行過(guò)程就是下面這樣
這種不連續(xù)的執(zhí)行,就叫做異步。相應(yīng)地,連續(xù)的執(zhí)行,就叫做同步
"異步模式"非常重要。在瀏覽器端,耗時(shí)很長(zhǎng)的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng),最好的例子就是Ajax操作。在服務(wù)器端,"異步模式"甚至是唯一的模式,因?yàn)閳?zhí)行環(huán)境是單線程的,如果允許同步執(zhí)行所有http請(qǐng)求,服務(wù)器性能會(huì)急劇下降,很快就會(huì)失去響應(yīng)。接下來(lái)介紹下異步編程六種方法。
二、回調(diào)函數(shù)(Callback)回調(diào)函數(shù)是異步操作最基本的方法。以下代碼就是一個(gè)回調(diào)函數(shù)的例子:
ajax(url, () => { // 處理邏輯 })
但是回調(diào)函數(shù)有一個(gè)致命的弱點(diǎn),就是容易寫出回調(diào)地獄(Callback hell)。假設(shè)多個(gè)請(qǐng)求存在依賴性,你可能就會(huì)寫出如下代碼:
ajax(url, () => { // 處理邏輯 ajax(url1, () => { // 處理邏輯 ajax(url2, () => { // 處理邏輯 }) }) })
回調(diào)函數(shù)的優(yōu)點(diǎn)是簡(jiǎn)單、容易理解和實(shí)現(xiàn),缺點(diǎn)是不利于代碼的閱讀和維護(hù),各個(gè)部分之間高度耦合,使得程序結(jié)構(gòu)混亂、流程難以追蹤(尤其是多個(gè)回調(diào)函數(shù)嵌套的情況),而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)。此外它不能使用 try catch 捕獲錯(cuò)誤,不能直接 return。
三、事件監(jiān)聽(tīng)這種方式下,異步任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個(gè)事件是否發(fā)生。
下面是兩個(gè)函數(shù)f1和f2,編程的意圖是f2必須等到f1執(zhí)行完成,才能執(zhí)行。首先,為f1綁定一個(gè)事件(這里采用的jQuery的寫法)
f1.on("done", f2);
上面這行代碼的意思是,當(dāng)f1發(fā)生done事件,就執(zhí)行f2。然后,對(duì)f1進(jìn)行改寫:
function f1() { setTimeout(function () { // ... f1.trigger("done"); }, 1000); }
上面代碼中,f1.trigger("done")表示,執(zhí)行完成后,立即觸發(fā)done事件,從而開(kāi)始執(zhí)行f2。
這種方法的優(yōu)點(diǎn)是比較容易理解,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù),而且可以"去耦合",有利于實(shí)現(xiàn)模塊化。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型,運(yùn)行流程會(huì)變得很不清晰。閱讀代碼的時(shí)候,很難看出主流程。
四、發(fā)布訂閱我們假定,存在一個(gè)"信號(hào)中心",某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心"發(fā)布"(publish)一個(gè)信號(hào),其他任務(wù)可以向信號(hào)中心"訂閱"(subscribe)這個(gè)信號(hào),從而知道什么時(shí)候自己可以開(kāi)始執(zhí)行。這就叫做"發(fā)布/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
首先,f2向信號(hào)中心jQuery訂閱done信號(hào)。
jQuery.subscribe("done", f2);
然后,f1進(jìn)行如下改寫:
function f1() { setTimeout(function () { // ... jQuery.publish("done"); }, 1000); }
上面代碼中,jQuery.publish("done")的意思是,f1執(zhí)行完成后,向信號(hào)中心jQuery發(fā)布done信號(hào),從而引發(fā)f2的執(zhí)行。
f2完成執(zhí)行后,可以取消訂閱(unsubscribe)
jQuery.unsubscribe("done", f2);
這種方法的性質(zhì)與“事件監(jiān)聽(tīng)”類似,但是明顯優(yōu)于后者。因?yàn)榭梢酝ㄟ^(guò)查看“消息中心”,了解存在多少信號(hào)、每個(gè)信號(hào)有多少訂閱者,從而監(jiān)控程序的運(yùn)行。
五、Promise/A+Promise本意是承諾,在程序中的意思就是承諾我過(guò)一段時(shí)間后會(huì)給你一個(gè)結(jié)果。 什么時(shí)候會(huì)用到過(guò)一段時(shí)間?答案是異步操作,異步是指可能比較長(zhǎng)時(shí)間才有結(jié)果的才做,例如網(wǎng)絡(luò)請(qǐng)求、讀取本地文件等
1.Promise的三種狀態(tài)Pending----Promise對(duì)象實(shí)例創(chuàng)建時(shí)候的初始狀態(tài)
Fulfilled----可以理解為成功的狀態(tài)
Rejected----可以理解為失敗的狀態(tài)
這個(gè)承諾一旦從等待狀態(tài)變成為其他狀態(tài)就永遠(yuǎn)不能更改狀態(tài)了,比如說(shuō)一旦狀態(tài)變?yōu)?resolved 后,就不能再次改變?yōu)镕ulfilled
let p = new Promise((resolve, reject) => { reject("reject") resolve("success")//無(wú)效代碼不會(huì)執(zhí)行 }) p.then( value => { console.log(value) }, reason => { console.log(reason)//reject } )
當(dāng)我們?cè)跇?gòu)造 Promise 的時(shí)候,構(gòu)造函數(shù)內(nèi)部的代碼是立即執(zhí)行的
new Promise((resolve, reject) => { console.log("new Promise") resolve("success") }) console.log("end") // new Promise => end2.promise的鏈?zhǔn)秸{(diào)用
每次調(diào)用返回的都是一個(gè)新的Promise實(shí)例(這就是then可用鏈?zhǔn)秸{(diào)用的原因)
如果then中返回的是一個(gè)結(jié)果的話會(huì)把這個(gè)結(jié)果傳遞下一次then中的成功回調(diào)
如果then中出現(xiàn)異常,會(huì)走下一個(gè)then的失敗回調(diào)
在 then中使用了return,那么 return 的值會(huì)被Promise.resolve() 包裝(見(jiàn)例1,2)
then中可以不傳遞參數(shù),如果不傳遞會(huì)透到下一個(gè)then中(見(jiàn)例3)
catch 會(huì)捕獲到?jīng)]有捕獲的異常
接下來(lái)我們看幾個(gè)例子:
// 例1 Promise.resolve(1) .then(res => { console.log(res) return 2 //包裝成 Promise.resolve(2) }) .catch(err => 3) .then(res => console.log(res))
// 例2 Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error("My Error") }) .catch(() => 1) .then(x => x + 1) .then(x => console.log(x)) //2 .catch(console.error)
// 例3 let fs = require("fs") function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, "utf8", (err, data) => { if (err) reject(err) resolve(data) }) }) } read("./name.txt") .then(function(data) { throw new Error() //then中出現(xiàn)異常,會(huì)走下一個(gè)then的失敗回調(diào) }) //由于下一個(gè)then沒(méi)有失敗回調(diào),就會(huì)繼續(xù)往下找,如果都沒(méi)有,就會(huì)被catch捕獲到 .then(function(data) { console.log("data") }) .then() .then(null, function(err) { console.log("then", err)// then error }) .catch(function(err) { console.log("error") })
Promise不僅能夠捕獲錯(cuò)誤,而且也很好地解決了回調(diào)地獄的問(wèn)題,可以把之前的回調(diào)地獄例子改寫為如下代碼:
ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))
它也是存在一些缺點(diǎn)的,比如無(wú)法取消 Promise,錯(cuò)誤需要通過(guò)回調(diào)函數(shù)捕獲。
六、生成器Generators/ yieldGenerator 函數(shù)是 ES6 提供的一種異步編程解決方案,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同,Generator 最大的特點(diǎn)就是可以控制函數(shù)的執(zhí)行。
語(yǔ)法上,首先可以把它理解成,Generator 函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)。
Generator 函數(shù)除了狀態(tài)機(jī),還是一個(gè)遍歷器對(duì)象生成函數(shù)。
可暫停函數(shù), yield可暫停,next方法可啟動(dòng),每次返回的是yield后的表達(dá)式結(jié)果。
yield表達(dá)式本身沒(méi)有返回值,或者說(shuō)總是返回undefined。next方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)作上一個(gè)yield表達(dá)式的返回值。
我們先來(lái)看個(gè)例子:
function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z) } let it = foo(5) console.log(it.next()) // => {value: 6, done: false} console.log(it.next(12)) // => {value: 8, done: false} console.log(it.next(13)) // => {value: 42, done: true}
可能結(jié)果跟你想象不一致,接下來(lái)我們逐行代碼分析:
首先 Generator 函數(shù)調(diào)用和普通函數(shù)不同,它會(huì)返回一個(gè)迭代器
當(dāng)執(zhí)行第一次 next 時(shí),傳參會(huì)被忽略,并且函數(shù)暫停在 yield (x + 1) 處,所以返回 5 + 1 = 6
當(dāng)執(zhí)行第二次 next 時(shí),傳入的參數(shù)12就會(huì)被當(dāng)作上一個(gè)yield表達(dá)式的返回值,如果你不傳參,yield 永遠(yuǎn)返回 undefined。此時(shí) let y = 2 12,所以第二個(gè) yield 等于 2 12 / 3 = 8
當(dāng)執(zhí)行第三次 next 時(shí),傳入的參數(shù)13就會(huì)被當(dāng)作上一個(gè)yield表達(dá)式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42
我們?cè)賮?lái)看個(gè)例子:有三個(gè)本地文件,分別1.txt,2.txt和3.txt,內(nèi)容都只有一句話,下一個(gè)請(qǐng)求依賴上一個(gè)請(qǐng)求的結(jié)果,想通過(guò)Generator函數(shù)依次調(diào)用三個(gè)文件
//1.txt文件 2.txt
//2.txt文件 3.txt
//3.txt文件 結(jié)束
let fs = require("fs") function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, "utf8", function(err, data) { if (err) reject(err) resolve(data) }) }) } function* r() { let r1 = yield read("./1.txt") let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let it = r() let { value, done } = it.next() value.then(function(data) { // value是個(gè)promise console.log(data) //data=>2.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>3.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>結(jié)束 }) }) }) // 2.txt=>3.txt=>結(jié)束
從上例中我們看出手動(dòng)迭代Generator 函數(shù)很麻煩,實(shí)現(xiàn)邏輯有點(diǎn)繞,而實(shí)際開(kāi)發(fā)一般會(huì)配合 co 庫(kù)去使用。co是一個(gè)為Node.js和瀏覽器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加優(yōu)雅的方式編寫非阻塞代碼。
安裝co庫(kù)只需:npm install co
上面例子只需兩句話就可以輕松實(shí)現(xiàn)
function* r() { let r1 = yield read("./1.txt") let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let co = require("co") co(r()).then(function(data) { console.log(data) }) // 2.txt=>3.txt=>結(jié)束=>undefined
我們可以通過(guò) Generator 函數(shù)解決回調(diào)地獄的問(wèn)題,可以把之前的回調(diào)地獄例子改寫為如下代碼:
function *fetch() { yield ajax(url, () => {}) yield ajax(url1, () => {}) yield ajax(url2, () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()七、async/await 1.Async/Await簡(jiǎn)介
使用async/await,你可以輕松地達(dá)成之前使用生成器和co函數(shù)所做到的工作,它有如下特點(diǎn):
async/await是基于Promise實(shí)現(xiàn)的,它不能用于普通的回調(diào)函數(shù)。
async/await與Promise一樣,是非阻塞的。
async/await使得異步代碼看起來(lái)像同步代碼,這正是它的魔力所在。
一個(gè)函數(shù)如果加上 async ,那么該函數(shù)就會(huì)返回一個(gè) Promise
async function async1() { return "1" } console.log(async1()) // -> Promise {: "1"}
Generator函數(shù)依次調(diào)用三個(gè)文件那個(gè)例子用async/await寫法,只需幾句話便可實(shí)現(xiàn)
let fs = require("fs") function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, "utf8", function(err, data) { if (err) reject(err) resolve(data) }) }) } async function readResult(params) { try { let p1 = await read(params, "utf8")//await后面跟的是一個(gè)Promise實(shí)例 let p2 = await read(p1, "utf8") let p3 = await read(p2, "utf8") console.log("p1", p1) console.log("p2", p2) console.log("p3", p3) return p3 } catch (error) { console.log(error) } } readResult("1.txt").then( // async函數(shù)返回的也是個(gè)promise data => { console.log(data) }, err => console.log(err) ) // p1 2.txt // p2 3.txt // p3 結(jié)束 // 結(jié)束2.Async/Await并發(fā)請(qǐng)求
如果請(qǐng)求兩個(gè)文件,毫無(wú)關(guān)系,可以通過(guò)并發(fā)請(qǐng)求
let fs = require("fs") function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, "utf8", function(err, data) { if (err) reject(err) resolve(data) }) }) } function readAll() { read1() read2()//這個(gè)函數(shù)同步執(zhí)行 } async function read1() { let r = await read("1.txt","utf8") console.log(r) } async function read2() { let r = await read("2.txt","utf8") console.log(r) } readAll() // 2.txt 3.txt八、總結(jié)
1.JS 異步編程進(jìn)化史:callback -> promise -> generator -> async + await
2.async/await 函數(shù)的實(shí)現(xiàn),就是將 Generator 函數(shù)和自動(dòng)執(zhí)行器,包裝在一個(gè)函數(shù)里。
3.async/await可以說(shuō)是異步終極解決方案了。
(1) async/await函數(shù)相對(duì)于Promise,優(yōu)勢(shì)體現(xiàn)在:
處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫出代碼
并且也能優(yōu)雅地解決回調(diào)地獄問(wèn)題。
當(dāng)然async/await函數(shù)也存在一些缺點(diǎn),因?yàn)?await 將異步代碼改造成了同步代碼,如果多個(gè)異步代碼沒(méi)有依賴性卻使用了 await 會(huì)導(dǎo)致性能上的降低,代碼沒(méi)有依賴性的話,完全可以使用 Promise.all 的方式。
(2) async/await函數(shù)對(duì) Generator 函數(shù)的改進(jìn),體現(xiàn)在以下三點(diǎn):
內(nèi)置執(zhí)行器。
Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,所以才有了 co 函數(shù)庫(kù),而 async 函數(shù)自帶執(zhí)行器。也就是說(shuō),async 函數(shù)的執(zhí)行,與普通函數(shù)一模一樣,只要一行。
更廣的適用性。 co 函數(shù)庫(kù)約定,yield 命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象,而 async 函數(shù)的 await 命令后面,可以跟 Promise 對(duì)象和原始類型的值(數(shù)值、字符串和布爾值,但這時(shí)等同于同步操作)。
更好的語(yǔ)義。 async 和 await,比起星號(hào)和 yield,語(yǔ)義更清楚了。async 表示函數(shù)里有異步操作,await 表示緊跟在后面的表達(dá)式需要等待結(jié)果。
參考文章Promises/A+
前端面試之道
Javascript異步編程的4種方法
你不知道的JavaScript(中卷)
async 函數(shù)的含義和用法
Async/Await替代Promise的6個(gè)理由
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/101004.html
摘要:以下為幾種異步編程方式的總結(jié),希望與君共勉?;卣{(diào)函數(shù)事件監(jiān)聽(tīng)發(fā)布訂閱模式異步編程傳統(tǒng)的解決方案回調(diào)函數(shù)和事件監(jiān)聽(tīng)初始示例假設(shè)有兩個(gè)函數(shù)和,是一個(gè)需要一定時(shí)間的函數(shù)。 異步編程 眾所周知 JavaScript 是單線程工作,也就是只有一個(gè)腳本執(zhí)行完成后才能執(zhí)行下一個(gè)腳本,兩個(gè)腳本不能同時(shí)執(zhí)行,如果某個(gè)腳本耗時(shí)很長(zhǎng),后面的腳本都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行。以下為幾種異步編程方式的總...
摘要:本文最早為雙十一而作,原標(biāo)題雙大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個(gè)友善的吐槽讀書(shū)清單也要收費(fèi)。這本書(shū)便從的異步編程講起,幫助我們?cè)O(shè)計(jì)快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡(jiǎn)單的頁(yè)面。 本文最早為雙十一而作,原標(biāo)題雙 11 大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進(jìn)行了深入的交流,現(xiàn)免費(fèi)分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標(biāo)題雙大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個(gè)友善的吐槽讀書(shū)清單也要收費(fèi)。這本書(shū)便從的異步編程講起,幫助我們?cè)O(shè)計(jì)快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡(jiǎn)單的頁(yè)面。 本文最早為雙十一而作,原標(biāo)題雙 11 大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進(jìn)行了深入的交流,現(xiàn)免費(fèi)分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標(biāo)題雙大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個(gè)友善的吐槽讀書(shū)清單也要收費(fèi)。這本書(shū)便從的異步編程講起,幫助我們?cè)O(shè)計(jì)快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡(jiǎn)單的頁(yè)面。 本文最早為雙十一而作,原標(biāo)題雙 11 大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進(jìn)行了深入的交流,現(xiàn)免費(fèi)分享到這里,不足之處歡迎指教...
閱讀 3609·2021-11-15 11:37
閱讀 2974·2021-11-12 10:36
閱讀 4403·2021-09-22 15:51
閱讀 2381·2021-08-27 16:18
閱讀 881·2019-08-30 15:44
閱讀 2163·2019-08-30 10:58
閱讀 1769·2019-08-29 17:18
閱讀 3269·2019-08-28 18:25