异步调用可以转化为同步调用吗?
源起
小飛是一名剛?cè)胄星岸瞬痪玫男氯?#xff0c;因為進到了某個大公司,儼然成為了學(xué)弟學(xué)妹眼中'大神',大家遇到j(luò)s問題都喜歡問他,這不,此時他的qq彈出了這樣一條消息
"hi,大神在嗎?我有個問題想問,現(xiàn)在我們的代碼里面有這樣的東西,可是得不到正確的返回結(jié)果
| 1 2 3 4 5 6 7 | function?getDataByAjax () { return?$.ajax(...postParam) } var?data = getDataByAjax() if?(data) { ???console.log(data.info) } |
?
"哦,你這里是異步調(diào)用,不能直接獲得返回值,你要把if語句寫到回調(diào)函數(shù)中",小飛不假思索的說到,對于一個‘專業(yè)’的fe來說,這根本不是一個問題。
“可是我希望只是改造getDataByAjax這個方法,讓后面的代碼成立。”
“研究這個沒有意義,異步是js的精髓,同步的話會阻塞js調(diào)用,超級慢的,但是你要一再堅持的話,用async:true就好了”
“不愧是大神,我回去立刻試一試,么么噠”
兩天后,她哭喪著臉登上了qq
“試了一下你的方法,但是根本行不通,哭~~”
“別急,我看看你這個postParam的參數(shù)行嗎”
| 1 2 3 4 5 6 | { ... ???dataType:?'jsonp', async:?true ... } |
?
"這是一個jsonp請求啊,老掉牙的東西了,,jsonp請求是沒有辦法同步的"
“我知道jsonp請求的原理是通過script標(biāo)簽實現(xiàn)的,但是,你看,script也是支持同步的呀,你看http://www.w3school.com.cn/tags/attscriptasync.asp”
“額,那可能是jquery沒有實現(xiàn)吧,哈哈”
“大神,你能幫我實現(xiàn)一個jsonp的同步調(diào)用方式嘛,拜托了(星星眼)”
雖然他有點奇怪jquery為什么沒有實現(xiàn),但是既然w3school的標(biāo)準(zhǔn)擺在那里,碼兩行代碼又沒什么,
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | export?const loadJsonpSync = (url) => { var?result; ?window.callback1 = (data) => (result = data) let?head = window.document.getElementsByTagName('head')[0] let?js = window.document.createElement('script') ?js.setAttribute('type',?'text/javascript') ?js.setAttribute('async',?'sync') // 這句顯式聲明強調(diào)src不是按照異步方式調(diào)用的 ?js.setAttribute('src', url) ?head.appendChild(js) return?result } |
?
額,運行起來結(jié)果竟然是undefined!w3cshool的文檔竟然也不準(zhǔn),還權(quán)威呢,我看也不怎么著,小飛暗自想到。
“剛才試了一下,w3school文檔上寫的有問題,這個異步屬性根本就是錯的”
“可是我剛還試過一次這個,我確認(rèn)是好的呀”
| 1 2 | <script src="loop50000 && put('frist').js"></script> <script src="put('second').js"></script> |
(有興趣的同學(xué)可以實現(xiàn)以下兩個js,并且加上async的標(biāo)簽進行嘗試。)
“這個,我就搞不清楚了”,小飛訕訕的說到
對方已離線
抽象
關(guān)于這個問題,相信不只是小飛,很多人都難以解答。為什么ajax可以做到同步,但jsonp不行,推廣到nodejs上,為什么readFile也可以做到同步(readFileSync),但有的庫卻不行。
(至于script的async選項我們暫時避而不談,是因為現(xiàn)在的知識維度暫時還不夠,但是不要著急,下文中會給出明確的解釋)
現(xiàn)在,讓我們以計算機科學(xué)的角度抽象這個問題:
既然是抽象問題,那么我們就可以不從工程角度/性能角度/實現(xiàn)語言等等等方面來看(同步比異步效率低下),每增加一個維度,復(fù)雜程度將以幾何爆炸般增長下去。
首先,我們來明確一點,==在計算機科學(xué)領(lǐng)域==同步和異步的定義
同步(英語:Synchronization),指對在一個系統(tǒng)中所發(fā)生的事件(event)之間進行協(xié)調(diào),在時間上出現(xiàn)一致性與統(tǒng)一化的現(xiàn)象。在系統(tǒng)中進行同步,也被稱為及時(in time)、同步化的(synchronous、in sync)。--摘自百度百科異步的概念和同步相對。即時間不一致,不統(tǒng)一
明確了這一點,我們可以借助甘特圖來表示同步和異步
?
其中t1和t2是同步的,t1和t3是異步的。
答案就在操作系統(tǒng)原理的大學(xué)教材上,我們有自旋鎖,信號量來解決問題,偽代碼如下
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | spinLock () { // 自旋鎖 ??fork Wait 3000 unlock()?//開啟一個異步線程,等待三秒后執(zhí)行解鎖動作 ??loop until unlock?// 不斷進行空循環(huán)直到解鎖動作 Put ‘unlock’ } //pv原語,當(dāng)信號量為假時立即執(zhí)行下一步,同時將信號量置真 //反之將當(dāng)前執(zhí)行棧掛起,置入等待喚醒隊列 //uv原語,將信號量置為假,并從等待喚醒隊列中喚醒一個執(zhí)行棧 Semaphore () { ??pv() ??fork Wait 3000 uv() ??pv() ??uv() Put?'unlock' }? |
很好,至此都可以在操作系統(tǒng)原理的教材上翻到答案。于是我們在此基礎(chǔ)上添加約束條件
僅僅依賴于js本身,我們是否可以將異步代碼轉(zhuǎn)化為同步代碼呢?(ASYNCCALL => SYNCCALL)論證
帶著這個問題,我們翻看一下jquery的源碼
https://github.com/jquery/jquery/blob/262acc6f1e0f71a3a8b786e3c421b2e645799ea0/src/ajax/xhr.js#L42
可以看出, ajax的同步機制本質(zhì)上是由XMLHttpRequest實現(xiàn)的,而非js原生實現(xiàn)。
同樣的道理,我們再翻看一下nodejs的源碼
https://github.com/nodejs/node/blob/v8.3.0/lib/fs.js#L550
從readFileSync->tryReadSync->readSync一路追下去,會追到一個c++ binding,?https://github.com/nodejs/node/blob/v8.3.0/src/node_file.cc#L1167
| 1 2 3 4 5 6 | if?(req->IsObject()) { ???ASYNC_CALL(read, req, UTF8, fd, &uvbuf, 1, pos); }?else?{ ???SYNC_CALL(read, 0, fd, &uvbuf, 1, pos) ???args.GetReturnValue().Set(SYNC_RESULT); } |
同步的奧妙在于c++的宏定義上,這是一種借由c++來實現(xiàn)的底層同步方式。
觀察了這兩種最廣泛的異步轉(zhuǎn)同步式調(diào)用,我們發(fā)現(xiàn)均沒有采用js來實現(xiàn)。
似乎從現(xiàn)象層面上來看js無法原生支持,但是這還不夠,我們探究在js語義下上面的自旋鎖/信號量的特性模擬實現(xiàn)(我知道你們一定會嗤之以鼻,==js本身就是單線程的,只是模擬了多線程的特性== 我無比贊同這句話,所以這里用的不是實現(xiàn),而是特性模擬實現(xiàn)),另外,由于settimeout具有fork相似的異步執(zhí)行特性,所以我們用setitmeout暫時代替fork
自旋鎖
1.第一個實現(xiàn)版本
| 1 2 3 4 5 6 7 | var?lock =?true setTimeout(function?() { lock =?false }, 5000) while(lock); console.log('unlock') |
我們預(yù)期在5000ms后執(zhí)行unlock語句,但是悲劇的是,整個chrome進程僵死掉了。
為了解釋清楚這個問題,我們讀一下阮一峰老師的event loop模型
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
看樣子咱們已經(jīng)清楚的了解了event loop這個js運行順序的本質(zhì)(同步執(zhí)行代碼立即執(zhí)行,異步代碼入等待隊列),那么,我們可以基于此給出js vm的調(diào)度實現(xiàn)(eventloop的一種實現(xiàn)),當(dāng)然,咱們?yōu)榱私忉屪孕i失敗只需要模擬異步操作, 同步操作,和循環(huán)就好
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //taskQueue:任務(wù)隊列 //runPart:當(dāng)前正在執(zhí)行的任務(wù)(同步指令集) //instruct: 正在執(zhí)行的指令 function?eventloop (taskQueue) { while(runPart = taskQueue.shift()) { while(instruct = runPart.shift()) { const { type, act, codePart } = instruct switch(type) { case?'SYNC': ?????????console.log(act) if?(act ===?'loop') ???????????runPart.unshift({ ?????????????act:?'loop', ?????????????type:?'SYNC' }) break case?'ASYNC': ?????????taskQueue.push(codePart) break } } } } |
然后轉(zhuǎn)化我們的第一個版本自旋鎖
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | let?taskQueue = [ [ {act:?'var lock = true', type:?'SYNC'},?//var lock = true { ???????act:?'setTimeout', ???????type:?'ASYNC', ???????codePart: [ {act:?'lock = false', type:?'SYNC'} ] },?// setTimeout(function () { lock = false }, 5000) /*{ ???????act: 'loop', ???????type: 'SYNC' ???},*/?// while(lock); { ???????act:?'console.log(\'sync\')', ???????type:?'SYNC' }?// console.log('unlock') ] ]<em id="__mceDel">?</em> |
測試一下,符合evnet loop的定義,然后放開注釋,我們成功的讓loop block住了整個執(zhí)行過程,lock = false永遠(yuǎn)也沒有機會執(zhí)行!!!
(真實的調(diào)度機制遠(yuǎn)比這個復(fù)雜的多得多的,有興趣的可以看看webkit~~~的jscore的實現(xiàn)哈)
知道了原理,我們就來手動的改進這部分代碼
2.改進的代碼
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var?lock =?true setTimeout(function?() { lock =?false ???console.log('unlock') }, 5000) function?sleep() { var?i = 5000 while(i--); } var?foo = () => setTimeout(function?() { ???sleep() lock && foo() }) foo() |
?
這個版本的改進我們對while(true);做了切塊的動作,實際上這種技巧被廣泛的應(yīng)用到改善頁面體驗的方面,所以,有些人因為時序無法預(yù)知而抗拒使用settimeout這種想法是錯誤的!
http://blog.csdn.net/kongls08/article/details/6996528,
可是,如果把代碼最后的foo() 變成 foo() && console.log('wait5sdo'),
我們的代碼依然沒有成功,why
?
注意看我們標(biāo)紅的地方,如果你完成了小測驗1,就會得到和這張圖一致的順序
==同步執(zhí)行的代碼片段必然在異步之前。==
所以,無論從理論還是實際出發(fā),我們都不得不承認(rèn),在js中,把異步方法改成同步方法這個命題是水月鏡花
哦對了,最后還需要解釋一下最開始我們埋下的坑, 為什么jsonp中的async沒有生效,現(xiàn)在解釋起來真的是相當(dāng)輕松,即document.appendChild的動作是交由dom渲染線程完成的,所謂的async阻塞的是dom的解析,而非js引擎的阻塞。實際上,在async獲取資源后,與js引擎的交互依舊是push taskQueue的動作,也就是我們所說的async call
推薦閱讀: 關(guān)于dom解析請大家參考webkit技術(shù)內(nèi)幕第九章資源加載部分峰回路轉(zhuǎn)
相信很多新潮的同學(xué)已經(jīng)開始運用切了async/await語法,在下面的語法中,getAjax1和console之間的具有同步的特性
| 1 2 3 4 | async?function?() { var?data = await getAjax1() ???console.log(data) } |
講完了event loop和異步的本質(zhì),我們來重新審視一下async/await。
老天,這段代碼親手推翻了==同步執(zhí)行的代碼片段必然在異步之前。== 的黃金定律!
驚不驚喜,意不意外,這在我們的模型里如同三體里的質(zhì)子一樣的存在。我們重新審視了一遍上面的模型,實在找不到漏洞,找不到任何可以推翻的點,所以真的必須承認(rèn),async/await絕對是一個超級神奇的魔法。
到這里來看我們不得不暫時放棄前面的推論,從async/await本身來看這個問題
相信很多人都會說,async/await是CO的語法糖,CO又是generator/promise的語法糖,好的,那我們不妨去掉這層語法糖,來看看這種代碼的本質(zhì), 關(guān)于CO,讀的人太多了,我實在不好老生常談,可以看看這篇文章,咱們就直接繞過去了,這里給出一個簡易的實現(xiàn)
http://www.cnblogs.com/jiasm/p/5800210.html
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function?wrap(wait) { var?iter ?iter = wait() const f = () => { const { value } = iter.next() ???value && value.then(f) } ?f() } function?*wait() { var?p = () =>?new?Promise(resolve => { ?????setTimeout(() => resolve(), 3000) }) yield?p() ?console.log('unlock1') yield?p() ?console.log('unlock2') ?console.log('it\'s sync!!') } |
終于,我們發(fā)現(xiàn)了問題的關(guān)鍵,如果單純的看wait生成器(注意,不是普通的函數(shù)),是不是覺得非常眼熟。這就是我們最開始提出的spinlock偽代碼!!!
這個已經(jīng)被我們完完全全的否定過了,js不可能存在自旋鎖,事出反常必有妖,是的,yield和*就是表演async/await魔法的妖精。
generator和yield字面上含義。Gennerator叫做生成器,yield這塊ruby,python,js等各種語言界爭議很大,但是大多數(shù)人對于‘讓權(quán)’這個概念是認(rèn)同的(以前看到過maillist上面的爭論,但是具體的內(nèi)容已經(jīng)找不到了)
所謂讓權(quán),是指cpu在執(zhí)行時讓出使用權(quán)利,操作系統(tǒng)的角度來看就是‘掛起’原語,在eventloop的語義下,似乎是暫存起當(dāng)時正在執(zhí)行的代碼塊(在我們的eventloop里面對應(yīng)runPart),然后順序的執(zhí)行下一個程序塊。
我們可以修改eventloop來實現(xiàn)讓權(quán)機制
至此,通過修改eventloop模型固然可以解決問題,但是,這并不能被稱之為魔法。
和諧共存的世界
實際上通過babel,我們可以輕松的降級使用yield,(在es5的世界使用讓權(quán)的概念!!)
看似不可能的事情,現(xiàn)在,讓我們撿起曾經(jīng)論證過的
==同步執(zhí)行的代碼片段必然在異步之前。== 這個定理,在此基礎(chǔ)上進行進行逆否轉(zhuǎn)化
這是一個圈子里人盡皆知的話,但直到現(xiàn)在他才變得有說服力(我們繞了一個好長的圈子)
現(xiàn)在,讓我們允許使用callback,不使用generator/yield的情況下完成一個wait generator相同的功能!!!
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function?wait() { const p = () => ({value:?new?Promise(resolve => setTimeout(() => resolve(), 3000))}) let?state = { next: () => { ???????state.next = programPart return?p() } } function?programPart() { ?????console.log('unlocked1') ?????state.next = programPart2 return?p() } function?programPart2() { ?????console.log('unlocked2') ?????console.log('it\'s sync!!') return?{value: void 0} } return?state } |
?
太棒了,我們成功的完成了generator到function的轉(zhuǎn)化(雖然成本高昂),同時,這段代碼本身也解釋清楚了generator的本質(zhì),高階函數(shù),片段生成器,或者直接叫做函數(shù)生成器!這和scip上的翻譯完全一致,同時擁有自己的狀態(tài)(有限狀態(tài)機)
推薦閱讀 計算機程序的構(gòu)造和解釋 第一章generator部分小測驗3 實際上我們提供的解決方式存在缺陷,請從作用域角度談?wù)?
其實,在不知不覺中,我們已經(jīng)重新發(fā)明了計算機科學(xué)中大名鼎鼎的CPS變換
https://en.wikipedia.org/wiki/Continuation-passing_style
最后的最后,容我向大家介紹一下facebook的CPS自動變換工具--regenerator。他在我們的基礎(chǔ)上修正了作用域的缺陷,讓generator在es5的世界里自然優(yōu)雅。我們向facebook脫帽致敬!!https://github.com/facebook/regenerator
后記
同步異步 可以說是整個圈子里面最喜歡談?wù)摰膯栴},但是,談來談去,似乎絕大多數(shù)變成了所謂的‘約定俗稱’,大家意味追求新技術(shù)的同時,卻并不關(guān)心新技術(shù)是如何在老技術(shù)上傳承發(fā)展的,知其然而不知其所以然,人云亦云的寫著似是而非的js。
==技術(shù),不應(yīng)該浮躁==
PS: 最大的功勞不是CO,也不是babel。regenerator的出現(xiàn)比babel早幾個月,而且最初的實現(xiàn)是基于esprima/recast的,關(guān)于resprima/recast,國內(nèi)似乎了解的并不多,其實在babel剛剛誕生之際, esprima/esprima-fb/acron 以及recast/jstransfrom/babel-generator幾大族系圍繞著react產(chǎn)生過一場激烈的斗爭,或許將來的某一天,我會再從實現(xiàn)細(xì)節(jié)上談一談為什么babel笑到了最后~~~~
?
轉(zhuǎn)載于:https://www.cnblogs.com/gongchixin/articles/7412763.html
總結(jié)
以上是生活随笔為你收集整理的异步调用可以转化为同步调用吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈对Fragment的认识
- 下一篇: Mathematica干涉图处理