主頁 > 前端設計 > 七天學會NodeJS(三)行程管理(util.format、Process、Child Process、Cluster)、子行程父行程之間通訊、異步編程、例外處理、域(Domain)

七天學會NodeJS(三)行程管理(util.format、Process、Child Process、Cluster)、子行程父行程之間通訊、異步編程、例外處理、域(Domain)

2021-09-01 14:42:39 前端設計

七天學會NodeJS(三)行程管理(util.format、Process、Child Process、Cluster)、子行程父行程之間通訊、異步編程、例外處理、域(Domain)

文章目錄

  • 七天學會NodeJS(三)行程管理(util.format、Process、Child Process、Cluster)、子行程父行程之間通訊、異步編程、例外處理、域(Domain)
    • 1. 行程管理
      • 開門紅
      • API走馬觀花
        • Process
        • Child Process
        • Cluster
      • 應用場景
        • 如何獲取命令列引數
        • 如何退出程式
        • 如何控制輸入輸出
        • 如何降權
        • 如何創建子行程
        • 行程間如何通訊
        • 如何守護子行程
      • 小結
    • 2. 異步編程
      • 回呼
      • 代碼設計模式
        • 函式回傳值
        • 遍歷陣列
        • 例外處理
      • 域(Domain)
        • 陷阱
      • 小結

