JavaScript >> Javascript 文檔 >  >> Node.js

在 Node.js 中避免回調地獄

簡介

我承認我是那些決定學習 Node.js 的人之一,僅僅是因為它周圍的嗡嗡聲以及每個人都在談論它。我想如果它在生命的早期就得到這麼多的支持,它一定有什麼特別之處。我大多來自 C、Java 和 Python 背景,所以 JavaScript 的異步風格與我之前遇到的任何東西都大不相同。

你們中的許多人可能都知道,真正底層的所有 JavaScript 都是一個處理排隊事件的單線程事件循環。如果您要在單個線程中執行長時間運行的任務,則該進程將阻塞,導致其他事件必須等待處理(即 UI 掛起、數據未保存等)。這正是您在事件驅動系統中要避免的。這是一個很棒的視頻,詳細解釋了 JavaScript 事件循環。

為了解決這個阻塞問題,JavaScript 嚴重依賴回調,回調是在長時間運行的進程(IO、計時器等)完成後運行的函數,從而允許代碼執行超過長時間運行的任務。

downloadFile('example.com/weather.json', function(err, data) {
	console.log('Got weather data:', data);
});

問題:回調地獄

雖然回調的概念在理論上很好,但它可能會導致一些非常混亂且難以閱讀的代碼。試想一下是否需要在回調後進行回調:

getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){ 
            getMoreData(c, function(d){ 
	            getMoreData(d, function(e){ 
		            ...
		        });
	        });
        });
    });
});

如您所見,這真的會失控。加入一些 if 語句,for 循環、函數調用或註釋,你會得到一些非常難以閱讀的代碼。初學者尤其容易成為這種情況的受害者,不知道如何避免這種“厄運金字塔”。

替代品

圍繞它進行設計

僅僅因為這個(糟糕的設計),很多程序員就陷入了回調地獄。他們並沒有真正提前考慮他們的代碼結構,也沒有意識到他們的代碼已經變得多麼糟糕,直到為時已晚。與您正在編寫的任何代碼一樣,您應該停下來思考在編寫代碼之前或編寫過程中可以做些什麼來使其更簡單和更具可讀性。這裡有一些技巧可以用來避免回調地獄 (或至少管理它)。

使用模塊

在幾乎每一種編程語言中,降低複雜性的最好方法之一就是模塊化。 JavaScript 編程也不例外。每當您編寫代碼時,請花一些時間退後一步,找出您經常遇到的常見模式。

您是否在不同的地方多次編寫相同的代碼?你的代碼的不同部分是否遵循一個共同的主題?如果是這樣,您就有機會清理並抽象和重用代碼。

您可以查看數千個模塊以供參考,但這裡有一些需要考慮。它們處理常見但非常具體的任務,否則這些任務會使您的代碼變得混亂並降低可讀性:Pluralize、csv、qs、clone。

給你的函數命名

在閱讀代碼時(尤其是雜亂無章的代碼),當小空間被如此多的嵌套回調所擁擠時,很容易忘記邏輯流程甚至語法。幫助解決這個問題的一種方法是命名你的函數,所以你所要做的就是看一眼這個名字,你就會更好地了解它的作用。它還給你的眼睛一個語法參考點。

考慮以下代碼:

