深入理解 JavaScript 非同步系列(1)——基礎
前言
http://www.cnblogs.com/wangfupeng1988/p/6513070.html 2014年秋季寫完了《深入理解javascript原型和閉包系列》,已經幫助過很多人走出了 js 原型、作用域、閉包的困惑,至今仍能經常受到好評的留言。
很早之前我就總結了JS三座大山這個概念(雖然沒有到處宣揚),前兩座(原型、作用域)已經基本講明白,而第三座(非同步)也應該做一個總結。
於是,2017年初春,我花費大約一週的業餘時間來對 JS 非同步做一個完整的總結,和各位同學共勉共進步!
第一部分,什麼是非同步
提醒:如果你是初學 js 的同學,尚未有太多專案經驗和基礎知識,請就此打住,不要看這篇教程
我思考問題、寫文章一般都不按討論出牌,別人寫過的東西我不會再照著抄一遍。因此,後面所有的內容,都是我看了許多資料之後,個人重新思考提煉總結出來的,這肯定不能算是初級教程。
如果你是已有 js 開發經驗,並瞭解非同步的基礎知識,到這裡來想深入瞭解一下Promise
Generator
和async-awati
,那就太好了,非常歡迎。
本節內容概述
- JS 為何會有非同步
- 非同步的實現原理是什麼
- 常用的非同步操作有哪些
JS 為何會有非同步
首先記住一句話 —— JS 是單執行緒的語言,所謂“單執行緒”就是一根筋,對於拿到的程式,一行一行的執行,上面的執行為完成,就傻傻的等著。例如
var i, t = Date.now()for (i = 0; i < 100000000; i++) { } console.log(Date.now() - t) // 250 (chrome瀏覽器)
上面的程式花費 250ms 的時間執行完成,執行過程中就會有卡頓,其他的事兒就先撂一邊不管了。
執行程式這樣沒有問題,但是對於 JS 最初使用的環境 ———— 瀏覽器客戶端 ———— 就不一樣了。因此在瀏覽器端執行的 js ,可能會有大量的網路請求,而一個網路資源啥時候返回,這個時間是不可預估的。這種情況也要傻傻的等著、卡頓著、啥都不做嗎?———— 那肯定不行。
因此,JS 對於這種場景就設計了非同步 ———— 即,發起一個網路請求,就先不管這邊了,先幹其他事兒,網路請求啥時候返回結果,到時候再說。這樣就能保證一個網頁的流程執行。
非同步的實現原理
先看一段比較常見的程式碼
var ajax = $.ajax({ url: '/data/data1.json', success: function () { console.log('success') } })
上面程式碼中$.ajax()
需要傳入兩個引數進去,url
和success
,其中url
是請求的路由,success
是一個函式。這個函式傳遞過去不會立即執行,而是等著請求成功之後才能執行。對於這種傳遞過去不執行,等出來結果之後再執行的函式,叫做callback
,即回撥函式
再看一段更加能說明回撥函式的 nodejs 程式碼。和上面程式碼基本一樣,唯一區別就是:上面程式碼時網路請求,而下面程式碼時 IO 操作。
var fs = require('fs') fs.readFile('data1.json', (err, data) => { console.log(data.toString()) })
從上面兩個 demo 看來,實現非同步的最核心原理,就是將callback
作為引數傳遞給非同步執行函式,當有結果返回之後再觸發 callback
執行,就是如此簡單!
常用的非同步操作
開發中比較常用的非同步操作有:
- 網路請求,如
ajax
http.get
- IO 操作,如
readFile
readdir
- 定時函式,如
setTimeout
setInterval
最後,請思考,事件繫結是不是也是非同步操作?例如$btn.on('click', function() {...})
。這個問題很有意思,我會再後面的章節經過分析之後給出答案,各位先自己想一下。
第二部分,非同步和 event-loop
提到非同步,就必須提 event-loop 。event-loop 中文翻譯叫做“事件輪詢”,它是能體現出單執行緒中非同步操作是如何被執行的。
首先,,只有不到半個小時的時間,但是將的非常詳細。如果那個連結失效,訪問這裡(密碼: xx9f)
其次,再結合阮一峰老師的《什麼是event loop》一起看一下。將這兩個看完就基本瞭解 event loop 了
最後,event-loop 是一塊內容比較獨立的技術性知識,它是什麼樣子就是什麼樣子,講解起來可變通性非常小。因此,本節說一下我對 event-loop 的理解和體會
本節內容概述
- 舉例說明
- 核心概念
- 思考兩個問題
舉例說明
給出一段簡單的 js 程式碼,並用比較通俗、簡單的說法介紹一下執行過程。詳細過程還需各位去看視訊,因為我沒必要把半小時的視訊都寫到這裡。
console.log('line 1') setTimeout(console.log, 1000, 'line 2') console.log('line 3')
以上一共三行程式碼,該程式被執行的時候,會依次挨行執行
- 第一步,執行第一行,將結果
line 1
打印出來 - 第二步,執行第二行,注意此時會將這個操作暫時儲存到其他地方,因為
setTimeout
是一個非同步執行操作。 - 第三步,執行第三行,將結果
line 3
打印出出來 - 第四步,等待最後一行程式(一共三行)都全部執行完了,然後立馬實時檢視剛才暫存的非同步操作有沒有。如果有可執行的,就立即拿到出來繼續執行。
- 第五步,執行完畢之後,再實時檢視暫存位置中是否還有未執行的非同步回撥。
以上只拿了setTimeout
舉例子,但是對於網路請求、IO操作、事件繫結道理都是一樣的。如果我講的簡單例子你還是看不懂,一定要去看文章最初提到的《what the hack is event loop》視訊,重要重要!!!
思考三個問題
第一題,以下程式碼的輸出順序是什麼
setTimeout(console.log, 0, 'a') console.log('b') console.log('c')
答案是b c a
,有疑問的需要再去看上面的介紹或者那個視訊。
第二題,以下程式碼中,最後輸出的結果是否是 500
var i, t = Date.now() for (i = 0; i < 100000000; i++) { } function fn() { console.log(Date.now() - t) // 輸出多少??? } setTimeout(fn, 500)
答案是大於 500ms ,因為 for 函式需要花費一些時間,等 for 執行完之後再開始計算 500ms 之後執行 fn
第三題,事件繫結是不是非同步操作?
這個問題大家根據 event-loop 的講解和視訊來思考,我們下一節再給出解答。
第三部分,事件繫結算不算非同步?
如果你認真看了上一節的 event-loop 的,你會發現原來事件繫結和非同步操作的實現機制是一樣的,那麼事件繫結是不是就是非同步操作呢?(宣告一下,這裡說的事件繫結是如下程式碼的形式)
$btn.on('click', function (e) { console.log('你點選了按鈕') })
PS:這個問題貌似沒有加過有人討論或者發起討論,但是當我瞭解了 event-loop 之後,我就發現這兩者有很大聯絡,很早就像討論一下這個話題。不知道哪位同仁跟我有一樣的想法?
本節內容概述
- 共同之處
- 不同之處
- 我的觀點
共同之處
從技術實現以及書寫方法上來講,他們是一樣的。例如事件繫結和 IO 操作的寫法基本相同
$btn.on('click', function (e) { console.log('你點選了按鈕') }) fs.readFile('data1.json', function (err, data) { // 獲取資料 })
最終執行的方式也基本一樣,都通過 evet-loop 執行。
不同之處
在我看來至少有兩處不同。
第一,event-loop 執行時,呼叫的源不一樣。非同步操作是系統自動呼叫,無論是setTimeout
時間到了還是$.ajax
請求返回了,系統會自動呼叫。而事件繫結就需要使用者手動觸發
第二,從設計上來將,事件繫結有著明顯的“訂閱-釋出”的設計模式,而非同步操作卻沒有。
我的觀點
我個人看程式碼比較偏重設計,一個東西是什麼要看它是未什麼而設計的。因此,我傾向於事件繫結不是非同步操作。雖然它也是通過 event-loop 實現呼叫的,但是它的設計目錄卻和非同步操作完全不一樣。
其實,事件繫結在 js 中扮演著非常重要的角色,各個地方都會用到事件繫結的形式。例如 web 頁面監控滑鼠、鍵盤,以及 nodejs 中的 EventEmitter
應用非常廣泛(特別是涉及到資料流時)。而事件繫結被應用到非常廣泛,卻沒有發生像非同步操作帶來的程式邏輯問題,反而大家用的非常開心————這又一個兩者不一樣的例證。
如果你覺得我的觀點有問題,也可以大膽提出自己的建議和意見,發表出來!說對說錯都無所謂,也不會扣你落戶積分,只要能自圓其說就是好的。