搜尋
首頁web前端js教程跟我學習javascript解決非同步程式設計異常方案_javascript技巧

一、JavaScript非同步程式設計的兩個核心困難

非同步I/O、事件驅動使得單一執行緒的JavaScript得以在不阻塞UI的情況下執行網路、檔案存取功能,且使其在後端實現了較高的效能。然而非同步風格也引來了一些麻煩,其中比較核心的問題是:

1、函數巢狀過深

JavaScript的非同步呼叫是基於回呼函數,當多個非同步事務多層依賴時,回呼函數會形成多層次的巢狀,程式碼變成
金字塔型結構。這不僅使得程式碼變難看難懂,更使得調試、重構的過程充滿風險。

2、異常處理

回呼巢狀不僅是讓程式碼變得雜亂,也使得錯誤處理更複雜。這裡主要講講異常處理。

二、異常處理

像許多時髦的語言一樣,JavaScript 也允許拋出異常,隨後再用一個try/catch 語句塊捕獲。如果拋出的異常未被捕獲,大多數JavaScript環境都會提供一個有用的堆疊軌跡。舉個例子,下面這段程式碼由於'{'為無效JSON 物件而拋出異常。

function JSONToObject(jsonStr) {
 return JSON.parse(jsonStr);
}
var obj = JSONToObject('{');
//SyntaxError: Unexpected end of input
//at Object.parse (native)
//at JSONToObject (/AsyncJS/stackTrace.js:2:15)
//at Object.<anonymous> (/AsyncJS/stackTrace.js:4:11)

堆疊軌跡不僅告訴我們哪裡拋出了錯誤,而且說明了最初出錯的地方:第4 行程式碼。遺憾的是,自頂向下地追蹤非同步錯誤起源並不都這麼直截了當。

非同步程式設計中可能拋出錯誤的情況有兩種:回呼函數錯誤、非同步函數錯誤。

1、回呼函數錯誤

如果從非同步回調中拋出錯誤,會發生什麼事?讓我們先來做個測試。

setTimeout(function A() {
 setTimeout(function B() {
 setTimeout(function C() {
  throw new Error('Something terrible has happened!');
 }, 0);
 }, 0);
}, 0);

上述應用的結果是一條極為簡短的堆疊軌跡。

Error: Something terrible has happened!
at Timer.C (/AsyncJS/nestedErrors.js:4:13)

等等,A 和B 發生了什麼事?為什麼它們沒有出現在堆疊軌跡中?這是因為執行C 的時候,非同步函數的上下文已經不存在了,A 和B 並不在記憶體堆疊裡。這3 個函數都是從事件佇列直接運行的。基於同樣的理由,利用try/catch 語句區塊並不能捕捉從非同步回呼中拋出的錯誤。另外回呼函數中的return也失去了意義。

try {
 setTimeout(function() {
 throw new Error('Catch me if you can!');
 }, 0);
} catch (e) {
console.error(e);
}

看到這裡的問題了嗎?這裡的try/catch 語句區塊只捕捉setTimeout函數本身內部發生的那些錯誤。因為setTimeout 非同步地運行其回調,所以即使延遲設定為0,回調拋出的錯誤也會直接流向應用程式。

總的來說,取用非同步回呼的函數即使包裝上try/catch 語句區塊,也只是無用之舉。 (特例是,該非同步函數確實是在同步地做某些事且容易出錯。例如,Node 的fs.watch(file,callback)就是這樣一個函數,它在目標檔案不存在時會拋出一個錯誤。)正因為此,Node.js 中的回呼幾乎總是接受一個錯誤作為其首個參數,這樣就允許回調自己來決定如何處理這個錯誤。

2、非同步函數錯誤

由於非同步函數是立刻回傳的,非同步事務中發生的錯誤是無法透過try-catch來捕捉的,只能採用由呼叫方提供錯誤處理回呼的方案來解決。

例如Node中常見的function (err, ...) {...}回呼函數,就是Node中處理錯誤的約定:即將錯誤作為回呼函數的第一個實參傳回。再例如HTML5中FileReader物件的onerror函數,會被用來處理非同步讀取檔案過程中的錯誤。

舉個例子,下面這個Node 應用程式嘗試非同步地讀取一個文件,也負責記錄下任何錯誤(如「文件不存在」)。

var fs = require('fs');
 fs.readFile('fhgwgdz.txt', function(err, data) {
 if (err) {
 return console.error(err);
 };
 console.log(data.toString('utf8'));
});

客戶端JavaScript 函式庫的一致性要稍微差些,不過最常見的模式是,針對成敗這兩種情形各規定一個單獨的回調。 jQuery 的Ajax 方法就遵循了這個模式。

$.get('/data', {
 success: successHandler,
 failure: failureHandler
});

不管API 形態像什麼,始終要記住的是,只能在回呼內部處理源自於回呼的非同步錯誤。

三、未捕獲異常的處理