var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, function(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

看這個可能需要幾秒鐘來了解每個回調的作用以及它從哪裡開始。向函數添加一些額外的信息(名稱)可以大大提高可讀性,尤其是當您在回調中深入多個級別時:

var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function appendText(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, function notifyUser(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

現在只需快速瀏覽一下,您就會知道第一個函數會附加一些文本,而第二個函數會通知用戶更改。

事先聲明你的函數

減少代碼混亂的最佳方法之一是保持更好的代碼分離。如果你事先聲明一個回調函數並在稍後調用它,你將避免使回調地獄如此難以使用的深度嵌套結構。

所以你可以從這裡開始......

var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, function(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

...對此:

var fs = require('fs');

function notifyUser(err) {
    if(err) return console.log(err);
    console.log('Appended text!');
};

function appendText(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, notifyUser);
}

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', appendText);

雖然這可能是幫助緩解問題的好方法,但它並不能完全解決問題。在閱讀以這種方式編寫的代碼時,如果您不記得每個函數的確切作用,那麼您將不得不返回並查看每個函數以追溯邏輯流程,這可能需要時間。

Async.js

值得慶幸的是,存在像 Async.js 這樣的庫來嘗試解決這個問題。異步在你的代碼之上添加了一層薄薄的函數,但可以通過避免回調嵌套來大大降低複雜性。

Async 中存在許多幫助方法,可以在不同的情況下使用,例如串行、並行、瀑布等。每個函數都有特定的用例,因此請花一些時間了解哪個函數在哪些情況下有幫助。

與 Async 一樣好,就像任何東西一樣,它並不完美。將系列、並行、永遠等結合起來很容易讓人忘乎所以,這時你又回到了你開始使用凌亂代碼的地方。注意不要過早優化。僅僅因為一些異步任務可以並行運行並不總是意味著它們應該。實際上,由於 Node 只是單線程的,因此使用 Async 並行運行任務幾乎沒有性能提升。

上面的代碼可以使用 Async 的瀑布來簡化:

免費電子書:Git Essentials

查看我們的 Git 學習實踐指南,其中包含最佳實踐、行業認可的標準以及隨附的備忘單。停止谷歌搜索 Git 命令並真正學習 它!

var fs = require('fs');
var async = require('async');

var myFile = '/tmp/test';

async.waterfall([
    function(callback) {
        fs.readFile(myFile, 'utf8', callback);
    },
    function(txt, callback) {
        txt = txt + '\nAppended something!';
        fs.writeFile(myFile, txt, callback);
    }
], function (err, result) {
    if(err) return console.log(err);
    console.log('Appended text!');
});

承諾

儘管 Promises 可能需要一點時間來掌握,但在我看來,它們是你可以在 JavaScript 中學習的更重要的概念之一。在開發我的一個 SaaS 應用程序期間,我最終使用 Promises 重寫了整個代碼庫。它不僅大大減少了代碼行數,而且使代碼的邏輯流程更容易遵循。

下面是一個使用非常快速且非常流行的 Promise 庫 Bluebird 的示例:

var Promise = require('bluebird');
var fs = require('fs');
Promise.promisifyAll(fs);

var myFile = '/tmp/test';
fs.readFileAsync(myFile, 'utf8').then(function(txt) {
	txt = txt + '\nAppended something!';
	fs.writeFile(myFile, txt);
}).then(function() {
	console.log('Appended text!');
}).catch(function(err) {
	console.log(err);
});

注意這個解決方案不僅比以前的解決方案更短,而且更容易閱讀(雖然,誠然,Promise 風格的代碼可能需要一些時間來適應)。花時間學習和理解 Promises,這將是值得的。然而,Promises 絕對不能解決我們在異步編程中的所有問題,所以不要假設使用它們你將擁有一個快速、乾淨、無錯誤的應用程序。關鍵是知道它們什麼時候對你有用。

一些你應該檢查的 Promise 庫是 Q、Bluebird,或者如果你使用 ES6 的話,還有內置的 Promise。

異步/等待

注意:這是一個 ES7 功能,目前在 Node 或 io.js 中不受支持。但是,您現在可以將它與 Babel 之類的轉譯器一起使用。

清理代碼的另一個選擇是使用 async 功能。這將允許您編寫看起來更像同步代碼但仍然是異步的代碼。

一個例子:

async function getUser(id) {
    if (id) {
        return await db.user.byId(id);
    } else {
        throw 'Invalid ID!';
    }
}

try {
	let user = await getUser(123);
} catch(err) {
	console.error(err);
}

db.user.byId(id) 調用返回一個 Promise ,我們通常必須與 .then() 一起使用 ,但使用 await 我們可以直接返回解析後的值。

注意包含 await 的函數 調用以 async 為前綴 ,它告訴我們它包含異步代碼並且還必須使用 await 調用 .

這種方法的另一大優勢是我們現在可以使用 try/catch , for , 和 while 使用我們的異步函數,這比將 Promise 鏈接在一起更直觀。

除了使用 Babel 和 Traceur 之類的編譯器之外,您還可以通過 asyncawait 包在 Node 中獲得類似的功能。

結論

避免像回調地獄這樣的常見問題並不容易,所以不要指望馬上結束你的挫敗感。我們都陷入其中。試著放慢速度,花點時間考慮一下代碼的結構。就像任何事情一樣,熟能生巧。

你有沒有遇到回調地獄?如果是這樣,你如何繞過它?在評論中告訴我們!


Tutorial JavaScript 教程
  1. 如何在 PrimeREACT 中使用 Toast 組件

  2. 如何將 React 應用程序加載時間減少 70%

  3. 在 Laravel 5.5 中開始使用 React

  4. 應用動畫

  5. 如何使用 Javascript 和 2Captcha 繞過驗證碼

  6. 區別 TypeError 和 ReferenceError

  7. 如何使用 React 設置 Tailwind CSS

  1. SVG 清理

  2. Socket IO 服務器到服務器

  3. 反應頁面/路由器轉換

  4. iframe 未在 Chrome 中讀取 cookie

  5. 一勞永逸地理解 JavaScript 中的提升

  6. 使鏈接使用 POST 而不是 GET

  7. #3。為不同的功能創建 api 端點和路由 ☀

  1. 如何將 Github 連接到 AWS CodePipelines?

  2. 使用 Vue.js 和 AI 創建 Profile Pic Maker 應用

  3. 如何在javascript中創建貨幣轉換器

  4. 6 個 Node.js 靜態站點生成器