NodeJS簡介

19 March 2021

簡介

Nodejs的前身就是javascript
javascript是一個在瀏覽器(v8 engine)上執行的程式語言,因為很多開發者覺得這個太好用了, 例如他可以達到非同步執行,很方便的處理好concurrency的問題(有event handler, callback queue來幫忙安排task, js也沒有interrupt來中斷目前執行的task的問題),因此把整套v8 engine搬出來當作js的執行環境, 讓開發者能夠開發javascript程式並且把作為. 這樣做就不用藉由瀏覽器之手來執行js的程式, 而且nodejs除了原本在瀏覽器上支援的webAPI(如setTimeout等等)外, 也另外有更多API如fs,http,crypto,os來作更多server-side的工作. nodejs本身也像是python那樣提供開發者開發套件讓其他人使用. 後來也很常被拿來寫網站後端(如Expressjs, Nestjs…).

Event loop

Event loop是一種系統運行的模式, 特色就是非常強調asynchornous exectuion, 主要用於那種同時要執行很多任務的系統而又不希望因為某個task卡住或者很花時間影響到其他task, 又或者會突發事件要處理,不容易事先預測處理. 最有名的應用當屬於Nodejs, javascript, 跟瀏覽器的部分. 因為他們常常要同時處理很多的task/event (如渲染網站,執行網站javscript code, 處理event,跟load檔案),這些必須要非同步話處理, 這樣就可以邊load新檔案邊呈現頁面, 而不用把所有事情處理完(極度費時)才顯示完整網頁.

以前瀏覽器設計javascript的初衷在於提供一個網站跟使用者更複雜的互動方式, 但為了避免處理concurrency的問題和減少開發難度, 以前的javascript執行是single thread, 這樣問題就在於blocking, 當網站變得越來越複雜, 勢必有些task會很花時間(例如把所以資料都load好後才能渲染), 使用者體驗會變差. 所以發展出了event loop有一個queue來處理task, 並把很花時間的task送給後面的worker thread來handle, 這又就可以達到concurrent execution.

大致上概念可以想成synchornous的指令會直接執行, async的指令(如callback等等)會跑進task queue裡面, event loop理會不斷的檢查各個task queue看有沒有東西再把它解決掉. 然後每個browser tab會有自己的event loop

Single Threaded

Nodejs本身的架構是一個single threaded配合後面有一個pool的worker thread的架構.

架構

javascript是一個Single threaded programming language, 只有一個主要的thread來處理所有的task, 利用callstack來決定task執行的順序(某方面來說就是像一般執行檔執行時會有的stack), 以下範例是先以javascript的v8 engine為例

  • call stack: stack的資料結構儲存要執行的task,最先push進去的task最後處理,可以用來在遞迴中trace function call的過程來除bug
  • webapi: 瀏覽器提供的api來供使用者的javascript程式使用, 大部分瀏覽器會共同支援某些api,例如DOM, ajax,settimeout等等, 然後個別又會支援個別特色的api
  • callback queue: queue的資料結構, 用來存callback的task, FIFO
  • event loop: 相當於某種程度的scheduler, 決定每個task分別要送去哪裡, 不斷的去檢查是否有task完成的event
