Node.js 的運作方式

已完成

本單元會說明 Node.js 如何處理傳入工作到 JavaScript 執行階段。

工作類型

JavaScript 應用程式有兩種工作類型:

  • 同步模式:這些工作會依照順序發生。 它們不依賴另一個資源即可完成。 範例包括數學運算或字串操作。
  • 非同步:這些工作可能不會立即完成,因為它們相依于其他資源。 範例包括網路要求或檔案系統作業。

因為您希望程式盡快執行,所以會希望 JavaScript 引擎能夠在等候非同步作業的回應時繼續運作。 為了這樣做,它會將非同步工作新增至工作佇列,並繼續處理下一個工作。

使用事件迴圈管理工作佇列

Node.js 使用 JavaScript 引擎的事件驅動架構來處理非同步要求。 下圖說明 V8 事件迴圈在高層級的運作方式:

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

以適當語法表示 (如下所示) 的非同步工作,會新增至事件迴圈。 工作包含要完成的工作,以及要接收結果的回撥函式。 當密集作業完成時,會以結果觸發回撥函式。

同步作業與非同步作業

Node.js API 為部分相同的作業 (例如檔案作業),提供非同步和同步作業。 雖然通常您應該一律先考慮非同步操作,但有時候您可能會使用同步作業。

例如,當命令列介面 (CLI) 讀取檔案,然後立即使用檔案中的資料時。 在此情況下,您可以使用檔案作業的同步版本,因為沒有其他系統或人員等候使用應用程式。

不過,如果您要建立網頁伺服器,則應該一律使用檔案作業的非同步版本,以免封鎖單一執行緒處理其他使用者要求的執行能力。

在身為 TailWind Traders 開發人員的工作中,您必須了解同步和非同步作業之間的差異,以及何時使用何種作業。

透過異步操作的效能

Node.js 會利用 JavaScript 的獨特事件驅動本質,讓撰寫伺服器工作的作業快速且高效能。 JavaScript 搭配非同步技術正確使用時,可能會產生與 C 等低階語言相同的效能結果,因為 V8 引擎可以提升效能。

非同步技術有 3 種樣式,您必須能夠在工作中辨識:

  • Async/await (建議):使用 和 await 關鍵詞來接收異步操作結果的最新異步技術async。 Async/await 用於許多程式設計語言。 一般而言,具有較新相依性的新專案會使用此非同步程式碼樣式。
  • 回撥:使用回撥函式來接收非同步作業結果的原始非同步技術。 您會在舊版程式碼基底和舊版 Node.js API 中看到此技術。
  • 承諾:較新的非同步技術,使用 Promise 物件來接收非同步作業的結果。 您會在較新的程式碼基底和較新的 Node.js API 中看到此技術。 您可能必須在工作中撰寫 Promise 型程式碼,以包裝不會更新的舊版 API。 藉由對此包裝使用 Promise,您可以允許程式碼用於比起較新的 async/await 程式碼樣式更大的 Node.js 版本化專案中。

Async/await

Async/await 是處理異步程序設計的最新方式。 Async/await 是在 Promise 上的糖衣語法,能使非同步程式碼看起來更像同步程式碼。 也更容易閱讀和維護。

使用 async/await 的相同範例如下所示:

// async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
async function readFileAsync() {
  try {
    // `await` before the async method
    const data = await fs.readFile(filePath, 'utf-8');
    console.log(data);
    console.log('Done!');
  } catch (error) {
    console.log('An error occurred...: ', error);
  }
}

