首頁  >  文章  >  web前端  >  總結分享了解nodejs的幾個關鍵節點

總結分享了解nodejs的幾個關鍵節點

青灯夜游
青灯夜游轉載
2022-07-06 20:36:502116瀏覽

總結分享了解nodejs的幾個關鍵節點

本文是個人在實際開發和學習中對nodejs的一些理解,現整理出來方便日後查閱,如果能給您啟發將不勝榮幸。

非阻塞I/O

I/O:即 Input / Output,一個系統的輸入與輸出。

一個系統可以理解為一個個體,比如說一個人,你說話就是輸出,你聽就是輸入。

阻塞 I/O 與非阻塞 I/O 的差異就在於系統接收輸入再到輸出期間,能不能接收其他輸入

下面以兩個例子來說明什麼是阻塞I/O 和非阻塞I/O:

#1、打飯

總結分享了解nodejs的幾個關鍵節點

首先我們要確定一個系統的範圍,在這個例子中食堂阿姨和餐廳的服務生看成是一個系統,輸入就是點菜,輸出就是端菜

那麼在點菜和端菜之間能不能接受其他人的點菜,就可以判斷是阻塞I/O還是非阻塞I/O。

對於食堂阿姨,他在點菜的時候,是不能幫其他同學點菜的,只有這個同學點完菜端菜走了之後,才能接受下一個同學的點菜,所以食堂阿姨是阻塞I/O。

對於餐廳服務員,他可以在點完菜以後,這個客人端菜之前是可以服務下一位客人的,所以服務員是非阻塞I/O。

2、做家事

總結分享了解nodejs的幾個關鍵節點

#在洗衣服的時候,是不需要等著洗衣機旁邊的,這個時候可以去掃地和整理書桌,當整理完書桌後衣服也洗好了,這個時候去晾衣服,那麼總共只需要25分鐘。

洗衣服其實就是一個非阻塞I/O,在把衣服丟進洗衣機和洗完衣服期間,你是可以乾其他事情的。

非阻塞I/O之所以能提升效能,是因為它可以把不必要的等待給節省掉。

理解非阻塞I/O的要點在於

  • #確定一個進行I/O的系統邊界。這非常關鍵,如果把系統擴大,上面餐廳的例子,如果把系統擴大到整個餐廳,那麼廚師肯定是一個阻塞 I/O。
  • 在 I/O 過程中,能不能進行其他 I/O。

nodejs的非阻塞 I/O

nodejs的非阻塞 I/O 是怎麼體現的呢?前面說過要理解非阻塞 I/O 的一個重要點是先確定一個系統邊界,node的系統邊界就是主執行緒

如果下面的架構圖按照線程的維護劃分,左邊虛線部分是nodejs線程,右邊虛線部分是c 線程。

總結分享了解nodejs的幾個關鍵節點

現在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() <p>執行之後,沒有被捕獲,錯誤被丟到了全局,導致整個nodejs程式崩潰了。 </p><p><img src="https://img.php.cn/upload/image/244/886/980/1657110712466688.png" title="1657110712466688.png" alt="總結分享了解nodejs的幾個關鍵節點"></p><p>沒有被try catch捕捉是因為setTimeout重新開啟了事件循環,每開啟一個事件循環就重新生一個呼叫棧context,try catch是屬於上一個事件循環的呼叫堆疊的,setTimeout的回呼函數執行的時候,呼叫堆疊都不一樣了,在這個新的呼叫堆疊中是沒有try catch,所以這個錯誤被丟到全局,無法捕捉。具體可以參考這篇文章<a href="https://juejin.cn/post/6995749646366670855" target="_blank" title="https://juejin.cn/post/6995749646366670855">非同步佇列進行try catch時的問題</a>。 </p><p>那麼怎麼辦呢?把錯誤也當作一個參數:</p><pre class="brush:php;toolbar:false">function interview(callback) {
    setTimeout(() => {
        if(Math.random() <p>但是這樣就比較麻煩,在回呼中還要判斷,所以就產生一種約定成熟的規定,第一個參數是err,如果不存在表示執行成功。 </p><pre class="brush:php;toolbar:false">function interview(callback) {
    setTimeout(() => {
        if(Math.random() <h3 data-id="heading-5"><strong>非同步流程控制</strong></h3><p>nodejs的回呼寫法,不僅會帶來回呼地域,還會帶來<strong>非同步流程控制</strong>的問題。 </p><p>非同步流程控制主要是指當並發的時候,怎麼來處理並發的邏輯。還是上面的例子,如果你同事面試兩家公司,只有成功面試兩家的時候,才可以不面試第三家,那怎麼寫這個邏輯呢?需要全域頂一個變數count:</p><pre class="brush:php;toolbar:false">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

目前事件循環得不到的結果,但未來的事件循環會給你結果。很像渣男說的話。

promise不只是渣男,還是狀態機:

  • pending
  • fulfilled/resolved
  • rejectd
const pro = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('2')
    }, 200)
})
console.log(pro) // 打印:Promise { <pending> }</pending>

then & .catch

  • resolved 狀態的promise 會呼叫後面的第一個then
  • rejected 狀態的promise 會呼叫後面的第一個catch
  • 任何一個reject 狀態且後面沒有.catch 的promise,都會造成瀏覽器或node 環境的全域錯誤。 uncaught 表示未捕獲的錯誤。

總結分享了解nodejs的幾個關鍵節點

執行then或catch會傳回一個新的promise,該promise最終狀態根據then和catch的回呼函數的執行結果決定:

  • 如果回呼函數總是throw new Error,該promise是rejected狀態
  • 如果回呼函數始終是return,則該promise是resolved狀態
  • 但如果回呼函數總是return一個promise,該promise會和回呼函數return的promise狀態保持一致
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了,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')
    })

async/await

#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 函式庫提供的能力。

總結分享了解nodejs的幾個關鍵節點

代码演示:

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(使用同步的方法写异步逻辑)。

更多node相关知识,请访问:nodejs 教程

以上是總結分享了解nodejs的幾個關鍵節點的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除