console.log("hi")
setTimeout(function(cb){
  console.log("there)
},5000)

以這個範例而言, event loop就分別會有三個task, 兩個console log跟setTimeout

  1. event loop會依序執行這三個task push到call stack
  2. setTimeout會被送到webapi裡作執行,
  3. 當setTimeout在webapi執行時,call stack會去執行第二個console.log, 此時call stack跟web api都有task在同時執行
  4. 當webapi執行完,eventloop會把callback放到task queue, 當setTimeout在webapi結束時, task queue裡面的callback會被eventloop push到call stack執行

Event handler blocking

因此可以知道重點在於不能夠把call stack給block住, 運算複雜的task應該要弄到背景的worker thread來執行. call stack block住的話就是會影響整個UI的狀況, call queue block住的話就是在call queue裡面的call back function要等很久才能執行到. 因為event loop的作用就是移到call stack時要把call stack的task處理一個,然後再移到call queue時把裡面的call back task移到call stack裡面, 所以call queue flooding只是會讓event loop來不及把她移到call stack, 但call stack太多task則是會讓event loop沒辦法移動到其他地方task queue做處理而卡在單一個call stack裡面的task

舉例來說 如果在一般同步的執行時,會直接程式碼進入call stack, 此時如果Call stack的task是會執行很久,甚至停不下來,那就會導致整個event loop停住而不會往下一個task queue執行. 如下面例子

fs.readFile("hello",()=>{
  console.log("hi")
})

while(true){}

因為後面的同步執行是無限迴圈, 所以event loop會卡在這行,然後call back queue就永遠不會執行,hi就永遠不會印

Thread Pool

為了避免上面所述的Event handler blocking, 因此要避免把運算複雜的task放到call stack或者task queue裡面. 那實際上這些CPU-intensive的task就要分配給其他thread來作. 因此nodejs除了一個single-thread的event loop處理各個task queue外, 也會maintain一個thread pool, 這個thread pool就是負責處理複雜的task, event handler會把http request, crypto運算file read等等複雜運算或牽扯到IO的task分配給worker thread來實作, 等作完再把結果傳給event handler. 這樣event handler就不會block在這些task. 而對於IO的處理其實又有些不同, 因為IO本身是交給作業系統處理, 所以實際執行這些IO操作的thread會是OS提供的, 等於說nodejs的thread pool裡的thread在handle IO task時其實只是把這個task在下放到OS來作, 自己就去做其他的task了, 當IO作完OS會通知Nodejs的thread, 該thread才會把後續的call back放到task queue裡.

詳情可以看這個網站link, 介紹了很仔細的event loop跟thread pool之間的關係, 以及crypto, dns lookup等等都是thread pool來負責.

Nodejs Event Handler跟thread pool的實作是透過libuv

Nodejs task queue

事實上整個event loop會更複雜原因是因為task queue會有複數個,每個task queue處理特定類別的task/callback, 但運作原理都依樣, 只是會處理不同類型的task, 而且瀏覽器的event loop實作細節跟nodejs的實作細節也有所差異, 底下以nodejs為例來說明.

  • Timers: 有關setTimeout,setInterval相關的callback都會被丟到這裡, 即這裡存計時器時間到時要執行的callback
  • Pending Callbacks: 作業系統層級相關的task, 像是網路相關錯誤(TCP error, connection refuse)之類的task會在這裡pending
  • Idle prepare: 給內部nodejs使用的task
  • Polling: 負責處理IO相關的callback
  • Close callbacks: 跟關閉有關的function的callback都會放到這裡

相較於上述這幾個task queue, 下面兩個有更高的priority

  • nextTick queue: 最高優先度的task queue, 所有process.nextTick的callback都會跑到這裡, 只要裡面有東西, event loop就會先執行這個
  • microTask queue: promise的狀態的callback變成resolve或reject實會被排進來執行的callback.

Promise本身這個指令是同步的, 是他的resolve, reject的callback才是非同步, 所以promise本身不會排進event loop

### V8 Engine (TBD)

chrome跟Nodejs都是使用v8 engine來執行javascript, V8 Engine符合ECMAScript的標準

Callback function

會有這個神奇的東西的原因就是NodeJS本身是非同步執行,也就是說他不是sequential執行, 當我一個運算比較久時,後面的指令可以先執行導致後面的指令比前面的早完成. 因此當我希望能夠有一些執行的順序(例如發requests 得到requests後再做判斷要做啥操作), 如果我直接寫,可能requests還沒弄玩後面的判斷式就已經執行導致錯誤.

Arrow Function

壹般而言, 我們call function都會用這樣的方式

function a(ret,function(ret){...})

Arrow function的特色是我們用箭頭取代function這個keyword, 他可以想成是一個沒有名字的function, 很常用於callback(因為callback其實是一個function,而且我們不在乎這個function叫啥名字)

const a = (var b)=>{...}
等於
const a = function c(var b){...} 只是沒有function name

Callback function

使用: 基本上function會定義好參數, 像request文件裡就寫說request回傳request(url, callback(err,resp,body))之類的

request(url, function(error,response,body){
    ...
})

建立callback function

const geocode = (address, callback) => {
  // 地點將隨帶入的參數變化
  const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(address)}.json?access_token=${API_KEY}&limit=1`

  request(url, (error, { body(包含resp,body) }) => {
    if (error) { return callback('Unable to connect to location services', undefined) }
    if (body.features.length === 0) { return callback('Unable to find this location, try again!', undefined) }
    const { features } = body
    callback(undefined, {
      longitude: features[0].center[0],
      latitude: features[0].center[1],
      location: features[0].place_name
    })
  })
}

geocode(address,function(err,{ret}){...})

由此可知 geocode()他的callback會是function(err,{longitude,latitude,location})這樣的模式,就如同上述定義

callback hell 就如上述例子, 當我想要sequentially的使用callback function(或以上述例子,依照順序的發送request) 這樣就會產生整個callback list很長而不容易維護, debug的問題

Promise

某一部分, 利用不斷的.then, .catch的方式來避免複雜的callback hell. 讓程式碼維護, 可讀性變高

Promise本身是一個物件, 在promise中可以分成這三種狀態

  • pending(執行中,等待執行結果)
  • resolve: 執行成功(trigger onsuccessful event), 會把參數傳進並執行then section
  • reject: 執行失敗(trigger onfail event), 會把參數傳進並執行catch section
  • then: 只會在onsuccessful event發生時執行
  • catch: 只會在onfail event時執行

創建Promise Object

const getData = new Promise((resolve, reject) => {
  // 非同步的作業...code...  (例如request之類的)
  if (error) { return reject('錯誤訊息')}  // 執行失敗,用rejct, 跑去執行catch section
  resolve({         // 執行成功, 去執行then section
    data1: 'abc',
    data2: '123'
  })
})

執行Promise

getData
  // 使用 then 方法,並將成功訊息印出來
  .then(data => {console.log('成功資料', data)})
  .catch(err => {console.log(err)})

連續照順序的promise, then() 方法回傳的是一個「新的 promise」,因此得以往下互相串接

getData
  .then(({data}) => {
    console.log(data.data1) // abc
    return data.data1 + 'def'   // 使用return將資訊傳進下個callback function(相當於resolve)
  })
  // 獲得前一個 then() 回傳的結果
  .then(({data}) => {           // Data, resolve裡的東西
    console.log(data) // abcdef
    return data + 'ghi'
  })
  // 獲得前一個 then() 回傳的結果
  .then(({data}) => {
    console.log(data) // abcdefghi
  })
  // 使用 catch 方法,並將錯誤訊息印出來
  .catch(error => console.log('錯誤訊息', error))

Async Await

基本上就是把那個function變成同步執行
只要在function前加async關鍵字, 要synchronize的那行加await就行, 而且他的return直我們還能得到, 然後最好要用try, catch來保證我們的程式正常執行

const SendIntroList = async context => {
    const {event,session} = context
    console.log("user : ",session,", request: follow, info : ",event)
    try{
      await context.pushTemplate(...)
    }
    catch(error){...}

Reference