總結:

  • 行程管理

    • 本章小結

      • 使用process物件管理自身
      • 使用child_process模塊創建和管理子行程
    • util.format(format, […])

      • 根據第一個引數,回傳一個格式化字串,類似printf的格式化輸出,
      • 第一個引數是一個字串,包含零個或多個占位符, 每一個占位符被替換為與其對應的轉換后的值, 支持的占位符有:
        • %s - 字串.
        • %d - 數字 (整型和浮點型).
        • %j - JSON. 如果這個引數包含回圈物件的參考,將會被替換成字串 '[Circular]'
        • %% - 單獨一個百分號('%'),不會消耗一個引數,
      • 如果占位符沒有相對應的引數,占位符將不會被替換,
    • Proces

      • 官方檔案: http://nodejs.org/api/process.html

      • 在NodeJS中,可以通過process物件感知和控制NodeJS自身行程的方方面面,

      • process不是內置模塊,而是一個全域物件,因此在任何地方都可以直接使用,

      • 如何獲取命令列引數

        • 在NodeJS中可以通過process.argv獲取命令列引數
        • 由于argv[0]固定等于NodeJS執行程式的絕對路徑,argv[1]固定等于主模塊的絕對路徑,
        • 第一個命令列引數從argv[2]開始
      • 如何退出程式

        • 正常退出狀態碼=0
        • 捕捉例外后退出 process.exit(1)
      • 如何控制輸入輸出

        • NodeJS程式的標準輸入流(stdin)、一個標準輸出流(stdout)、一個標準錯誤流(stderr)分別對應process.stdinprocess.stdoutprocess.stderr
        • 第一個是只讀資料流,后邊兩個是只寫資料流
      • 子行程父行程之間通訊

        • /* parent.js  這是父行程*/
          //父行程在創建子行程,在options.stdio欄位中通過ipc開啟了一條IPC通道,之后就可以監聽子行程物件的message事件接收來自子行程的訊息
          var child = child_process.spawn('node', [ 'child.js' ], {
                  stdio: [ 0, 1, 2, 'ipc' ]
              });
          
          child.on('message', function (msg) {
              console.log(msg);
          });
          
          //通過.send方法給子行程發送訊息
          child.send({ hello: 'hello' });
          
          /* child.js  這是子行程*/
          //在子行程這邊,可以在process物件上監聽message事件接收來自父行程的訊息
          process.on('message', function (msg) {
              msg.hello = msg.hello.toUpperCase();
              //并通過.send方法向父行程發送訊息,
              process.send(msg);
          });
          
    • Child Process

      • 官方檔案: http://nodejs.org/api/child_process.html
      • 使用child_process模塊可以創建和控制子行程,
      • 該模塊提供的API中最核心的是.spawn,其余API都是針對特定使用場景對它的進一步封裝,算是一種語法糖,
        • 使用.spawn(exec, args, options)方法,創建子行程,該方法支持三個引數,
        • 第一個引數是執行檔案路徑,可以是執行檔案的相對或絕對路徑,也可以是根據PATH環境變數能找到的執行檔案名,
        • 第二個引數中,陣列中的每個成員都按順序對應一個命令列引數,
        • 第三個引數可選,用于配置子行程的執行環境與行為,
    • Cluster

      • cluster模塊是對child_process模塊的進一步封裝,專用于解決單行程NodeJS Web服務器無法充分利用多核CPU的問題,
      • 使用該模塊可以簡化多行程服務器程式的開發,讓每個核上運行一個作業行程,并統一通過主行程監聽埠和分發請求,
  • 異步編程

    • 本章小結

      • 不掌握異步編程就不算學會NodeJS,
      • 異步編程依托于回呼來實作,而使用回呼不一定就是異步編程,
      • 異步編程下的函式間資料傳遞、陣列遍歷和例外處理與同步編程有很大差別,
      • 使用domain模塊簡化異步代碼的例外處理,并小心陷阱,
    • 簡介

      • NodeJS最大的賣點——事件機制和異步IO,對開發者并不是透明的,
    • 回呼

      • 你不知道用戶何時單擊按鈕, 因此,為點擊事件定義了一個事件處理程式, 該事件處理程式會接受一個函式,該函式會在該事件被觸發時被呼叫:

        document.getElementById('button').addEventListener('click', () => {
          //被點擊
        })
        

        這就是所謂的回呼,回呼是一個簡單的函式,會作為值被傳給另一個函式,并且僅在事件發生時才被執行

      • 我們仍然回到JS是單執行緒運行的這個事實上,這決定了JS在執行完一段代碼之前無法執行包括回呼函式在內的別的代碼,也就是說,即使平行執行緒完成作業了,通知JS主執行緒執行回呼函式了,回呼函式也要等到JS主執行緒空閑時才能開始執行,

    • 例外處理

      • 在NodeJS中,幾乎所有異步API都按照以下方式設計,回呼函式中第一個引數都是err,因此我們在撰寫自己的異步函式時,也可以按照這種方式來處理例外,與NodeJS的設計風格保持一致,

      • function async(fn, callback) {
            // Code execution path breaks here.
            setTimeout(function () {
                try {
                    callback(null, fn());
                } catch (err) {
                    callback(err);
                }
            }, 0);
        }
        
        async(null, function (err, data) {
            if (err) {
                console.log('Error: %s', err.message);
            } else {
                // Do something.
            }
        });
        
        -- Console ------------------------------
        Error: object is not a function
        
      • 回呼函式已經讓代碼變得復雜了,而異步方式下對例外的處理更加劇了代碼的復雜度,如果NodeJS的最大賣點最后變成這個樣子,那就沒人愿意用NodeJS了,因此接下來會介紹NodeJS提供的一些解決方案,

    • 域(Domain)

      • 官方檔案: http://nodejs.org/api/domain.html

      • 可以簡化異步代碼的例外處理

      • 為了讓代碼好看點,我們可以在每處理一個請求時,使用domain模塊創建一個子域(JS子運行環境),在子域內運行的代碼可以隨意拋出例外,而這些例外可以通過子域物件的error事件統一捕獲,

        function async(request, callback) {
            // Do something.
            asyncA(request, function (data) {
                // Do something
                asyncB(request, function (data) {
                    // Do something
                    asyncC(request, function (data) {
                        // Do something
                        callback(data);
                    });
                });
            });
        }
        
        http.createServer(function (request, response) {
            var d = domain.create();
        
            d.on('error', function () {
                response.writeHead(500);
                response.end();
            });
        
            d.run(function () {
                async(request, function (data) {
                    response.writeHead(200);
                    response.end(data);
                });
            });
        });
        

1. 行程管理

NodeJS可以感知和控制自身行程的運行環境和狀態,也可以創建子行程并與其協同作業,這使得NodeJS可以把多個程式組合在一起共同完成某項作業,并在其中充當膠水和調度器的作用,本章除了介紹與之相關的NodeJS內置模塊外,還會重點介紹典型的使用場景,

開門紅

