JS的非同步世界
前言
JS的非同步由來已久,各種非同步概念也早早堆在開發者面前。可現實程式碼中,仍然充斥了各種因非同步順序處理不當的bug,或因不好好思考,或因不瞭解真相。今天,就特來再次好好探索一番JS的非同步世界。
01 非同步的由來--單執行緒
上世紀末,網際網路仍處於極慢速時代,穿梭於客戶端與服務端的請求,對於時間的耗費是如此的奢侈。而即將面世的LiveScript,便被網景公司考慮同時在瀏覽器和服務端使用,在瀏覽器端對錶單進行校驗,從而提高表單提交效率。為了將這一指令碼語言推向市場,網景與sun聯合開發,最終以Java冠名為JavaScript。
剛面世的JavaScript,是為網頁設計人員準備的,不需要太複雜的語言設計,能簡單上手,自然就是最好的。
於是,單執行緒,弱型別,一開始就成為了JavaScript的基因。而其中的單執行緒,便是最戲劇性的存在,Ryan Dahl因為JavaScript是單執行緒語言,從而選擇了js開發了輕量級伺服器(nodejs),使得js從瀏覽器端延伸到伺服器。隨著JS開發隊伍和程式複雜度的同步發展,非同步處理成為了JS程式的重中之重。
02 JS是一個充滿非同步的世界
先來匯入幾個非同步的常見場景
dom使用者輸入響應
ducument.addEventListener('click', function(){})
Ajax
$.ajax(<url>, function() {})
定時/延時
setTimeout(function() {}, 1000) setInterval(function() {}, 1000)
檔案讀取
var reader = new FileReader(); reader.readAsDataURL(file); reader.onload = function() {}
以上的場景基本有個共同特性,耗時!
舉個栗子,我們去銀行取錢,當人很多時,如果還是排隊模式,會耗費很多時間(同步模式)。於是設立了取號機,取了號,不用排隊,在一旁坐著,安心開啟電腦寫個文件,等叫號後再去辦業務(非同步模式)。
同理,由於單執行緒的特性,當JS應用越來越複雜,耗時的程式如果以同步來進行,就會阻塞js的單執行緒,如大水衝過狹窄的河道,勢必決堤。那JS是怎麼開拓導流渠道的呢?其實在js的單執行緒(主執行緒)背後,規律的運行了很多執行緒:
- dom事件處理執行緒
- http請求執行緒
- 定時器執行緒
- ...
這些執行緒就充當了JS大江的小河道,當短時有大流量時,接納吸收,將過濾處理後的正常水流,再匯入JS主幹道。
與其說JS是單執行緒,不如說JS是有著自動化多執行緒處理的主執行緒。無需手動編碼介入新開執行緒,切換執行緒,訊息同步等冗繁的處理。專用執行緒會接管相關任務,並將處理結果送回主執行緒進行順序處理。
說到這裡稍微提一下web worker,雖然是自定義的多執行緒,最終還是子執行緒地位,仍舊將處理完成的結果以回撥函式方式匯入到主執行緒進行非同步處理。
03 非同步處理一般流程
先看以下程式碼,非同步模式開始了
var img = new Image() var imgLoadCallback = function() {} img.src = 'http://????' img.onload = callback
“http君,麻煩幫取一個圖片資料,好了後交給imgLoadCallback君。” — js主執行緒老大
“任務收到,您先忙,圖片請求交給我了,好了之後我叫imgLoadCallback君到休息室排隊,您空了通知下 Event Loop巡檢官。” — http請求執行緒
img.src = 'http://????' img.onload = imgLoadCallback
“圖片已取到,imgLoadCallback君去休息室排隊等候吧!” — http請求執行緒
imgLoadCallback入棧JS任務佇列
“剛好忙完手上的事情了,Event Loop君,幫看下休息室有沒有人排隊” — JS主執行緒老大
“老大,已把等候者imgLoadCallback叫過來處理任務” — Event Loop巡檢官
執行imgLoadCallback
“事情都交給合適的人去辦了,突然就清閒下來了,老大就是要這樣當啊,嘿嘿嘿… Event Loop君,定時看下休息室有沒有人排隊吧… ” — JS主執行緒老大
JS主執行緒通過Event Loop讀取任務佇列
講完故事,再來看這張非同步示意圖,是否能理解了?