readFileAsync()
  .then(() => {
    console.log('Success!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

在ES2017中發行 async/await 時,關鍵詞只能用於具有最上層函式的承諾函式中。 雖然承諾不必有 thencatch 區段,但仍需要有 promise 語法才能執行。

async 式一律會傳回承諾,即使其內部沒有呼叫也一 await 樣。 promise 會使用 函式所傳回的值來解析。 如果函式擲回錯誤,則會使用擲回的值拒絕承諾。

Promise

因為巢狀回撥可能難以閱讀和管理,所以 Node.js 已針對 Promise 新增支援。 Promise 是物件,表示非同步作業的最終完成 (或失敗)。

Promise 函式的格式如下:

// Create a basic promise function
function promiseFunction() {
  return new Promise((resolve, reject) => {
    // do something

    if (error) {
      // indicate success
      reject(error);
    } else {
      // indicate error
      resolve(data);
    }
  });
}

// Call a basic promise function
promiseFunction()
  .then((data) => {
    // handle success
  })
  .catch((error) => {
    // handle error
  });

Promise 已履行時會呼叫 then 方法,Promise 遭拒絕時則會呼叫 catch 方法。

若要使用 Promise 以非同步方式讀取檔案,程式碼為:

// promises asynchronous example

const fs = require('fs').promises;
const filePath = './file.txt';

// request to read a file
fs.readFile(filePath, 'utf-8')
  .then((data) => {
    console.log(data);
    console.log('Done!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

console.log(`I'm the last line of the file!`);

最上層 async/await

最新版的 Node.js 新增了 ES6 模組的最上層 async/await。 您必須在 package.json 中新增名為 type 的屬性 (其值為 module),才能使用這項功能。

{
    "type": "module"
}

然後,您可以在程式碼的最上層使用 await 關鍵字

// top-level async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
try {
  // `await` before the async method
  const data = await fs.readFile(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");

回撥

Node.js 最初發行時,會使用回撥函式來處理非同步程式設計。 回撥是作為引數傳遞至其他函式的函式。 工作完成時,會呼叫回撥函式。

函式的參數順序十分重要。 回撥函式是函式的最後一個參數。

// Callback function is the last parameter
function(param1, param2, paramN, callback)

您維護的程式碼中函式名稱可能不會稱為 callback。 該名稱可以稱為 cbdonenext。 函式的名稱並不重要,但參數的順序很重要。

請注意,沒有語法指示函式為非同步。 您必須透過閱讀文件或繼續閱讀程式碼得知函式為非同步。

使用具名回撥函式的回撥範例

下列程式碼會分隔非同步函式與回撥。 這較易於閱讀和了解,並可讓您重複使用其他非同步函式的回撥。

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// callback
const callback = (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
};

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);

console.log("I'm the last line of the file!");

正確的結果是:

I'm the last line of the file!
Hi, developers!
Done!

首先,非同步函式 fs.readFile 會啟動並進入事件迴圈。 然後,程式碼執行會繼續進行下一個程式碼行,也就是最後一個 console.log。 讀取檔案之後,會呼叫回撥函式,並執行兩個 console.log 陳述式。

匿名函式的回撥範例

下列範例會使用匿名回撥函式,這表示函式沒有名稱且無法由其他匿名函式重複使用。

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
});

console.log("I'm the last line of the file!");

正確的結果是:

I'm the last line of the file!
Hi, developers!
Done!

執行程式碼時,會啟動非同步函式 fs.readFile 並進入事件迴圈。 接下來,執行會繼續進行下列程式碼行,也就是最後一個 console.log。 讀取檔案時,會呼叫回撥函式,並執行兩個 console.log 陳述式。

巢狀回撥

因為您可能需要呼叫後續的非同步回撥,然後呼叫另一個非同步回撥,所以回撥程式碼可能會變成巢狀。 這稱為回撥地獄,令人難以閱讀和維護。

// nested callback example

// file system module from Node.js
const fs = require('fs');

fs.readFile(param1, param2, (error, data) => {
  if (!error) {
    fs.writeFile(paramsWrite, (error, data) => {
      if (!error) {
        fs.readFile(paramsRead, (error, data) => {
          if (!error) {
            // do something
          }
        });
      }
    });
  }
});

同步 API

Node.js 也有一組同步 API。 這些 API 會封鎖程式執行,直到工作完成為止。 當您想要讀取檔案,然後立即使用檔案中的資料時,同步 API 即相當實用。

Node.js 中的同步 (封鎖) 函式會使用 functionSync 的命名慣例。 例如,非同步 readFile API 具有名為 readFileSync 的同步對應項目。 請務必在您自己的專案中堅持此標準,以便您的程式碼易於閱讀和理解。

// synchronous example

const fs = require('fs');

const filePath = './file.txt';

try {
  // request to read a file
  const data = fs.readFileSync(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}

身為 TailWind Traders 的新開發人員,組織可能會要求您修改任何類型的 Node.js 程式碼。 請務必了解同步和非同步 API 之間的差異,以及非同步程式碼的不同語法。