我們已經知道了NodeJS自帶的fs模塊比較基礎,把一個目錄里的所有檔案和子目錄都拷貝到另一個目錄里需要寫不少代碼,另外我們也知道,終端下的cp命令比較好用,一條cp -r source/* target命令就能搞定目錄拷貝,那我們首先看看如何使用NodeJS呼叫終端命令來簡化目錄拷貝,示例代碼如下:

var child_process = require('child_process');
var util = require('util');

function copy(source, target, callback) {
    child_process.exec(
        util.format('cp -r %s/* %s', source, target), callback);
}

copy('a', 'b', function (err) {
    // ...
});

從以上代碼中可以看到,子行程是異步運行的,通過回呼函式回傳執行結果,

API走馬觀花

我們先大致看看NodeJS提供了哪些和行程管理有關的API,這里并不逐一介紹每個API的使用方法,官方檔案已經做得很好了,

Process

官方檔案: http://nodejs.org/api/process.html

任何一個行程都有啟動行程時使用的命令列引數,有標準輸入標準輸出,有運行權限,有運行環境和運行狀態,在NodeJS中,可以通過process物件感知和控制NodeJS自身行程的方方面面,另外需要注意的是,process不是內置模塊,而是一個全域物件,因此在任何地方都可以直接使用,

Child Process

官方檔案: http://nodejs.org/api/child_process.html

使用child_process模塊可以創建和控制子行程,該模塊提供的API中最核心的是.spawn,其余API都是針對特定使用場景對它的進一步封裝,算是一種語法糖,

Cluster

官方檔案: http://nodejs.org/api/cluster.html

cluster模塊是對child_process模塊的進一步封裝,專用于解決單行程NodeJS Web服務器無法充分利用多核CPU的問題,使用該模塊可以簡化多行程服務器程式的開發,讓每個核上運行一個作業行程,并統一通過主行程監聽埠和分發請求,

應用場景

和行程管理相關的API單獨介紹起來比較枯燥,因此這里從一些典型的應用場景出發,分別介紹一些重要API的使用方法,

如何獲取命令列引數

在NodeJS中可以通過process.argv獲取命令列引數,但是比較意外的是,node執行程式路徑和主模塊檔案路徑固定占據了argv[0]argv[1]兩個位置,而第一個命令列引數從argv[2]開始,為了讓argv使用起來更加自然,可以按照以下方式處理,

function main(argv) {
    // ...
}

main(process.argv.slice(2));

如何退出程式

通常一個程式做完所有事情后就正常退出了,這時程式的退出狀態碼為0,或者一個程式運行時發生了例外后就掛了,這時程式的退出狀態碼不等于0,如果我們在代碼中捕獲了某個例外,但是覺得程式不應該繼續運行下去,需要立即退出,并且需要把退出狀態碼設定為指定數字,比如1,就可以按照以下方式:

try {
    // ...
} catch (err) {
    // ...
    process.exit(1);
}

如何控制輸入輸出

NodeJS程式的標準輸入流(stdin)、一個標準輸出流(stdout)、一個標準錯誤流(stderr)分別對應process.stdinprocess.stdoutprocess.stderr,第一個是只讀資料流,后邊兩個是只寫資料流,對它們的操作按照對資料流的操作方式即可,例如,console.log可以按照以下方式實作,

function log() {
    process.stdout.write(
        util.format.apply(util, arguments) + '\n');
}

如何降權

在Linux系統下,我們知道需要使用root權限才能監聽1024以下埠,但是一旦完成埠監聽后,繼續讓程式運行在root權限下存在安全隱患,因此最好能把權限降下來,以下是這樣一個例子,

http.createServer(callback).listen(80, function () {
    var env = process.env,
        uid = parseInt(env['SUDO_UID'] || process.getuid(), 10),
        gid = parseInt(env['SUDO_GID'] || process.getgid(), 10);

    process.setgid(gid);
    process.setuid(uid);
});

上例中有幾點需要注意:

  1. 如果是通過sudo獲取root權限的,運行程式的用戶的UID和GID保存在環境變數SUDO_UIDSUDO_GID里邊,如果是通過chmod +s方式獲取root權限的,運行程式的用戶的UID和GID可直接通過process.getuidprocess.getgid方法獲取,
  2. process.setuidprocess.setgid方法只接受number型別的引數,
  3. 降權時必須先降GID再降UID,否則順序反過來的話就沒權限更改程式的GID了,

如何創建子行程

以下是一個創建NodeJS子行程的例子,

var child = child_process.spawn('node', [ 'xxx.js' ]);

child.stdout.on('data', function (data) {
    console.log('stdout: ' + data);
});

child.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
});

child.on('close', function (code) {
    console.log('child process exited with code ' + code);
});

上例中使用了.spawn(exec, args, options)方法,該方法支持三個引數,第一個引數是執行檔案路徑,可以是執行檔案的相對或絕對路徑,也可以是根據PATH環境變數能找到的執行檔案名,第二個引數中,陣列中的每個成員都按順序對應一個命令列引數,第三個引數可選,用于配置子行程的執行環境與行為,

另外,上例中雖然通過子行程物件的.stdout.stderr訪問子行程的輸出,但通過options.stdio欄位的不同配置,可以將子行程的輸入輸出重定向到任何資料流上,或者讓子行程共享父行程的標準輸入輸出流,或者直接忽略子行程的輸入輸出,

行程間如何通訊

在Linux系統下,行程之間可以通過信號互相通信,以下是一個例子,

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ]);

child.kill('SIGTERM');

/* child.js */
process.on('SIGTERM', function () {
    cleanUp();
    process.exit(0);
});

