本文目錄一覽:
- 1、怎樣用JS實現非同步轉同步
- 2、JS Promise 同步和非同步代碼執行的區別
- 3、javascript同步和非同步的區別與實現方式
- 4、js執行順序+同步非同步
- 5、js 是同步執行 還是非同步執行
怎樣用JS實現非同步轉同步
源起
小飛是一名剛入行前端不久的新人,因為進到了某個大公司,儼然成為了學弟學妹眼中’大神’,大家遇到js問題都喜歡問他,這不,此時他的qq彈出了這樣一條消息
“hi,大神在嗎?我有個問題想問,現在我們的代碼裡面有這樣的東西,可是得不到正確的返回結果
1234567
function getDataByAjax () {return $.ajax(…postParam)}var data = getDataByAjax()if (data) { console.log(data.info)}
“哦,你這裡是非同步調用,不能直接獲得返回值,你要把if語句寫到回調函數中”,小飛不假思索的說到,對於一個『專業』的fe來說,這根本不是一個問題。
「可是我希望只是改造getDataByAjax這個方法,讓後面的代碼成立。」
「研究這個沒有意義,非同步是js的精髓,同步的話會阻塞js調用,超級慢的,但是你要一再堅持的話,用async:true就好了」
「不愧是大神,我回去立刻試一試,么么噠」
兩天後,她哭喪著臉登上了qq
「試了一下你的方法,但是根本行不通,哭~~」
「別急,我看看你這個postParam的參數行嗎」
123456
{… dataType: ‘jsonp’,async: true…}
“這是一個jsonp請求啊,老掉牙的東西了,,jsonp請求是沒有辦法同步的”
「我知道jsonp請求的原理是通過script標籤實現的,但是,你看,script也是支持同步的呀,你看tags/attscriptasync.asp」
「額,那可能是jquery沒有實現吧,哈哈」
「大神,你能幫我實現一個jsonp的同步調用方式嘛,拜託了(星星眼)」
雖然他有點奇怪jquery為什麼沒有實現,但是既然w3school的標準擺在那裡,碼兩行代碼又沒什麼,
1234567891011121314
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文檔上寫的有問題,這個非同步屬性根本就是錯的」
「可是我剛還試過一次這個,我確認是好的呀」
12
script src=”loop50000 put(‘frist’).js”/scriptscript src=”put(‘second’).js”/script
(有興趣的同學可以實現以下兩個js,並且加上async的標籤進行嘗試。)
「這個,我就搞不清楚了」,小飛訕訕的說到
對方已離線
抽象
關於這個問題,相信不只是小飛,很多人都難以解答。為什麼ajax可以做到同步,但jsonp不行,推廣到nodejs上,為什麼readFile也可以做到同步(readFileSync),但有的庫卻不行。
(至於script的async選項我們暫時避而不談,是因為現在的知識維度暫時還不夠,但是不要著急,下文中會給出明確的解釋)
現在,讓我們以計算機科學的角度抽象這個問題:
我們是否可以將非同步代碼轉化為同步代碼呢?(ASYNCCALL = SYNCCALL)
既然是抽象問題,那麼我們就可以不從工程角度/性能角度/實現語言等等等方面來看(同步比非同步效率低下),每增加一個維度,複雜程度將以幾何爆炸般增長下去。
首先,我們來明確一點,==在計算機科學領域==同步和非同步的定義
同步(英語:Synchronization),指對在一個系統中所發生的事件(event)之間進行協調,在時間上出現一致性與統一化的現象。在系統中進行同步,也被稱為及時(in time)、同步化的(synchronous、in sync)。–摘自百度百科
非同步的概念和同步相對。即時間不一致,不統一
明確了這一點,我們可以藉助甘特圖來表示同步和非同步
其中t1和t2是同步的,t1和t3是非同步的。
答案就在操作系統原理的大學教材上,我們有自旋鎖,信號量來解決問題,偽代碼如下
1234567891011121314151617
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的源碼
src/ajax/xhr.js#L42
可以看出, ajax的同步機制本質上是由XMLHttpRequest實現的,而非js原生實現。
同樣的道理,我們再翻看一下nodejs的源碼
lib/fs.js#L550
從readFileSync-tryReadSync-readSync一路追下去,會追到一個c++ binding, node_file.cc#L1167
123456
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.第一個實現版本
1234567
var lock = truesetTimeout(function () {lock = false}, 5000) while(lock);console.log(‘unlock’)
我們預期在5000ms後執行unlock語句,但是悲劇的是,整個chrome進程僵死掉了。
為了解釋清楚這個問題,我們讀一下阮一峰老師的event loop模型
event-loop.html
看樣子咱們已經清楚的了解了event loop這個js運行順序的本質(同步執行代碼立即執行,非同步代碼入等待隊列),那麼,我們可以基於此給出js vm的調度實現(eventloop的一種實現),當然,咱們為了解釋自旋鎖失敗只需要模擬非同步操作, 同步操作,和循環就好
123456789101112131415161718192021222324
//taskQueue:任務隊列//runPart:當前正在執行的任務(同步指令集)//instruct: 正在執行的指令 function eventloop (taskQueue) {while(runPart = taskQueue.shift()) {while(instruct = runPart.shift()) {const { type, act, codePart } = instructswitch(type) {case ‘SYNC’: console.log(act)if (act === ‘loop’) runPart.unshift({ act: ‘loop’, type: ‘SYNC’})breakcase ‘ASYNC’: taskQueue.push(codePart)break}}}}
然後轉化我們的第一個版本自旋鎖
1234567891011121314151617181920
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住了整個執行過程,lock = false永遠也沒有機會執行!!!
(真實的調度機制遠比這個複雜的多得多的,有興趣的可以看看webkit~~~的jscore的實現哈)
知道了原理,我們就來手動的改進這部分代碼
2.改進的代碼
12345678910111213141516
var lock = truesetTimeout(function () {lock = false console.log(‘unlock’)}, 5000) function sleep() {var i = 5000while(i–);} var foo = () = setTimeout(function () { sleep()lock foo()})foo()
這個版本的改進我們對while(true);做了切塊的動作,實際上這種技巧被廣泛的應用到改善頁面體驗的方面,所以,有些人因為時序無法預知而抗拒使用settimeout這種想法是錯誤的!
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之間的具有同步的特性
1234
async function () {var data = await getAjax1() console.log(data)}
講完了event loop和非同步的本質,我們來重新審視一下async/await。
老天,這段代碼親手推翻了==同步執行的代碼片段必然在非同步之前。== 的黃金定律!
驚不驚喜,意不意外,這在我們的模型里如同三體里的質子一樣的存在。我們重新審視了一遍上面的模型,實在找不到漏洞,找不到任何可以推翻的點,所以真的必須承認,async/await絕對是一個超級神奇的魔法。
到這裡來看我們不得不暫時放棄前面的推論,從async/await本身來看這個問題
相信很多人都會說,async/await是CO的語法糖,CO又是generator/promise的語法糖,好的,那我們不妨去掉這層語法糖,來看看這種代碼的本質, 關於CO,讀的人太多了,我實在不好老生常談,可以看看這篇文章,咱們就直接繞過去了,這裡給出一個簡易的實現
/5800210.html
1234567891011121314151617181920
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相同的功能!!!
1234567891011121314151617181920
function wait() {const p = () = ({value: new Promise(resolve = setTimeout(() = resolve(), 3000))})let state = {next: () = { state.next = programPartreturn p()}}function programPart() { console.log(‘unlocked1’) state.next = programPart2return p()}function programPart2() { console.log(‘unlocked2’) console.log(‘it\’s sync!!’)return {value: void 0}}return state}
太棒了,我們成功的完成了generator到function的轉化(雖然成本高昂),同時,這段代碼本身也解釋清楚了generator的本質,高階函數,片段生成器,或者直接叫做函數生成器!這和scip上的翻譯完全一致,同時擁有自己的狀態(有限狀態機)
推薦閱讀 計算機程序的構造和解釋 第一章generator部分
小測驗3 實際上我們提供的解決方式存在缺陷,請從作用域角度談談
其實,在不知不覺中,我們已經重新發明了計算機科學中大名鼎鼎的CPS變換
Continuation-passing_style
最後的最後,容我向大家介紹一下facebook的CPS自動變換工具–regenerator。他在我們的基礎上修正了作用域的缺陷,讓generator在es5的世界裡自然優雅。我們向facebook脫帽致敬!!egenerator
後記
同步非同步 可以說是整個圈子裡面最喜歡談論的問題,但是,談來談去,似乎絕大多數變成了所謂的『約定俗稱』,大家意味追求新技術的同時,卻並不關心新技術是如何在老技術上傳承發展的,知其然而不知其所以然,人云亦云的寫著似是而非的js。
==技術,不應該浮躁==
PS: 最大的功勞不是CO,也不是babel。regenerator的出現比babel早幾個月,而且最初的實現是基於esprima/recast的,關於resprima/recast,國內似乎了解的並不多,其實在babel剛剛誕生之際, esprima/esprima-fb/acron 以及recast/jstransfrom/babel-generator幾大族系圍繞著react產生過一場激烈的鬥爭,或許將來的某一天,我會再從實現細節上談一談為什麼babel笑到了最後~~~~
JS Promise 同步和非同步代碼執行的區別
同步的話,必須這個操作完了才會執行下一步,在等待期間瀏覽器會掛起不能執行任何接下來的js代碼;非同步則是【告訴】瀏覽器去做,【告訴】是一瞬間的事情,然後就繼續執行下一步了,等到結果返回來了,瀏覽器會通知js執行相應的回調。
javascript同步和非同步的區別與實現方式
javascript語言是單線程機制。所謂單線程就是按次序執行,執行完一個任務再執行下一個。
對於瀏覽器來說,也就是無法在渲染頁面的同時執行代碼。
單線程機制的優點在於實現起來較為簡單,運行環境相對簡單。缺點在於,如果中間有任務需要響應時間過長,經常會導致
頁面載入錯誤或者瀏覽器無響應的狀況。這就是所謂的「同步模式」,程序執行順序與任務排列順序一致。對於瀏覽器來說,
同步模式效率較低,耗時長的任務都應該使用非同步模式;而在伺服器端,非同步模式則是唯一的模式,如果採用同步模式個人認為
伺服器很快就會出現12306在高峰期的表現。。。。
非同步模式的四種方式:
1.回調函數callback
所謂回調函數,就是將函數作為參數傳到需要回調的函數內部再執行。
典型的例子就是發送ajax請求。例如:
$.ajax({
async: false,
cache: false,
dataType: ‘json’,
url: “url”,
success: function(data) {
console.log(‘success’);
},
error: function(data) {
console.log(‘error’);
}
})
當發送ajax請求後,等待回應的過程不會堵塞程序運行,耗時的操作相當於延後執行。
回調函數的優點在於簡單,容易理解,但是可讀性較差,耦合度較高,不易於維護。
2.事件驅動
javascript可以稱之為是基於對象的語言,而基於對象的基本特徵就是事件驅動(Event-Driven)。
事件驅動,指的是由滑鼠和熱鍵的動作引發的一連串的程序操作。
例如,為頁面上的某個
$(‘#btn’).onclick(function(){
console.log(‘click button’);
});
綁定事件相當於在元素上進行監聽,是否執行註冊的事件代碼取決於事件是否發生。
優點在於容易理解,一個元素上可以綁定多個事件,有利於實現模塊化;但是缺點在於稱為事件驅動的模型後,流程不清晰。
3.發布/訂閱
發布訂閱模式(publish-subscribe pattern)又稱為觀察者模式(Observer pattern)。
該模式中,有兩類對象:觀察者和目標對象。目標對象中存在著一份觀察者的列表,當目標對象
的狀態發生改變時,主動通知觀察者,從而建立一種發布/訂閱的關係。
jquery有相關的插件,在這不是重點不細說了。。。。回頭寫個實現貼上來
4.promise模式
promise對象是CommonJS工作組提供的一種規範,用於非同步編程的統一介面。
promise對象通常實現一種then的方法,用來在註冊狀態發生改變時作為對應的回調函數。
promise模式在任何時刻都處於以下三種狀態之一:未完成(unfulfilled)、已完成(resolved)和拒絕(rejected)。以CommonJS
Promise/A
標準為例,promise對象上的then方法負責添加針對已完成和拒絕狀態下的處理函數。then方法會返回另一個promise對象,以便於形成promise管道,這種返回promise對象的方式能夠支持開發人員把非同步操作串聯起來,如then(resolvedHandler,
rejectedHandler); 。resolvedHandler
回調函數在promise對象進入完成狀態時會觸發,並傳遞結果;rejectedHandler函數會在拒絕狀態下調用。
Jquery在1.5的版本中引入了一個新的概念叫Deferred,就是CommonJS promise A標準的一種衍生。可以在jQuery中創建
$.Deferref的對象。同時也對發送ajax請求以及數據類型有了新的修改,參考JQuery API。
除了以上四種,javascript中還可以利用各種函數模擬非同步方式,更有詭異的諸如用同步調用非同步的case
只能用team里同事形容java和javascript的一句話作為結尾:
「寫java像在高速路上開車,寫javascript像在草原上開車」————-以此來形容javascript這種無類型的語言有多自由
but,如果草原上都是坑。
js執行順序+同步非同步
按照js同步執行的順序,函數調用會首先執行for循環,循環5次開啟了5個延遲器,延時器內部的回調函數將會非同步執行,會在延時1s後進入消息隊列等待執行。循環了5次,所以此時i的值為5,然後同步執行console.log列印5,第一次同步執行結束,然後開始執行消息隊列的非同步任務,列印5個5,中間的undefined是函數調用無返回值返回的。
執行順序和第一題相同,不同的是延時器被包裹在了一個立即執行函數內容,並把每一次循環的i作為參數傳入,此時循環內部的函數形成了一個私有作用域,每一次的i變數被獨立保存了起來,因此每一次循環的i的值都會被列印出來。
延時器內部回調函數是非同步函數,將在延時結束後,進入消息隊列等待執行,因此同步的console.log(“CCCC”)最優先執行,然後延時0ms的延時器的回調先進入消息隊列,1000ms後第一個延時器的回調再進入消息隊列等待執行,因此先執行0ms的回調列印DDDD,再執行1000ms的回調列印BBBB。
這裡與上一題不同的是中間多了一個執行3s的同步while循環,因此執行順序是這樣的:
第一個延時器延時1s後內部非同步回調函數進入消息隊列等待執行,
等待while循環3s後列印”CCCC”,
循環結束後第一個延時器內部的回調已經進入消息隊列第一個執行位置等待執行。
第二個延時器延時0s後內部非同步回調函數進入消息隊列等待執行,延時結束後排到第一個延時器的回調函數後面,
因此非同步隊里內部先列印”BBBB”,再列印”DDDD”
js 是同步執行 還是非同步執行
你好,js是同步執行的,一個簡單示例解釋,
for(var i = 0;i 10;i++)
console.log(i)
for(var i = 10;i 20;i++)
console.log(i)
以上兩個for循環,第一個列印1-10,第二個列印10-20,結果是1-20按順序輸出
js中代碼是同步執行的,只有在ajax的情況下,會導致代碼執行順序改變,是因為ajax的請求時間導致
希望可以幫助到你
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/289257.html