天天看點

似是而非的JS - 異步調用可以轉化為同步調用嗎?

源起

小飛是一名剛入行前端不久的新人,因為進到了某個大公司,俨然成為了學弟學妹眼中'大神',大家遇到js問題都喜歡問他,這不,此時他的qq彈出了這樣一條消息

"hi,大神在嗎?我有個問題想問,現在我們的代碼裡面有這樣的東西,可是得不到正确的傳回結果

function getDataByAjax () {
return $.ajax(...postParam)
}
var data = getDataByAjax()
if (data) {
   console.log(data.info)
}
      

  

"哦,你這裡是異步調用,不能直接獲得傳回值,你要把if語句寫到回調函數中",小飛不假思索的說到,對于一個‘專業’的fe來說,這根本不是一個問題。

“可是我希望隻是改造getDataByAjax這個方法,讓後面的代碼成立。”

“研究這個沒有意義,異步是js的精髓,同步的話會阻塞js調用,超級慢的,但是你要一再堅持的話,用async:true就好了”

“不愧是大神,我回去立刻試一試,麼麼哒”

兩天後,她哭喪着臉登上了qq

“試了一下你的方法,但是根本行不通,哭~~”

“别急,我看看你這個postParam的參數行嗎”

{
...
   dataType: 'jsonp',
async: true
...
}
      

"這是一個jsonp請求啊,老掉牙的東西了,,jsonp請求是沒有辦法同步的"

“我知道jsonp請求的原理是通過script标簽實作的,但是,你看,script也是支援同步的呀,你看http://www.w3school.com.cn/tags/attscriptasync.asp”

“額,那可能是jquery沒有實作吧,哈哈”

“大神,你能幫我實作一個jsonp的同步調用方式嘛,拜托了(星星眼)”

雖然他有點奇怪jquery為什麼沒有實作,但是既然w3school的标準擺在那裡,碼兩行代碼又沒什麼,

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的文檔竟然也不準,還權威呢,我看也不怎麼着,小飛暗自想到。

“剛才試了一下,w3school文檔上寫的有問題,這個異步屬性根本就是錯的”

“可是我剛還試過一次這個,我确認是好的呀”

<script src="loop50000 && put('frist').js"></script>
<script src="put('second').js"></script>
      

(有興趣的同學可以實作以下兩個js,并且加上async的标簽進行嘗試。)

“這個,我就搞不清楚了”,小飛讪讪的說到

對方已離線

抽象

關于這個問題,相信不隻是小飛,很多人都難以解答。為什麼ajax可以做到同步,但jsonp不行,推廣到nodejs上,為什麼readFile也可以做到同步(readFileSync),但有的庫卻不行。

(至于script的async選項我們暫時避而不談,是因為現在的知識次元暫時還不夠,但是不要着急,下文中會給出明确的解釋)

現在,讓我們以計算機科學的角度抽象這個問題:

我們是否可以将異步代碼轉化為同步代碼呢?(ASYNCCALL => SYNCCALL)

既然是抽象問題,那麼我們就可以不從工程角度/性能角度/實作語言等等等方面來看(同步比異步效率低下),每增加一個次元,複雜程度将以幾何爆炸般增長下去。

首先,我們來明确一點,==在計算機科學領域==同步和異步的定義

同步(英語:Synchronization),指對在一個系統中所發生的事件(event)之間進行協調,在時間上出現一緻性與統一化的現象。在系統中進行同步,也被稱為及時(in time)、同步化的(synchronous、in sync)。--摘自百度百科

異步的概念和同步相對。即時間不一緻,不統一

明确了這一點,我們可以借助甘特圖來表示同步和異步

似是而非的JS - 異步調用可以轉化為同步調用嗎?

其中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

似是而非的JS - 異步調用可以轉化為同步調用嗎?

注意看我們标紅的地方,如果你完成了小測驗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笑到了最後~~~~

如果你喜歡我們的文章,關注我們的公衆号和我們互動吧。

似是而非的JS - 異步調用可以轉化為同步調用嗎?

我們是轉轉FE團隊,歡迎大家關注公衆号 大轉轉FE 。更多的了解我們

我們是轉轉FE團隊,歡迎大家關注公衆号 大轉轉FE 。更多的了解我們。官網 http://zzfed.com

繼續閱讀