在上例中,父行程通過.kill方法向子行程發送SIGTERM信號,子行程監聽process物件的SIGTERM事件回應信號,不要被.kill方法的名稱迷惑了,該方法本質上是用來給行程發送信號的,行程收到信號后具體要做啥,完全取決于信號的種類和行程自身的代碼,

另外,如果父子行程都是NodeJS行程,就可以通過IPC(行程間通訊)雙向傳遞資料,以下是一個例子,

/* parent.js  這是父行程*/
//父行程在創建子行程,在options.stdio欄位中通過ipc開啟了一條IPC通道,之后就可以監聽子行程物件的message事件接收來自子行程的訊息
var child = child_process.spawn('node', [ 'child.js' ], {
        stdio: [ 0, 1, 2, 'ipc' ]
    });

child.on('message', function (msg) {
    console.log(msg);
});

//通過.send方法給子行程發送訊息
child.send({ hello: 'hello' });

/* child.js  這是子行程*/
//在子行程這邊,可以在process物件上監聽message事件接收來自父行程的訊息
process.on('message', function (msg) {
    msg.hello = msg.hello.toUpperCase();
    //并通過.send方法向父行程發送訊息,
    process.send(msg);
});

可以看到,父行程在創建子行程時,在options.stdio欄位中通過ipc開啟了一條IPC通道,之后就可以監聽子行程物件的message事件接收來自子行程的訊息,并通過.send方法給子行程發送訊息,在子行程這邊,可以在process物件上監聽message事件接收來自父行程的訊息,并通過.send方法向父行程發送訊息,資料在傳遞程序中,會先在發送端使用JSON.stringify方法序列化,再在接收端使用JSON.parse方法反序列化

如何守護子行程

守護行程一般用于監控作業行程的運行狀態,在作業行程不正常退出時重啟作業行程,保障作業行程不間斷運行,以下是一種實作方式,

/* daemon.js */
function spawn(mainModule) {
    var worker = child_process.spawn('node', [ mainModule ]);

    worker.on('exit', function (code) {
        if (code !== 0) {
            spawn(mainModule);
        }
    });
}

spawn('worker.js');

可以看到,作業行程非正常退出時,守護行程立即重啟作業行程,

小結

本章介紹了使用NodeJS管理行程時需要的API以及主要的應用場景,總結起來有以下幾點:

  • 使用process物件管理自身,
  • 使用child_process模塊創建和管理子行程,

2. 異步編程

NodeJS最大的賣點——事件機制和異步IO,對開發者并不是透明的,開發者需要按異步方式撰寫代碼才用得上這個賣點,而這一點也遭到了一些NodeJS反對者的抨擊,但不管怎樣,異步編程確實是NodeJS最大的特點,沒有掌握異步編程就不能說是真正學會了NodeJS,本章將介紹與異步編程相關的各種知識,

