本篇文章主要介紹了Node.js 異步異常的處理與domain模組解析,具有一定的參考價值,感興趣的小伙伴們可以參考一下
非同步異常處理
非同步異常的特點
由於node的回調非同步特性,無法透過try catch來捕捉所有的異常:
try { process.nextTick(function () { foo.bar(); }); } catch (err) { //can not catch it }
而對於web服務而言,其實是非常希望這樣的:
//express风格的路由 app.get('/index', function (req, res) { try { //业务逻辑 } catch (err) { logger.error(err); res.statusCode = 500; return res.json({success: false, message: '服务器异常'}); } });
如果try catch能夠捕捉所有的異常,這樣我們可以在程式碼出現一些非預期的錯誤時,能夠記錄下錯誤的同時,友善的給呼叫者回傳一個500錯誤。可惜,try catch無法捕捉非同步中的異常。所以我們能做的只能是:
app.get('/index', function (req, res) { // 业务逻辑 }); process.on('uncaughtException', function (err) { logger.error(err); });
這個時候,雖然我們可以記錄下這個錯誤的日誌,而且進程也不會異常退出#,但是我們是沒有辦法對發現錯誤的請求友好返回的,只能夠讓它超時返回。
domain
在node v0.8+版本的時候,發布了一個模組domain。這個模組做的就是try catch所無法做到的:捕捉非同步回呼中出現的異常。
於是乎,我們上面那個無奈的例子好像有了解決的方案:
var domain = require('domain'); //引入一个domain的中间件,将每一个请求都包裹在一个独立的domain中 //domain来处理异常 app.use(function (req,res, next) { var d = domain.create(); //监听domain的错误事件 d.on('error', function (err) { logger.error(err); res.statusCode = 500; res.json({sucess:false, messag: '服务器异常'}); d.dispose(); }); d.add(req); d.add(res); d.run(next); }); app.get('/index', function (req, res) { //处理业务 });
我們透過中間件的形式,引入domain來處理非同步中的異常。當然,domain雖然捕捉到了異常,但是還是由於異常而導致的堆疊丟失會導致內存洩漏,所以出現這種情況的時候還是需要重啟這個進程的,有興趣的同學可以去看看domain-middleware這個domain中間件。
詭異的失效
我們的測試一切正常,當正式在生產環境中使用的時候,發現domain突然失效了!它竟然沒有捕捉到非同步中的異常,最終導致進程異常退出。經過一番排查,最後發現是由於引入了redis來存放session導致的。
var http = require('http'); var connect = require('connect'); var RedisStore = require('connect-redis')(connect); var domainMiddleware = require('domain-middleware'); var server = http.createServer(); var app = connect(); app.use(connect.session({ key: 'key', secret: 'secret', store: new RedisStore(6379, 'localhost') })); //domainMiddleware的使用可以看前面的链接 app.use(domainMiddleware({ server: server, killTimeout: 30000 }));
此時,當我們的業務邏輯程式碼中出現了異常,發現竟然沒有被domain捕獲!經過一番嘗試,終於將問題定位到了:
var domain = require('domain'); var redis = require('redis'); var cache = redis.createClient(6379, 'localhost'); function error() { cache.get('a', function () { throw new Error('something wrong'); }); } function ok () { setTimeout(function () { throw new Error('something wrong'); }, 100); } var d = domain.create(); d.on('error', function (err) { console.log(err); }); d.run(ok); //domain捕获到异常 d.run(error); //异常被抛出
奇怪了!都是非同步調用,為什麼前者被捕獲,後者卻沒辦法捕獲呢?
Domain剖析
回過頭來,我們來看看domain做了些什麼來讓我們捕獲異步的請求(代碼來自node v0.10.4,此部分可能正在快速變更優化)。
node事件循環機制
在看Domain的原理之前,我們先要了解nextTick和_tickCallback的兩個方法。
function laterCall() { console.log('print me later'); } process.nextTick(laterCallback); console.log('print me first');
上面這段程式碼寫過node的人都很熟悉,nextTick的功能就是把laterCallback放到下一個事件循環去執行。而_tickCallback方法則是一個非公開的方法,這個方法是在當前時間循環結束之後,呼叫之以繼續進行下一個事件循環的入口函數。
換而言之,node為事件循環維持了一個佇列,nextTick入隊,_tickCallback出列。
domain的實作
在了解了node的事件循環機制之後,我們再來看看domain做了些什麼。
domain本身其實是一個EventEmitter對象,它透過事件的方式來傳遞捕獲的錯誤。這樣我們在研究它的時候,就簡化到兩個點:
什麼時候觸發domain的error事件:
進程拋出了異常,沒有被任何的try catch捕獲到,這時候將會觸發整個process的processFatal,此時如果在domain包裹之中,將會在domain上觸發error事件,反之,將會在process上觸發uncaughtException事件。
domain如何在多個不同的事件循環中傳遞:
當domain被實例化之後,我們通常會呼叫它的run方法(如之前在web服務中的使用),來將某個函數在這個domain範例的包裹中執行。被包裹的函數在執行的時候,process.domain這個全域變數將會被指向這個domain實例。當這個事件循環中,拋出異常呼叫processFatal的時候,發現process.domain存在,就會在domain上觸發error事件。
在require引入domain模組之後,會重寫全域的nextTick和_tickCallback,注入一些domain相關的程式碼:
//简化后的domain传递部分代码 function nextDomainTick(callback) { nextTickQueue.push({callback: callback, domain: process.domain}); } function _tickDomainCallback() { var tock = nextTickQueue.pop(); //设置process.domain = tock.domain tock.domain && tock.domain.enter(); callback(); //清除process.domain tock.domain && tock.domain.exit(); } };
这个是其在多个事件循环中传递domain的关键:nextTick入队的时候,记录下当前的domain,当这个被加入队列中的事件循环被_tickCallback启动执行的时候,将新的事件循环的process.domain置为之前记录的domain。这样,在被domain所包裹的代码中,不管如何调用process.nextTick, domain将会一直被传递下去。
当然,node的异步还有两种情况,一种是event形式。因此在EventEmitter的构造函数有如下代码:
if (exports.usingDomains) { // if there is an active domain, then attach to it. domain = domain || require('domain'); if (domain.active && !(this instanceof domain.Domain)) { this.domain = domain.active; } }
实例化EventEmitter的时候,将会把这个对象和当前的domain绑定,当通过emit触发这个对象上的事件时,像_tickCallback执行的时候一样,回调函数将会重新被当前的domain包裹住。
而另一种情况,是setTimeout和setInterval,同样的,在timer的源码中,我们也可以发现这样的一句代码:
if (process.domain) timer.domain = process.domain;
跟EventEmmiter一样,之后这些timer的回调函数也将被当前的domain包裹住了。
node通过在nextTick, timer, event三个关键的地方插入domain的代码,让它们得以在不同的事件循环中传递。
更复杂的domain
有些情况下,我们可能会遇到需要更加复杂的domain使用。
domain嵌套:我们可能会外层有domain的情况下,内层还有其他的domain,使用情景可以在文档中找到
// create a top-level domain for the server var serverDomain = domain.create(); serverDomain.run(function() { // server is created in the scope of serverDomain http.createServer(function(req, res) { // req and res are also created in the scope of serverDomain // however, we'd prefer to have a separate domain for each request. // create it first thing, and add req and res to it. var reqd = domain.create(); reqd.add(req); reqd.add(res); reqd.on('error', function(er) { console.error('Error', er, req.url); try { res.writeHead(500); res.end('Error occurred, sorry.'); } catch (er) { console.error('Error sending 500', er, req.url); } }); }).listen(1337); });
为了实现这个功能,其实domain还会偷偷的自己维持一个domain的stack,有兴趣的童鞋可以在这里看到。
回头解决疑惑
回过头来,我们再来看刚才遇到的问题:为什么两个看上去都是同样的异步调用,却有一个domain无法捕获到异常?理解了原理之后不难想到,肯定是调用了redis的那个异步调用在抛出错误的这个事件循环内,是不在domain的范围之内的。我们通过一段更加简短的代码来看看,到底在哪里出的问题。
var domain = require('domain'); var EventEmitter = require('events').EventEmitter; var e = new EventEmitter(); var timer = setTimeout(function () { e.emit('data'); }, 10); function next() { e.once('data', function () { throw new Error('something wrong here'); }); } var d = domain.create(); d.on('error', function () { console.log('cache by domain'); }); d.run(next);
此时我们同样发现,错误不会被domain捕捉到,原因很清晰了:timer和e两个关键的对象在初始化的时候都时没有在domain的范围之内,因此,当在next函数中监听的事件被触发,执行抛出异常的回调函数时,其实根本就没有处于domain的包裹中,当然就不会被domain捕获到异常了!
其实node针对这种情况,专门设计了一个API:domain.add。它可以将domain之外的timer和event对象,添加到当前domain中去。对于上面那个例子:
d.add(timer); //or d.add(e);
将timer或者e任意一个对象添加到domain上,就可以让错误被domain捕获了。
再来看最开始redis导致domain无法捕捉到异常的问题。我们是不是也有办法可以解决呢?
其实对于这种情况,还是没有办法实现最佳的解决方案的。现在对于非预期的异常产生的时候,我们只能够让当前请求超时,然后让这个进程停止服务,之后重新启动。graceful模块配合cluster就可以实现这个解决方案。
domain十分强大,但不是万能的。希望在看过这篇文章之后,大家能够正确的使用domian,避免踩坑。
【相关推荐】
1. 免费js在线视频教程
3. php.cn独孤九贱(3)-JavaScript视频教程
以上是詳解js中的非同步異常處理與使用domian教程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

JavaScript是現代網站的核心,因為它增強了網頁的交互性和動態性。 1)它允許在不刷新頁面的情況下改變內容,2)通過DOMAPI操作網頁,3)支持複雜的交互效果如動畫和拖放,4)優化性能和最佳實踐提高用戶體驗。

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

Atom編輯器mac版下載
最受歡迎的的開源編輯器

MinGW - Minimalist GNU for Windows
這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

禪工作室 13.0.1
強大的PHP整合開發環境

WebStorm Mac版
好用的JavaScript開發工具