<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文是個人在實際開發和學習中對nodejs的一些理解,現整理出來方便日後查閱,如果能給您啟發將不勝榮幸。
I/O:即 Input / Output,一個系統的輸入和輸出。
一個系統可以理解為一個個體,比如說一個人,你說話就是輸出,你聽就是輸入。
阻塞 I/O 與非阻塞 I/O 的區別就在於系統接收輸入再到輸出期間,能不能接收其他輸入。
下面以兩個例子來說明什麼是阻塞 I/O 和非阻塞 I/O:
打飯
首先我們要確定一個系統的範圍,在這個例子中食堂阿姨和餐廳的服務生看成是一個系統,輸入就是點菜,輸出就是端菜。
那麼在點菜和端菜之間能不能接受其他人的點菜,就可以判斷是阻塞I/O還是非阻塞I/O。
對於食堂阿姨,他在點菜的時候,是不能幫其他同學點菜的,只有這個同學點完菜端菜走了之後,才能接受下一個同學的點菜,所以食堂阿姨是阻塞I/O。
對於餐廳服務員,他可以在點完菜以後,這個客人端菜之前是可以服務下一位客人的,所以服務員是非阻塞I/O。
做家務
在洗衣服的時候,是不需要等著洗衣機旁邊的,這個時候可以去掃地和整理書桌,當整理完書桌後衣服也洗好了,這個時候去晾衣服,那麼總共只需要25分鐘。
洗衣服其實就是一個非阻塞I/O,在把衣服扔進洗衣機和洗完衣服期間,你是可以幹其他事情的。
非阻塞I/O之所以能提升效能,是因為它可以把不必要的等待給節省掉。
理解非阻塞I/O的要點在於:
nodejs的非阻塞 I/O 是怎麼體現的呢?前面說過理解非阻塞 I/O 的一個重要點是先確定一個系統邊界,nodejs的系統邊界就是主執行緒。
如果下面的架構圖按照執行緒的維護劃分,左邊虛線部分是nodejs執行緒,右邊虛線部分是c++執行緒。
現在 nodejs 執行緒需要去查詢資料庫,這是一個典型的 I/O 操作,它不會等待 I/O 的結果,而且繼續處理其他的操作,它會把大量的計算能力分發到其他的c++執行緒去計算。
等到結果出來後返回給nodejs執行緒,在獲得結果之前nodejs 執行緒還能進行其他的I/O操作,所以是非阻塞的。
nodejs 執行緒 相當於左邊部分是服務員,c++ 執行緒是廚師。
所以,node的非阻塞I/O是通過呼叫c++的worker threads來完成的。
那當 c++ 執行緒獲取結果後怎麼通知 nodejs 執行緒呢?答案是事件驅動。
阻塞:I/O時程序休眠,等待I/O完成後進行下一步;
非阻塞:I/O時函數立即返回,程序不等待I/O完成。
那怎麼知道返回的結果,就需要用到事件驅動。
所謂事件驅動可以理解為跟前端點選事件一樣,我首先寫一個點選事件,但是我不知道什麼時候觸發,只有觸發的時候就去讓主執行緒執行事件驅動函數。
這種模式也是一種觀察者模式,就是我首先先監聽這個事件,等觸發時我就去執行。
那怎麼實現事件驅動呢?答案是非同步程式設計。
上面說過nodejs有大量的非阻塞I/O,那麼非阻塞I/O的結果是需要通過回撥函數來獲取的,這種通過回撥函數的方式,就是非同步程式設計。比如下面的程式碼是通過回撥函數獲取結果的:
glob(__dirname+'/**/*', (err, res) => { result = res console.log('get result') })
nodejs的回撥函數第一個引數是error,後面的引數才是結果。為什麼要這麼做呢?
try { interview(function () { console.log('smile') }) } catch(err) { console.log('cry', err) } function interview(callback) { setTimeout(() => { if(Math.random() < 0.1) { callback('success') } else { throw new Error('fail') } }, 500) }
執行之後,沒有被捕獲,錯誤被扔到了全域性,導致整個nodejs程式崩潰了。
沒有被try catch捕獲是因為setTimeout重新開啟了事件迴圈,每開啟一個事件迴圈就重新生一個呼叫棧context,try catch是屬於上一個事件迴圈的呼叫棧的,setTimeout的回撥函數執行的時候,呼叫棧都不一樣了,在這個新的呼叫棧中是沒有try catch,所以這個錯誤被扔到全域性,無法捕獲。具體可以參考這一篇文章JavaScript非同步佇列進行try catch時的問題解決。
那麼怎麼辦呢?把錯誤也作為一個引數:
function interview(callback) { setTimeout(() => { if(Math.random() < 0.5) { callback('success') } else { callback(new Error('fail')) } }, 500) } interview(function (res) { if (res instanceof Error) { console.log('cry') return } console.log('smile') })
但是這樣就比較麻煩,在回撥中還要判斷,所以就產生一種約定成熟的規定,第一個引數是err,如果不存在表示執行成功。
function interview(callback) { setTimeout(() => { if(Math.random() < 0.5) { callback(null, 'success') } else { callback(new Error('fail')) } }, 500) } interview(function (res) { if (res) { return } console.log('smile') })
nodejs的回撥寫法,不僅會帶來回撥地域,還會帶來非同步流程控制的問題。
非同步流程控制主要是指當並行的時候,怎麼來處理並行的邏輯。還是上面的例子,如果你同事面試兩家公司,只有當成功面試兩家的時候,才可以不面試第三家,那麼怎麼寫這個邏輯呢?需要全域性頂一個一個變數count:
var count = 0 interview((err) => { if (err) { return } count++ if (count >= 2) { // 處理邏輯 } }) interview((err) => { if (err) { return } count++ if (count >= 2) { // 處理邏輯 } })
像上面這種寫法就非常麻煩,且難看。所以,後來就出現了promise,async/await的寫法。
當前事件迴圈得不到的結果,但未來的事件迴圈會給你結果。很像一個渣男說的話。
promise不僅是一個渣男,還是一個狀態機:
const pro = new Promise((resolve, reject) => { setTimeout(() => { resolve('2') }, 200) }) console.log(pro) // 列印:Promise { <pending> }
執行then或者catch會返回一個新的promise,該promise最終狀態根據then和catch的回撥函數的執行結果決定:
function interview() { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) { resolve('success') } else { reject(new Error('fail')) } }) }) } var promise = interview() var promise1 = promise.then(() => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('accept') }, 400) }) })
promise1的狀態是由return裡面的promise的狀態決定的,也就是return裡面的promise執行完後的狀態就是promise1的狀態。這樣有什麼好處呢?這樣可以解決回撥地獄的問題。
var promise = interview() .then(() => { return interview() }) .then(() => { return interview() }) .then(() => { return interview() }) .catch(e => { console.log(e) })
then如果返回的promise的狀態是rejected,那麼會呼叫後面第一個catch,後面的then就不會在呼叫了。記住:rejected呼叫後面的第一個catch,resolved呼叫後面的第一個then。
如果promise僅僅是為了解決地獄回撥,太小看promise了,promise最主要的作用是解決非同步流程控制問題。下面如果要同時面試兩家公司:
function interview() { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) { resolve('success') } else { reject(new Error('fail')) } }) }) } promise .all([interview(), interview()]) .then(() => { console.log('smile') }) // 如果有一家公司rejected,就catch .catch(() => { console.log('cry') })
sync/await到底是什麼:
console.log(async function() { return 4 }) console.log(function() { return new Promise((resolve, reject) => { resolve(4) }) })
列印的結果一樣,也就是async/await是promse的語法糖而已。
我們知道try catch捕獲錯誤是依賴呼叫棧的,只能捕獲到呼叫棧以上的錯誤。但是如果使用await後能捕捉到呼叫棧所有函數的錯誤。即便這個錯誤是在另一個事件迴圈的呼叫棧丟擲的,比如setTimeout。
改造面試程式碼,可以看到程式碼精簡了很多。
try { await interview(1) await interview(2) await interview(2) } catch(e => { console.log(e) })
如果是並行任務呢?
await Promise.all([interview(1), interview(2)])
因為nodejs的非阻塞 I/0, 所以需要利用事件驅動的方式獲取 I/O 的結果,實現事件驅動拿到結果必須使用非同步程式設計,比如回撥函數。那麼如何來有序的執行這些回撥函數來獲取結果呢?那就需要使用事件迴圈。
事件迴圈是實現 nodejs 非阻塞 I/O 功能的關鍵基礎,非阻塞I/O和事件迴圈都是屬於 libuv
這個c++庫提供的能力。
程式碼演示:
const eventloop = { queue: [], loop() { while(this.queue.length) { const callback = this.queue.shift() callback() } setTimeout(this.loop.bind(this), 50) }, add(callback) { this.queue.push(callback) } } eventloop.loop() setTimeout(() => { eventloop.add(() => { console.log('1') }) }, 500) setTimeout(() => { eventloop.add(() => { console.log('2') }) }, 800)
setTimeout(this.loop.bind(this), 50)
保證了50ms就會去看佇列中是否有回撥,如果有就去執行。這樣就形成了一個事件迴圈。
當然實際的事件要複雜的多,佇列也不止一個,比如有一個檔案操作對列,一個時間對列。
const eventloop = { queue: [], fsQueue: [], timerQueue: [], loop() { while(this.queue.length) { const callback = this.queue.shift() callback() } this.fsQueue.forEach(callback => { if (done) { callback() } }) setTimeout(this.loop.bind(this), 50) }, add(callback) { this.queue.push(callback) } }
首先我們弄清楚了什麼是非阻塞I/O,即遇到I/O立刻跳過執行後面的任務,不會等待I/O的結果。當I/O處理好了之後就會呼叫我們註冊的事件處理常式,這就叫事件驅動。實現事件驅動就必須要用非同步程式設計,非同步程式設計是nodejs中最重要的環節,它從回撥函數到promise,最後到async/await(使用同步的方法寫非同步邏輯)。
到此這篇關於整理幾個關鍵節點深入理解nodejs的文章就介紹到這了,更多相關深入理解nodejs內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45