回呼

在代碼中,異步編程的直接體現就是回呼,異步編程依托于回呼來實作,但不能說使用了回呼后程式就異步化了,我們首先可以看看以下代碼,

function heavyCompute(n, callback) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }

    callback(count);
}

heavyCompute(10000, function (count) {
    console.log(count);
});

console.log('hello');

-- Console ------------------------------
100000000
hello

可以看到,以上代碼中的回呼函式仍然先于后續代碼執行,JS本身是單執行緒運行的,不可能在一段代碼還未結束運行時去運行別的代碼,因此也就不存在異步執行的概念,

但是,如果某個函式做的事情是創建一個別的執行緒或行程,并與JS主執行緒并行地做一些事情,并在事情做完后通知JS主執行緒,那情況又不一樣了,我們接著看看以下代碼,

setTimeout(function () {
    console.log('world');
}, 1000);

console.log('hello');

-- Console ------------------------------
hello
world

這次可以看到,回呼函式后于后續代碼執行了,如同上邊所說,JS本身是單執行緒的,無法異步執行,因此我們可以認為setTimeout這類JS規范之外的由運行環境提供的特殊函式做的事情是創建一個平行執行緒后立即回傳,讓JS主行程可以接著執行后續代碼,并在收到平行行程的通知后再執行回呼函式,除了setTimeoutsetInterval這些常見的,這類函式還包括NodeJS提供的諸如fs.readFile之類的異步API,

另外,我們仍然回到JS是單執行緒運行的這個事實上,這決定了JS在執行完一段代碼之前無法執行包括回呼函式在內的別的代碼,也就是說,即使平行執行緒完成作業了,通知JS主執行緒執行回呼函式了,回呼函式也要等到JS主執行緒空閑時才能開始執行,以下就是這么一個例子,

function heavyCompute(n) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }
}

var t = new Date();

setTimeout(function () {
    console.log(new Date() - t);
}, 1000);

heavyCompute(50000);

-- Console ------------------------------
8520

可以看到,本來應該在1秒后被呼叫的回呼函式因為JS主執行緒忙于運行其它代碼,實際執行時間被大幅延遲

代碼設計模式

異步編程有很多特有的代碼設計模式,為了實作同樣的功能,使用同步方式和異步方式撰寫的代碼會有很大差異,以下分別介紹一些常見的模式,

函式回傳值

使用一個函式的輸出作為另一個函式的輸入是很常見的需求,在同步方式下一般按以下方式撰寫代碼:

var output = fn1(fn2('input'));
// Do something.

而在異步方式下,由于函式執行結果不是通過回傳值,而是通過回呼函式傳遞,因此一般按以下方式撰寫代碼:

fn2('input', function (output2) {
    fn1(output2, function (output1) {
        // Do something.
    });
});

可以看到,這種方式就是一個回呼函式套一個回呼函多,套得太多了很容易寫出>形狀的代碼,

遍歷陣列

在遍歷陣列時,使用某個函式依次對資料成員做一些處理也是常見的需求,如果函式是同步執行的,一般就會寫出以下代碼:

var len = arr.length,
    i = 0;

for (; i < len; ++i) {
    arr[i] = sync(arr[i]);
}

// All array items have processed.

如果函式是異步執行的,以上代碼就無法保證回圈結束后所有陣列成員都處理完畢了,如果陣列成員必須一個接一個串行處理,則一般按照以下方式撰寫異步代碼:

