摘要:學(xué)習(xí)開(kāi)發(fā),無(wú)論是前端開(kāi)發(fā)還是都避免不了要接觸異步編程這個(gè)問(wèn)題就和其它大多數(shù)以多線程同步為主的編程語(yǔ)言不同的主要設(shè)計(jì)是單線程異步模型。由于異步編程可以實(shí)現(xiàn)非阻塞的調(diào)用效果,引入異步編程自然就是順理成章的事情了。
學(xué)習(xí)js開(kāi)發(fā),無(wú)論是前端開(kāi)發(fā)還是node.js,都避免不了要接觸異步編程這個(gè)問(wèn)題,就和其它大多數(shù)以多線程同步為主的編程語(yǔ)言不同,js的主要設(shè)計(jì)是單線程異步模型。正因?yàn)閖s天生的與眾不同,才使得它擁有一種獨(dú)特的魅力,也給學(xué)習(xí)者帶來(lái)了很多探索的道路。本文就從js的最初設(shè)計(jì)開(kāi)始,整理一下js異步編程的發(fā)展歷程。
什么是異步在研究js異步之前,先弄清楚異步是什么。異步是和同步相對(duì)的概念,同步,指的是一個(gè)調(diào)用發(fā)起后要等待結(jié)果返回,返回時(shí)候必須拿到返回結(jié)果。而異步的調(diào)用,發(fā)起之后直接返回,返回的時(shí)候還沒(méi)有結(jié)果,也不用等待結(jié)果,而調(diào)用結(jié)果是產(chǎn)生結(jié)果后通過(guò)被調(diào)用者通知調(diào)用者來(lái)傳遞的。
舉個(gè)例子,A想找C,但是不知道C的電話號(hào)碼,但是他有B的電話號(hào)碼,于是A給B打電話詢問(wèn)C的電話號(hào)碼,B需要查找才能知道C的電話號(hào)碼,之后會(huì)出現(xiàn)兩種場(chǎng)景看下面兩個(gè)場(chǎng)景:
A不掛電話,等到B找到號(hào)碼之后直接告訴A
A掛電話,B找到后再給A打電話告訴A
能感受到這兩種情況是不同的吧,前一種就是同步,后一種就是異步。
為什么是異步的先來(lái)看js的誕生,JavaScript誕生于1995年,由Brendan Eich設(shè)計(jì),最早是在Netscape公司的瀏覽器上實(shí)現(xiàn),用來(lái)實(shí)現(xiàn)在瀏覽器中處理簡(jiǎn)單的表單驗(yàn)證等用戶交互。至于后來(lái)提交到ECMA,形成規(guī)范,種種歷史不是這篇文章的重點(diǎn),提到這些就是想說(shuō)一點(diǎn),js的最初設(shè)計(jì)就是為了瀏覽器的GUI交互。對(duì)于圖形化界面處理,引入多線程勢(shì)必會(huì)帶來(lái)各種各樣的同步問(wèn)題,因此瀏覽器中的js被設(shè)計(jì)成單線程,還是很容易理解的。但是單線程有一個(gè)問(wèn)題:一旦這個(gè)唯一的線程被阻塞就沒(méi)辦法工作了--這肯定是不行的。由于異步編程可以實(shí)現(xiàn)“非阻塞”的調(diào)用效果,引入異步編程自然就是順理成章的事情了。
現(xiàn)在,js的運(yùn)行環(huán)境不限于瀏覽器,還有node.js,node.js設(shè)計(jì)的最初想法就是設(shè)計(jì)一個(gè)完全由事件驅(qū)動(dòng),非阻塞式IO實(shí)現(xiàn)的服務(wù)器運(yùn)行環(huán)境,因?yàn)榫W(wǎng)絡(luò)IO請(qǐng)求是一個(gè)非常大的性能瓶頸,前期使用其他編程語(yǔ)言都失敗了,就是因?yàn)槿藗児逃械耐骄幊趟枷耄藗兏鼉A向于使用同步設(shè)計(jì)的API。而js由于最初設(shè)計(jì)就是全異步的,人們不會(huì)有很多不適應(yīng),加上V8高性能引擎的出現(xiàn),才造就了node.js技術(shù)的產(chǎn)生。node.js擅長(zhǎng)處理IO密集型業(yè)務(wù),就得益于事件驅(qū)動(dòng),非阻塞IO的設(shè)計(jì),而這一切都與異步編程密不可分。
js異步原理這是一張簡(jiǎn)化的瀏覽器js執(zhí)行流程圖,nodejs和它不太一樣,但是都有一個(gè)隊(duì)列
這個(gè)隊(duì)列就是異步隊(duì)列,它是處理異步事件的核心,整個(gè)js調(diào)用時(shí)候,同步任務(wù)和其他編程語(yǔ)言一樣,在棧中調(diào)用,一旦遇上異步任務(wù),不立刻執(zhí)行,直接把它放到異步隊(duì)列里面,這樣就形成了兩種不同的任務(wù)。由于主線程中沒(méi)有阻塞,很快就完成,棧中任務(wù)邊空之后,就會(huì)有一個(gè)事件循環(huán),把隊(duì)列里面的任務(wù)一個(gè)一個(gè)取出來(lái)執(zhí)行。只要主線程空閑,異步隊(duì)列有任務(wù),事件循環(huán)就會(huì)從隊(duì)列中取出任務(wù)執(zhí)行。
說(shuō)的比較簡(jiǎn)單,js執(zhí)行引擎設(shè)計(jì)比這復(fù)雜的多得多,但是在js的異步實(shí)現(xiàn)原理中,事件循環(huán)和異步隊(duì)列是核心的內(nèi)容。
異步編程實(shí)現(xiàn)異步編程的代碼實(shí)現(xiàn),隨著時(shí)間的推移也在逐漸完善,不止是在js中,許多編程語(yǔ)言的使用者都在尋找一種優(yōu)雅的異步編程代碼書寫方式,下面來(lái)看js中的曾出現(xiàn)的幾種重要的實(shí)現(xiàn)方式。
最經(jīng)典的異步編程方式--callback提起異步編程,不能不提的就是回調(diào)(callback)的方式了,回調(diào)方式是最傳統(tǒng)的異步編程解決方案。首先要知道回調(diào)能解決異步問(wèn)題,但是不代表使用回調(diào)就是異步任務(wù)了。下面以最常見(jiàn)的網(wǎng)絡(luò)請(qǐng)求為例來(lái)演示callback是如何處理異步任務(wù)的,首先來(lái)看一個(gè)錯(cuò)誤的例子:
function getData(url) { const data = $.get(url); return data; } const data = getData("/api/data"); // 錯(cuò)誤,data為undefined
由于函數(shù)getData內(nèi)部需要執(zhí)行網(wǎng)絡(luò)請(qǐng)求,無(wú)法預(yù)知結(jié)果的返回時(shí)機(jī),直接通過(guò)同步的方式返回結(jié)果是行不通的,正確的寫法是像下面這樣:
function getData(url, callback) { $.get(url, data => { if (data.status === 200) { callback(null, data); } else { callback(data); } }); } getData("/api/data", (err, data) => { if (err) { console.log(err); } else { console.log(data); } });
callback方式利用了函數(shù)式編程的特點(diǎn),把要執(zhí)行的函數(shù)作為參數(shù)傳入,由被調(diào)用者控制執(zhí)行時(shí)機(jī),確保能夠拿到正確的結(jié)果。這種方式初看可能會(huì)有點(diǎn)難懂,但是熟悉函數(shù)式編程其實(shí)很簡(jiǎn)單,很好地解決了最基本的異步問(wèn)題,早期異步編程只能通過(guò)這種方式。
然而這種方式會(huì)有一個(gè)致命的問(wèn)題,在實(shí)際開(kāi)發(fā)中,模型總不會(huì)這樣簡(jiǎn)單,下面的場(chǎng)景是常有的事:
fun1(data => { // ... fun2(data, result => { // ... fun3(result, () => { // ... }); }); });
整個(gè)隨著系統(tǒng)越來(lái)越復(fù)雜,整個(gè)回調(diào)函數(shù)的層次會(huì)逐漸加深,里面再加上復(fù)雜的邏輯,代碼編寫維護(hù)都將變得十分困難,可讀性幾乎沒(méi)有。這被稱為毀掉地獄,一度困擾著開(kāi)發(fā)者,甚至是曾經(jīng)異步編程最為人詬病的地方。
從地獄中走出來(lái)--promise使用回調(diào)函數(shù)來(lái)編程很簡(jiǎn)單,但是回調(diào)地獄實(shí)在是太可怕了,嵌套層級(jí)足夠深之后絕對(duì)是維護(hù)的噩夢(mèng),而promise的出現(xiàn)就是解決這一問(wèn)題的。promise是按照規(guī)范實(shí)現(xiàn)的一個(gè)對(duì)象,ES6提供了原生的實(shí)現(xiàn),早期的三方實(shí)現(xiàn)也有很多。在此不會(huì)去討論promise規(guī)范和實(shí)現(xiàn)原理,重點(diǎn)來(lái)看promise是如何解決異步編程的問(wèn)題的。
Promise對(duì)象代表一個(gè)未完成、但預(yù)計(jì)將來(lái)會(huì)完成的操作,有三種狀態(tài):
pending:初始值,不是fulfilled,也不是rejected
resolved(也叫fulfilled):代表操作成功
rejected:代表操作失敗
整個(gè)promise的狀態(tài)只支持兩種轉(zhuǎn)換:從pending轉(zhuǎn)變?yōu)閞esolved,或從pending轉(zhuǎn)變?yōu)閞ejected,一旦轉(zhuǎn)化發(fā)生就會(huì)保持這種狀態(tài),不可以再發(fā)生變化,狀態(tài)發(fā)生變化后會(huì)觸發(fā)then方法。這里比較抽象,我們直接來(lái)改造上面的例子:
function getData(url) { return new Promise((resolve, reject) =>{ $.get(url, data => { if (data.status === 200) { reject(data); } else { resolve(data); } }); }); } getData("/api/data").then(data => { console.log(data); }).catch(err => { console.log(err); });
Promise是一個(gè)構(gòu)造函數(shù),它創(chuàng)建一個(gè)promise對(duì)象,接收一個(gè)回調(diào)函數(shù)作為參數(shù),而回調(diào)函數(shù)又接收兩個(gè)函數(shù)做參數(shù),分別代表promise的兩種狀態(tài)轉(zhuǎn)化。resolve回調(diào)會(huì)使promise由pending轉(zhuǎn)變?yōu)閞esolved,而reject 回調(diào)會(huì)使promise由pending轉(zhuǎn)變?yōu)閞ejected。
當(dāng)promise變?yōu)閞esolved時(shí)候,then方法就會(huì)被觸發(fā),在里面可以獲取到resolve的內(nèi)容,then方法。而一旦promise變?yōu)閞ejected,就會(huì)產(chǎn)生一個(gè)error。無(wú)論是resolve還是reject,都會(huì)返回一個(gè)新的Promise實(shí)例,返回值將作為參數(shù)傳入這個(gè)新Promise的resolve函數(shù),這樣就可以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,對(duì)于錯(cuò)誤的處理,系統(tǒng)提供了catch方法,錯(cuò)誤會(huì)一直向后傳遞,總是能被下一個(gè)catch捕獲。用promise可以有效地避免回調(diào)嵌套的問(wèn)題,代碼會(huì)變成下面的樣子:
fun1().then(data => { // ... return fun2(data); }).then(result => { // ... return fun3(result); }).then(() => { // ... });
整個(gè)調(diào)用過(guò)程變的很清晰,可維護(hù)性可擴(kuò)展性都會(huì)大大增強(qiáng),promise是一種非常重要的異步編程方式,它改變了以往的思維方式,也是后面新方式產(chǎn)生的重要基礎(chǔ)。
轉(zhuǎn)換思維--generatorpromise的寫法是最好的嗎,鏈?zhǔn)秸{(diào)用相比回調(diào)函數(shù)而言卻是可維護(hù)性增加了不少,但是和同步編程相比,異步看起來(lái)不是那么和諧,而generator的出現(xiàn)帶來(lái)了另一種思路。
generator是ES對(duì)協(xié)程的實(shí)現(xiàn),協(xié)程指的是函數(shù)并不是整個(gè)執(zhí)行下去的,一個(gè)函數(shù)執(zhí)行到一半可以移交執(zhí)行權(quán),等到可以的時(shí)候再獲得執(zhí)行權(quán),這種方式最大的特點(diǎn)就是同步的思維,除了控制執(zhí)行的yield命令之外,整體看起來(lái)和同步編程感覺(jué)幾乎一樣,下面來(lái)看一下這種方式的寫法:
function getDataPromise(url) { return new Promise((resolve, reject) =>{ $.get(url, data => { if (data.status === 200) { reject(data); } else { resolve(data); } }); }); } function *getDataGen(url) { yield getDataPromise(url); } const g = getDataGen("/api/data"); g.next();
generator與普通函數(shù)的區(qū)別就是前面多一個(gè)*,不過(guò)這不是重點(diǎn),重點(diǎn)是generator里面可以使用yield關(guān)鍵字來(lái)表示暫停,它接收一個(gè)promise對(duì)象,返回promise的結(jié)果并且停在此處等待,不是一次性執(zhí)行完。generator執(zhí)行后會(huì)返回一個(gè)iterator,iterator里面有一個(gè)next方法,每次調(diào)用next方法,generator都會(huì)向下執(zhí)行,直到遇上yield,返回結(jié)果是一個(gè)對(duì)象,里面有一個(gè)value屬性,值為當(dāng)前yield返回結(jié)果,done屬性代表整個(gè)generator是否執(zhí)行完畢。generator的出現(xiàn)使得像同步一樣編寫異步代碼成為可能,下面是使用generator改造后的結(jié)果:
* fun() { const data = yield fun1(); // ... const result = yield fun2(data); // ... yield fun3(result); // ... } const g = fun(); g.next(); g.next(); g.next(); g.next();
在generator的編寫過(guò)程中,我們還需要手動(dòng)控制執(zhí)行過(guò)程,而實(shí)際上這是可以自動(dòng)實(shí)現(xiàn)的,接下來(lái)的一種新語(yǔ)法的產(chǎn)生使得異步編程真的和同步一樣容易了。
新時(shí)代的寫法--async,await異步編程的最高境界,就是根本不用關(guān)心它是不是異步。在最新的ES中,終于有了這種激動(dòng)人心的語(yǔ)法了。async函數(shù)的寫法和generator幾乎相同,把*換成async關(guān)鍵字,把yield換成await即可。async函數(shù)內(nèi)部自帶generator執(zhí)行器,我們不再需要手動(dòng)控制執(zhí)行了,現(xiàn)在來(lái)看最終的寫法:
function getDataPromise(url) { return new Promise((resolve, reject) =>{ $.get(url, data => { if (data.status === 200) { reject(data); } else { resolve(data); } }); }); } async function getData(url) { return await getDataPromise(url); } const data = await getData(url);
除了多了關(guān)鍵字,剩下的和同步的編碼方式完全相同,對(duì)于異常捕獲也可以采取同步的try-catch方式,對(duì)于再?gòu)?fù)雜的場(chǎng)景也不會(huì)邏輯混亂了:
* fun() { const data = await fun1(); // ... const result = await fun2(data); // ... return await fun3(result); // ... } fun()
現(xiàn)在回去看回調(diào)函數(shù)的寫法,感覺(jué)好像換了一個(gè)世界。這種語(yǔ)法比較新,在不支持的環(huán)境要使用babel轉(zhuǎn)譯。
寫在最后在js中,異步編程是一個(gè)長(zhǎng)久的話題,很慶幸現(xiàn)在有這么好用的async和await,不過(guò)promise原理,回調(diào)函數(shù)都是要懂的,很重要的內(nèi)容,弄清楚異步編程模式,算是掃清了學(xué)習(xí)js尤其是node.js路上最大的障礙了。
尊重原創(chuàng),轉(zhuǎn)載分享前請(qǐng)先知悉作者,也歡迎指出錯(cuò)誤不足共同交流,更多內(nèi)容歡迎關(guān)注作者博客點(diǎn)擊這里
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/92607.html
摘要:是與服務(wù)器交換數(shù)據(jù)并更新部分網(wǎng)頁(yè)的藝術(shù),在不重新加載整個(gè)頁(yè)面的情況下。對(duì)象是的核心,所有現(xiàn)代瀏覽器均支持對(duì)象和使用。用于在后臺(tái)與服務(wù)器交換數(shù)據(jù)。及時(shí)有效地幫助學(xué)員解決疑難問(wèn)題,提高學(xué)員的學(xué)習(xí)積極性。 Asynchronous JavaScript and XML(異步的 JavaScript 和 XML)。 AJAX...
摘要:基于以太坊項(xiàng)目,以太坊團(tuán)隊(duì)目前運(yùn)營(yíng)了一個(gè)公開(kāi)的區(qū)塊鏈平臺(tái)以太坊網(wǎng)絡(luò)。主要特點(diǎn)以太坊區(qū)塊鏈底層也是一個(gè)類似比特幣網(wǎng)絡(luò)的網(wǎng)絡(luò)平臺(tái),智能合約運(yùn)行在網(wǎng)絡(luò)中的以太坊虛擬機(jī)里。以太坊采用交易作為執(zhí)行操作的最小單位。 以太坊將比特幣針對(duì)數(shù)字交易的功能進(jìn)一步進(jìn)行了拓展,面向更為復(fù)雜和靈活的應(yīng)用場(chǎng)景,支持了智能合約這一重要特性。 以太坊項(xiàng)目簡(jiǎn)介 以太坊:項(xiàng)目最初的目標(biāo)是打造以個(gè)智能合約的平臺(tái),該平臺(tái)支持...
摘要:本次主要分享關(guān)于上一篇區(qū)域的學(xué)習(xí)。區(qū)域?yàn)榈暮诵牟糠郑慕Y(jié)構(gòu)如下為了便于梳理思路,以上代碼省略了細(xì)節(jié),只保留了輪廓脈絡(luò)。最終暴露給開(kāi)發(fā)者的如下圖所示這里只分析了區(qū)域的結(jié)構(gòu),下一次會(huì)深入到函數(shù)語(yǔ)句粒度。 本次主要分享關(guān)于上一篇區(qū)域2的學(xué)習(xí)。區(qū)域2為Zepto的核心部分,它的結(jié)構(gòu)如下 var Zepto = (function() { var $, zepto = {}; fu...
摘要:確認(rèn)需要移除的長(zhǎng)度,最小為,確認(rèn)原數(shù)組變化后的新長(zhǎng)度按照索引位置,獲取長(zhǎng)度的新數(shù)組,用于返回對(duì)原數(shù)組進(jìn)行替換新元素。 array.splice(start,deleteCount,item1,item2....) splice方法從array移除n個(gè)元素(大于或等于0),并且可以用新的item替換被移除的元素。參數(shù)start是從數(shù)組array中移除元素的最開(kāi)始位置(數(shù)組的索引,正負(fù)數(shù)表...
閱讀 2814·2019-08-30 15:55
閱讀 2861·2019-08-30 15:53
閱讀 2299·2019-08-26 13:47
閱讀 2562·2019-08-26 13:43
閱讀 3161·2019-08-26 13:33
閱讀 2809·2019-08-26 11:53
閱讀 1801·2019-08-23 18:35
閱讀 804·2019-08-23 17:16