如果是從回調中拋出異常的,則由那個調用了回調的人負責捕獲該異常。但如果異常從未被捕獲,又會怎麼樣?這時,不同的JavaScript環境有著不同的遊戲規則…

1. 在瀏覽器環境

現代瀏覽器會在開發人員控制台顯示那些未捕獲的異常,接著返回事件佇列。要修改這種行為,可以給window.onerror 附加一個處理器。如果windows.onerror 處理器回傳true,則能阻止瀏覽器的預設錯誤處理行為。

window.onerror = function(err) {
 return true; //彻底忽略所有错误
};

在成品应用中, 会考虑某种JavaScript 错误处理服务, 譬如Errorception。Errorception 提供了一个现成的windows.onerror 处理器,它向应用服务器报告所有未捕获的异常,接着应用服务器发送消息通知我们。

2. 在Node.js 环境中

在Node 环境中,window.onerror 的类似物就是process 对象的uncaughtException 事件。正常情况下,Node 应用会因未捕获的异常而立即退出。但只要至少还有一个uncaughtException 事件处理
器,Node 应用就会直接返回事件队列。

process.on('uncaughtException', function(err) {
 console.error(err); //避免了关停的命运!
});

但是,自Node 0.8.4 起,uncaughtException 事件就被废弃了。据其文档所言,对异常处理而言,uncaughtException 是一种非常粗暴的机制,请勿使用uncaughtException,而应使用Domain 对象。

Domain 对象又是什么?你可能会这样问。Domain 对象是事件化对象,它将throw 转化为'error'事件。下面是一个例子。

var myDomain = require('domain').create();
myDomain.run(function() {
 setTimeout(function() {
 throw new Error('Listen to me!')
 }, 50);
});
myDomain.on('error', function(err) {
 console.log('Error ignored!');
});

源于延时事件的throw 只是简单地触发了Domain 对象的错误处理器。

Error ignored!

很奇妙,是不是?Domain 对象让throw 语句生动了很多。不管在浏览器端还是服务器端,全局的异常处理器都应被视作最后一根救命稻草。请仅在调试时才使用它。

四、几种解决方案

下面对几种解决方案的讨论主要集中于上面提到的两个核心问题上,当然也会考虑其他方面的因素来评判其优缺点。

1、Async.js

首先是Node中非常著名的Async.js,这个库能够在Node中展露头角,恐怕也得归功于Node统一的错误处理约定。
而在前端,一开始并没有形成这么统一的约定,因此使用Async.js的话可能需要对现有的库进行封装。

Async.js的其实就是给回调函数的几种常见使用模式加了一层包装。比如我们需要三个前后依赖的异步操作,采用纯回调函数写法如下:

asyncOpA(a, b, (err, result) => {
 if (err) {
 handleErrorA(err);
 }
 asyncOpB(c, result, (err, result) => {
 if (err) {
  handleErrorB(err);
 }
 asyncOpB(d, result, (err, result) => {
  if (err) {
  handlerErrorC(err);
  }
  finalOp(result);
 });
 });
});

如果我们采用async库来做:

async.waterfall([
 (cb) => {
 asyncOpA(a, b, (err, result) => {
  cb(err, c, result);
 });
 },
 (c, lastResult, cb) => {
 asyncOpB(c, lastResult, (err, result) => {
  cb(err, d, result);
 })
 },
 (d, lastResult, cb) => {
 asyncOpC(d, lastResult, (err, result) => {
  cb(err, result);
 });
 }
], (err, finalResult) => {
 if (err) {
 handlerError(err);
 }
 finalOp(finalResult);
});

可以看到,回调函数由原来的横向发展转变为纵向发展,同时错误被统一传递到最后的处理函数中。
其原理是,将函数数组中的后一个函数包装后作为前一个函数的末参数cb传入,同时要求:

每一个函数都应当执行其cb参数;cb的第一个参数用来传递错误。我们可以自己写一个async.waterfall的实现:

let async = {
 waterfall: (methods, finalCb = _emptyFunction) => {
 if (!_isArray(methods)) {
  return finalCb(new Error('First argument to waterfall must be an array of functions'));
 }
 if (!methods.length) {
  return finalCb();
 }
 function wrap(n) {
  if (n === methods.length) {
  return finalCb;
  }
  return function (err, ...args) {
  if (err) {
   return finalCb(err);
  }
  methods[n](...args, wrap(n + 1));
  }
 }
 wrap(0)(false);
 }
};

Async.js还有series/parallel/whilst等多种流程控制方法,来实现常见的异步协作。

Async.js的问题:

在外在上依然没有摆脱回调函数,只是将其从横向发展变为纵向,还是需要程序员熟练异步回调风格。
错误处理上仍然没有利用上try-catch和throw,依赖于“回调函数的第一个参数用来传递错误”这样的一个约定。

2、Promise方案

ES6的Promise来源于Promise/A+。使用Promise来进行异步流程控制,有几个需要注意的问题,
把前面提到的功能用Promise来实现,需要先包装异步函数,使之能返回一个Promise:

function toPromiseStyle(fn) {
 return (...args) => {
 return new Promise((resolve, reject) => {
  fn(...args, (err, result) => {
  if (err) reject(err);
  resolve(result);
  })
 });
 };
}

这个函数可以把符合下述规则的异步函数转换为返回Promise的函数:

回调函数的第一个参数用于传递错误,第二个参数用于传递正常的结果。接着就可以进行操作了:

let [opA, opB, opC] = [asyncOpA, asyncOpB, asyncOpC].map((fn) => toPromiseStyle(fn));

opA(a, b)
 .then((res) => {
 return opB(c, res);
 })
 .then((res) => {
 return opC(d, res);
 })
 .then((res) => {
 return finalOp(res);
 })
 .catch((err) => {
 handleError(err);
 });

通过Promise,原来明显的异步回调函数风格显得更像同步编程风格,我们只需要使用then方法将结果传递下去即可,同时return也有了相应的意义:
在每一个then的onFullfilled函数(以及onRejected)里的return,都会为下一个then的onFullfilled函数(以及onRejected)的参数设定好值。

如此一来,return、try-catch/throw都可以使用了,但catch是以方法的形式出现,还是不尽如人意。

3、Generator方案

ES6引入的Generator可以理解为可在运行中转移控制权给其他代码,并在需要的时候返回继续执行的函数。利用Generator可以实现协程的功能。

将Generator与Promise结合,可以进一步将异步代码转化为同步风格:

function* getResult() {
 let res, a, b, c, d;
 try {
 res = yield opA(a, b);
 res = yield opB(c, res);
 res = yield opC(d);
 return res;
 } catch (err) {
 return handleError(err);
 }
}

然而我们还需要一个可以自动运行Generator的函数:

function spawn(genF, ...args) {
 return new Promise((resolve, reject) => {
 let gen = genF(...args);

 function next(fn) {
  try {
  let r = fn();
  if (r.done) {
   resolve(r.value);
  }
  Promise.resolve(r.value)
   .then((v) => {
   next(() => {
    return gen.next(v);
   });
   }).catch((err) => {
   next(() => {
    return gen.throw(err);
   })
   });
  } catch (err) {
   reject(err);
  }
 }

 next(() => {
  return gen.next(undefined);
 });
 });
}

用这个函数来调用Generator即可:

spawn(getResult)
 .then((res) => {
 finalOp(res);
 })
 .catch((err) => {
 handleFinalOpError(err);
 });

可见try-catch和return实际上已经以其原本面貌回到了代码中,在代码形式上也已经看不到异步风格的痕迹。

类似的功能有co/task.js等库实现。

4、ES7的async/await

ES7中将会引入async function和await关键字,利用这个功能,我们可以轻松写出同步风格的代码,
同时依然可以利用原有的异步I/O机制。

采用async function,我们可以将之前的代码写成这样:

async function getResult() {
 let res, a, b, c, d;
 try {
 res = await opA(a, b);
 res = await opB(c, res);
 res = await opC(d);
 return res;
 } catch (err) {
 return handleError(err);
 }
}

getResult();

和Generator & Promise方案看起来没有太大区别,只是关键字换了换。
实际上async function就是对Generator方案的一个官方认可,将之作为语言内置功能。

async function的缺点:

await只能在async function内部使用,因此一旦你写了几个async function,或者使用了依赖于async function的库,那你很可能会需要更多的async function。

目前处于提案阶段的async function还没有得到任何浏览器或Node.JS/io.js的支持。Babel转码器也需要打开实验选项,并且对于不支持Generator的浏览器来说,还需要引进一层厚厚的regenerator runtime,想在前端生产环境得到应用还需要时间。

以上就是本文的全部内容,希望对大家的学习有所帮助。

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
C和JavaScript:連接解釋C和JavaScript:連接解釋Apr 23, 2025 am 12:07 AM

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

從網站到應用程序:JavaScript的不同應用從網站到應用程序:JavaScript的不同應用Apr 22, 2025 am 12:02 AM

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

Python vs. JavaScript:比較用例和應用程序Python vs. JavaScript:比較用例和應用程序Apr 21, 2025 am 12:01 AM

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

C/C在JavaScript口譯員和編譯器中的作用C/C在JavaScript口譯員和編譯器中的作用Apr 20, 2025 am 12:01 AM

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

JavaScript在行動中:現實世界中的示例和項目JavaScript在行動中:現實世界中的示例和項目Apr 19, 2025 am 12:13 AM

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

JavaScript和Web:核心功能和用例JavaScript和Web:核心功能和用例Apr 18, 2025 am 12:19 AM

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

了解JavaScript引擎:實施詳細信息了解JavaScript引擎:實施詳細信息Apr 17, 2025 am 12:05 AM

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

Python vs. JavaScript:學習曲線和易用性Python vs. JavaScript:學習曲線和易用性Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

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

熱工具

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

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

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)