(function next(i, len, callback) {
    if (i < len) {
        async(arr[i], function (value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function () {
    // All array items have processed.
}));

可以看到,以上代碼在異步函式執行一次并回傳執行結果后才傳入下一個陣列成員并開始下一輪執行,直到所有陣列成員處理完畢后,通過回呼的方式觸發后續代碼的執行,

如果陣列成員可以并行處理,但后續代碼仍然需要所有陣列成員處理完畢后才能執行的話,則異步代碼會調整成以下形式:

(function (i, len, count, callback) {
    for (; i < len; ++i) {
        (function (i) {
            async(arr[i], function (value) {
                arr[i] = value;
                if (++count === len) {
                    callback();
                }
            });
        }(i));
    }
}(0, arr.length, 0, function () {
    // All array items have processed.
}));

可以看到,與異步串行遍歷的版本相比,以上代碼并行處理所有陣列成員,并通過計數器變數來判斷什么時候所有陣列成員都處理完畢了,

例外處理

JS自身提供的例外捕獲和處理機制——try..catch..,只能用于同步執行的代碼,以下是一個例子,

function sync(fn) {
    return fn();
}

try {
    sync(null);
    // Do something.
} catch (err) {
    console.log('Error: %s', err.message);
}

-- Console ------------------------------
Error: object is not a function

可以看到,例外會沿著代碼執行路徑一直冒泡,直到遇到第一個try陳述句時被捕獲住,但由于異步函式會打斷代碼執行路徑,異步函式執行程序中以及執行之后產生的例外冒泡到執行路徑被打斷的位置時,如果一直沒有遇到try陳述句,就作為一個全域例外拋出,以下是一個例子,

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        callback(fn());
    }, 0);
}

try {
    async(null, function (data) {
        // Do something.
    });
} catch (err) {
    console.log('Error: %s', err.message);
}

-- Console ------------------------------
/home/user/test.js:4
        callback(fn());
                 ^
TypeError: object is not a function
    at null._onTimeout (/home/user/test.js:4:13)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因為代碼執行路徑被打斷了,我們就需要在例外冒泡到斷點之前用try陳述句把例外捕獲住,并通過回呼函式傳遞被捕獲的例外,于是我們可以像下邊這樣改造上邊的例子,

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        try {
            callback(null, fn());
        } catch (err) {
            callback(err);
        }
    }, 0);
}

async(null, function (err, data) {
    if (err) {
        console.log('Error: %s', err.message);
    } else {
        // Do something.
    }
});

-- Console ------------------------------
Error: object is not a function

可以看到,例外再次被捕獲住了,在NodeJS中,幾乎所有異步API都按照以上方式設計,回呼函式中第一個引數都是err,因此我們在撰寫自己的異步函式時,也可以按照這種方式來處理例外,與NodeJS的設計風格保持一致,

有了例外處理方式后,我們接著可以想一想一般我們是怎么寫代碼的,基本上,我們的代碼都是做一些事情,然后呼叫一個函式,然后再做一些事情,然后再呼叫一個函式,如此回圈,如果我們寫的是同步代碼,只需要在代碼入口點寫一個try陳述句就能捕獲所有冒泡上來的例外,示例如下,

function main() {
    // Do something.
    syncA();
    // Do something.
    syncB();
    // Do something.
    syncC();
}

try {
    main();
} catch (err) {
    // Deal with exception.
}

但是,如果我們寫的是異步代碼,就只有呵呵了,由于每次異步函式呼叫都會打斷代碼執行路徑,只能通過回呼函式來傳遞例外,于是我們就需要在每個回呼函式里判斷是否有例外發生,于是只用三次異步函式呼叫,就會產生下邊這種代碼,

function main(callback) {
    // Do something.
    asyncA(function (err, data) {
        if (err) {
            callback(err);
        } else {
            // Do something
            asyncB(function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    // Do something
                    asyncC(function (err, data) {
                        if (err) {
                            callback(err);
                        } else {
                            // Do something
                            callback(null);
                        }
                    });
                }
            });
        }
    });
}

main(function (err) {
    if (err) {
        // Deal with exception.
    }
});

可以看到,回呼函式已經讓代碼變得復雜了,而異步方式下對例外的處理更加劇了代碼的復雜度,如果NodeJS的最大賣點最后變成這個樣子,那就沒人愿意用NodeJS了,因此接下來會介紹NodeJS提供的一些解決方案,

域(Domain)

官方檔案: http://nodejs.org/api/domain.html

NodeJS提供了domain模塊,可以簡化異步代碼的例外處理,在介紹該模塊之前,我們需要首先理解“域”的概念,簡單的講,一個域就是一個JS運行環境,在一個運行環境中,如果一個例外沒有被捕獲,將作為一個全域例外被拋出,NodeJS通過process物件提供了捕獲全域例外的方法,示例代碼如下