image
04 回撥處理工具的進化
從前面的篇章已經能看出來了,非同步處理的結果是通過回撥放置到任務佇列轉接到主執行緒中的。
北京猿人刀跟火種,這麼寫非同步回撥,看上去也能令人接受。
$.ajax( url: '自家香蕉樹林', data: { picker: '猴子A' }, success: function(data) { $.ajax( url: '隔壁老孫家桃林', data: { exchanges: data.香蕉, buyer: '猴子A' }, success: function(data) { console.log('向本猴王進貢', data.桃子) } ) } )
進化成人類後交易過程變的複雜了,於是就變成回撥地獄,傳說中的callback hell
$.ajax( url: '自家香蕉樹林', data: { picker: '老王' }, success: function(data) { $.ajax( url: '集市販賣', data: { goods: data.香蕉, seller: '老王' }, success: function(data) { $.ajax( url: '隔壁老李桃子鋪', data: { exchanges: data.錢, buyer: '老王' }, success: function(data) { console.log('向本王進貢', data.桃子) } ) } ) } )
於是發明了鐵器promise,解決回撥地獄之痛
$.ajax( url: '自家香蕉樹林', data: { picker: '老王' } ) .then(function(data) { return $.ajax( url: '集市販賣', data: { goods: data.香蕉, seller: '老王' } ) }) .then(function(data) { return $.ajax( url: '隔壁老李桃子鋪', data: { exchanges: data.錢, buyer: '老王' } ) }) .then(function(data) { console.log('向本王進貢', data.桃子) })
關於promise的升級版async、await,本篇不多說了,理念上基本一致。
繼續...
這下一次命令,只會來供給本王一次桃子,每次都要發令,好麻煩,得下個令讓老王每天去賣香蕉買桃子,給我月供100個,於是就發生了以下的故事
var contributeTime; setInterval(function(){ $.ajax( url: '自家香蕉樹林', data: { picker: '老王' } ) .then(function(data) { return $.ajax( url: '集市販賣', data: { goods: data.香蕉, seller: '老王' } ) }) .then(function(data) { return $.ajax( url: '隔壁老李桃子鋪', data: { exchanges: data.錢, buyer: '老王' } ) }) .then(function(data) { var currentTime = new Date().getTime(); if (!contributeTime || (currentTime - contributeTime > '月')) { console.log('向本王進貢', [data.桃子,…]); //length=100 currentTime = contributeTime; } }) }, '天')
這過程,好像也太不優雅了點。
ReactX的JS版,RxJs來了,將非同步看作為單點,將其擴充套件了時間線,作為流來處理。所以對於一次又一次的進貢,都可進行時序管理,於是整個過程變成這樣:
import { ajax } from 'rxjs/ajax'; //此處特別寫引入,目的為不與jquery.ajax混淆 import { interval } from 'rxjs'; const ob = interval('天'); const peachPay = ob .pipe(switchMap(x => ajax.post('自家香蕉樹林', {picker: '老王'}))) .pipe(switchMap(data => ajax.post('集市販賣', {seller: '老王', goods: data.香蕉}))) .pipe(switchMap(data => ajax.post('隔壁老李桃子鋪', {buyer: '老王', exchanges: data.錢}))) .pipe(throttle(data => interval('月'))) .subscribe(data => console.log(`每月收到月供:${data.桃子.length}個${data.桃子}`));
整個過程順著管道不斷變換處理,就是一條全自動流水線!然鵝,然鵝,並一定每月就能供出100個桃子啊,萬一遇到農災,或者經濟蕭條…
以上例子僅提供思路,且讀且珍重!
05 比工具更重要的,是理解
前端開發中,諸多剪不斷理還亂的偶現bug來源於非同步處理的順序混亂。即便是非同步處理工具越來越先進,由於程式碼層面的順序和真實執行順序的不一致,也還是容易一不小心犯錯誤。
非同步處理工具不是萬能的,還是需不斷將非同步原理內化入思維模式中,種碼的時候,就需清晰的知道該段程式碼會什麼時候結出果實。
再注:以上程式碼未經執行驗證,僅示意流程與思路。望各路大神多多包涵!如有思路上都提供錯誤的,求拖出去打板子!