小飛是一名剛入行前端不久的新人,由於進到了某個大公司,儼然成爲了學弟學妹眼中'大神',你們遇到js問題都喜歡問他,這不,此時他的qq彈出了這樣一條消息javascript
"hi,大神在嗎?我有個問題想問,如今咱們的代碼裏面有這樣的東西,但是得不到正確的返回結果html
function getDataByAjax () { return $.ajax(...postParam) } var data = getDataByAjax() if (data) { console.log(data.info) }
"哦,你這裏是異步調用,不能直接得到返回值,你要把if語句寫到回調函數中",小飛不假思索的說到,對於一個‘專業’的fe來講,這根本不是一個問題。
「但是我但願只是改造getDataByAjax這個方法,讓後面的代碼成立。」
「研究這個沒有意義,異步是js的精髓,同步的話會阻塞js調用,超級慢的,可是你要一再堅持的話,用async:true就行了」
「不愧是大神,我回去馬上試一試,麼麼噠」前端
兩天後,她哭喪着臉登上了qq
「試了一下你的方法,可是根本行不通,哭~~」
「別急,我看看你這個postParam的參數行嗎」java
{ ... dataType: 'jsonp', async: true ... }
"這是一個jsonp請求啊,老掉牙的東西了,,jsonp請求是沒有辦法同步的"
「我知道jsonp請求的原理是經過script標籤實現的,可是,你看,script也是支持同步的呀,你看http://www.w3school.com.cn/tags/attscriptasync.asp」
「額,那多是jquery沒有實現吧,哈哈」
「大神,你能幫我實現一個jsonp的同步調用方式嘛,拜託了(星星眼)」
雖然他有點奇怪jquery爲何沒有實現,可是既然w3school的標準擺在那裏,碼兩行代碼又沒什麼,node
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') // 這句顯式聲明強調src不是按照異步方式調用的 js.setAttribute('src', url) head.appendChild(js) return result }
額,運行起來結果居然是undefined!w3cshool的文檔居然也不許,還權威呢,我看也不怎麼着,小飛暗自想到。python
「剛纔試了一下,w3school文檔上寫的有問題,這個異步屬性根本就是錯的」
「但是我剛還試過一次這個,我確認是好的呀」react
<script src="loop50000 && put('frist').js"></script> <script src="put('second').js"></script>
(有興趣的同窗能夠實現如下兩個js,而且加上async的標籤進行嘗試。)
「這個,我就搞不清楚了」,小飛訕訕的說到
對方已離線jquery
關於這個問題,相信不僅是小飛,不少人都難以解答。爲何ajax能夠作到同步,但jsonp不行,推廣到nodejs上,爲何readFile也能夠作到同步(readFileSync),但有的庫卻不行。
(至於script的async選項咱們暫時避而不談,是由於如今的知識維度暫時還不夠,可是不要着急,下文中會給出明確的解釋)
如今,讓咱們以計算機科學的角度抽象這個問題:c++
咱們是否能夠將異步代碼轉化爲同步代碼呢?(ASYNCCALL => SYNCCALL)
既然是抽象問題,那麼咱們就能夠不從工程角度/性能角度/實現語言等等等方面來看(同步比異步效率低下),每增長一個維度,複雜程度將以幾何爆炸般增加下去。git
首先,咱們來明確一點,==在計算機科學領域==同步和異步的定義
同步(英語:Synchronization),指對在一個系統中所發生的事件(event)之間進行協調,在時間上出現一致性與統一化的現象。在系統中進行同步,也被稱爲及時(in time)、同步化的(synchronous、in sync)。--摘自百度百科
異步的概念和同步相對。即時間不一致,不統一
明確了這一點,咱們能夠藉助甘特圖來表示同步和異步
其中t1和t2是同步的,t1和t3是異步的。
答案就在操做系統原理的大學教材上,咱們有自旋鎖,信號量來解決問題,僞代碼以下
spinLock () { // 自旋鎖 fork Wait 3000 unlock() //開啓一個異步線程,等待三秒後執行解鎖動做 loop until unlock // 不斷進行空循環直到解鎖動做 Put ‘unlock’ } //pv原語,當信號量爲假時當即執行下一步,同時將信號量置真 //反之將當前執行棧掛起,置入等待喚醒隊列 //uv原語,將信號量置爲假,並從等待喚醒隊列中喚醒一個執行棧 Semaphore () { pv() fork Wait 3000 uv() pv() uv() Put 'unlock' }
很好,至此均可以在操做系統原理的教材上翻到答案。因而咱們在此基礎上添加約束條件
僅僅依賴於js自己,咱們是否能夠將異步代碼轉化爲同步代碼呢?(ASYNCCALL => SYNCCALL)
帶着這個問題,咱們翻看一下jquery的源碼
https://github.com/jquery/jquery/blob/262acc6f1e0f71a3a8b786e3c421b2e645799ea0/src/ajax/xhr.js#L42
能夠看出, ajax的同步機制本質上是由XMLHttpRequest實現的,而非js原生實現。
一樣的道理,咱們再翻看一下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
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++來實現的底層同步方式。
觀察了這兩種最普遍的異步轉同步式調用,咱們發現均沒有采用js來實現。
彷佛從現象層面上來看js沒法原生支持,可是這還不夠,咱們探究在js語義下上面的自旋鎖/信號量的特性模擬實現(我知道大家必定會嗤之以鼻,==js自己就是單線程的,只是模擬了多線程的特性== 我無比贊同這句話,因此這裏用的不是實現,而是特性模擬實現),另外,因爲settimeout具備fork類似的異步執行特性,因此咱們用setitmeout暫時代替fork
1.第一個實現版本
var lock = true setTimeout(function () { lock = false }, 5000) while(lock); console.log('unlock')
咱們預期在5000ms後執行unlock語句,可是悲劇的是,整個chrome進程僵死掉了。
爲了解釋清楚這個問題,咱們讀一下阮一峯老師的event loop模型
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
看樣子我們已經清楚的瞭解了event loop這個js運行順序的本質(同步執行代碼當即執行,異步代碼入等待隊列),那麼,咱們能夠基於此給出js vm的調度實現(eventloop的一種實現),固然,我們爲了解釋自旋鎖失敗只須要模擬異步操做, 同步操做,和循環就好
//taskQueue:任務隊列 //runPart:當前正在執行的任務(同步指令集) //instruct: 正在執行的指令 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 } } } }
而後轉化咱們的第一個版本自旋鎖
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') ] ]
測試一下,符合evnet loop的定義,而後放開註釋,咱們成功的讓loop block住了整個執行過程,lock = false永遠也沒有機會執行!!!
(真實的調度機制遠比這個複雜的多得多的,有興趣的能夠看看webkit~~~的jscore的實現哈)
知道了原理,咱們就來手動的改進這部分代碼
2.改進的代碼
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);作了切塊的動做,實際上這種技巧被普遍的應用到改善頁面體驗的方面,因此,有些人由於時序沒法預知而抗拒使用settimeout這種想法是錯誤的!
http://blog.csdn.net/kongls08/article/details/6996528,
小測驗1: 改寫eventloop和taskQueue,使它支持改進後的代碼
但是,若是把代碼最後的foo() 變成 foo() && console.log('wait5sdo'),
咱們的代碼依然沒有成功,why
注意看咱們標紅的地方,若是你完成了小測驗1,就會獲得和這張圖一致的順序
==同步執行的代碼片斷必然在異步以前。==
因此,不管從理論仍是實際出發,咱們都不得不認可,在js中,把異步方法改爲同步方法這個命題是水月鏡花
哦對了,最後還須要解釋一下最開始咱們埋下的坑, 爲何jsonp中的async沒有生效,如今解釋起來真的是至關輕鬆,即document.appendChild的動做是交由dom渲染線程完成的,所謂的async阻塞的是dom的解析,而非js引擎的阻塞。實際上,在async獲取資源後,與js引擎的交互依舊是push taskQueue的動做,也就是咱們所說的async call
推薦閱讀: 關於dom解析請你們參考webkit技術內幕第九章資源加載部分
相信不少新潮的同窗已經開始運用切了async/await語法,在下面的語法中,getAjax1和console之間的具備同步的特性
async function () { var data = await getAjax1() console.log(data) }
講完了event loop和異步的本質,咱們來從新審視一下async/await。
老天,這段代碼親手推翻了==同步執行的代碼片斷必然在異步以前。== 的黃金定律!
驚不驚喜,意不意外,這在咱們的模型裏如同三體裏的質子同樣的存在。咱們從新審視了一遍上面的模型,實在找不到漏洞,找不到任何能夠推翻的點,因此真的必須認可,async/await絕對是一個超級神奇的魔法。
到這裏來看咱們不得不暫時放棄前面的推論,從async/await自己來看這個問題
相信不少人都會說,async/await是CO的語法糖,CO又是generator/promise的語法糖,好的,那咱們不妨去掉這層語法糖,來看看這種代碼的本質, 關於CO,讀的人太多了,我實在很差老生常談,能夠看看這篇文章,我們就直接繞過去了,這裏給出一個簡易的實現
http://www.cnblogs.com/jiasm/p/5800210.html
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!!') }
終於,咱們發現了問題的關鍵,若是單純的看wait生成器(注意,不是普通的函數),是否是以爲很是眼熟。這就是咱們最開始提出的spinlock僞代碼!!!
這個已經被咱們完徹底全的否認過了,js不可能存在自旋鎖,事出反常必有妖,是的,yield和*就是表演async/await魔法的妖精。
generator和yield字面上含義。Gennerator叫作生成器,yield這塊ruby,python,js等各類語言界爭議很大,可是大多數人對於‘讓權’這個概念是認同的(之前看到過maillist上面的爭論,可是具體的內容已經找不到了)
擴展閱讀---ruby元編程 閉包章節yield(ruby語義下的yield)
所謂讓權,是指cpu在執行時讓出使用權利,操做系統的角度來看就是‘掛起’原語,在eventloop的語義下,彷佛是暫存起當時正在執行的代碼塊(在咱們的eventloop裏面對應runPart),而後順序的執行下一個程序塊。
咱們能夠修改eventloop來實現讓權機制
小測驗2 修改eventloop使之支持yield原語
至此,經過修改eventloop模型當然能夠解決問題,可是,這並不能被稱之爲魔法。
實際上經過babel,咱們能夠輕鬆的降級使用yield,(在es5的世界使用讓權的概念!!)
看似不可能的事情,如今,讓咱們撿起曾經論證過的
==同步執行的代碼片斷必然在異步以前。== 這個定理,在此基礎上進行進行逆否轉化
==在異步代碼執行以後的代碼必然不是同步執行的(異步的)。==
這是一個圈子裏人盡皆知的話,但直到如今他才變得有說服力(咱們繞了一個好長的圈子)
如今,讓咱們容許使用callback,不使用generator/yield的狀況下完成一個wait generator相同的功能!!!
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的轉化(雖然成本高昂),同時,這段代碼自己也解釋清楚了generator的本質,高階函數,片斷生成器,或者直接叫作函數生成器!這和scip上的翻譯徹底一致,同時擁有本身的狀態(有限狀態機)
推薦閱讀 計算機程序的構造和解釋 第一章generator部分
小測驗3 實際上咱們提供的解決方式存在缺陷,請從做用域角度談談
其實,在不知不覺中,咱們已經從新發明了計算機科學中大名鼎鼎的CPS變換
https://en.wikipedia.org/wiki/Continuation-passing_style
最後的最後,容我向你們介紹一下facebook的CPS自動變換工具--regenerator。他在咱們的基礎上修正了做用域的缺陷,讓generator在es5的世界裏天然優雅。咱們向facebook脫帽致敬!!https://github.com/facebook/regenerator
同步異步 能夠說是整個圈子裏面最喜歡談論的問題,可是,談來談去,彷佛絕大多數變成了所謂的‘約定俗稱’,你們意味追求新技術的同時,卻並不關心新技術是如何在老技術上傳承發展的,知其然而不知其因此然,人云亦云的寫着似是而非的js。
==技術,不該該浮躁==
PS: 最大的功勞不是CO,也不是babel。regenerator的出現比babel早幾個月,並且最初的實現是基於esprima/recast的,關於resprima/recast,國內彷佛瞭解的並很少,其實在babel剛剛誕生之際, esprima/esprima-fb/acron 以及recast/jstransfrom/babel-generator幾大族系圍繞着react產生過一場激烈的鬥爭,或許未來的某一天,我會再從實現細節上談一談爲何babel笑到了最後~~~~
若是你喜歡咱們的文章,關注咱們的公衆號和咱們互動吧。