process.on('uncaughtException', function (err) {
    console.log('Error: %s', err.message);
});

setTimeout(function (fn) {
    fn();
});

-- Console ------------------------------
Error: undefined is not a function

雖然全域例外有個地方可以捕獲了,但是對于大多數例外,我們希望盡早捕獲,并根據結果決定代碼的執行路徑,我們用以下HTTP服務器代碼作為例子:

function async(request, callback) {
    // Do something.
    asyncA(request, function (err, data) {
        if (err) {
            callback(err);
        } else {
            // Do something
            asyncB(request, function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    // Do something
                    asyncC(request, function (err, data) {
                        if (err) {
                            callback(err);
                        } else {
                            // Do something
                            callback(null, data);
                        }
                    });
                }
            });
        }
    });
}

http.createServer(function (request, response) {
    async(request, function (err, data) {
        if (err) {
            response.writeHead(500);
            response.end();
        } else {
            response.writeHead(200);
            response.end(data);
        }
    });
});

以上代碼將請求物件交給異步函式處理后,再根據處理結果回傳回應,這里采用了使用回呼函式傳遞例外的方案,因此async函式內部如果再多幾個異步函式呼叫的話,代碼就變成上邊這副鬼樣子了,為了讓代碼好看點,我們可以在每處理一個請求時,使用domain模塊創建一個子域(JS子運行環境),在子域內運行的代碼可以隨意拋出例外,而這些例外可以通過子域物件的error事件統一捕獲,于是以上代碼可以做如下改造:

function async(request, callback) {
    // Do something.
    asyncA(request, function (data) {
        // Do something
        asyncB(request, function (data) {
            // Do something
            asyncC(request, function (data) {
                // Do something
                callback(data);
            });
        });
    });
}

http.createServer(function (request, response) {
    var d = domain.create();

    d.on('error', function () {
        response.writeHead(500);
        response.end();
    });

    d.run(function () {
        async(request, function (data) {
            response.writeHead(200);
            response.end(data);
        });
    });
});

可以看到,我們使用.create方法創建了一個子域物件,并通過.run方法進入需要在子域中運行的代碼的入口點,而位于子域中的異步函式回呼函式由于不再需要捕獲例外,代碼一下子瘦身很多,

陷阱

無論是通過process物件的uncaughtException事件捕獲到全域例外,還是通過子域物件的error事件捕獲到了子域例外,在NodeJS官方檔案里都強烈建議處理完例外后立即重啟程式,而不是讓程式繼續運行,按照官方檔案的說法,發生例外后的程式處于一個不確定的運行狀態,如果不立即退出的話,程式可能會發生嚴重記憶體泄漏,也可能表現得很奇怪,

但這里需要澄清一些事實,JS本身的throw..try..catch例外處理機制并不會導致記憶體泄漏,也不會讓程式的執行結果出乎意料,但NodeJS并不是存粹的JS,NodeJS里大量的API內部是用C/C++實作的,因此NodeJS程式的運行程序中,代碼執行路徑穿梭于JS引擎內部和外部,而JS的例外拋出機制可能會打斷正常的代碼執行流程,導致C/C++部分的代碼表現例外,進而導致記憶體泄漏等問題,

因此,使用uncaughtExceptiondomain捕獲例外,代碼執行路徑里涉及到了C/C++部分的代碼時,如果不能確定是否會導致記憶體泄漏等問題,最好在處理完例外后重啟程式比較妥當,而使用try陳述句捕獲例外時一般捕獲到的都是JS本身的例外,不用擔心上訴問題,

小結

本章介紹了JS異步編程相關的知識,總結起來有以下幾點:

  • 不掌握異步編程就不算學會NodeJS,
  • 異步編程依托于回呼來實作,而使用回呼不一定就是異步編程,
  • 異步編程下的函式間資料傳遞、陣列遍歷和例外處理與同步編程有很大差別,
  • 使用domain模塊簡化異步代碼的例外處理,并小心陷阱,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/296610.html

標籤:其他

上一篇:(Framework7 移動webapp) Springboot 入門培訓 8 Component 模板MVVM與AJAX

下一篇:vuex的基本概念

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more