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

跟我學習javascript解決非同步程式設計異常方案_javascript技巧

WBOY
發布: 2016-05-16 15:30:34
原創
1277 人瀏覽過

一、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,想在前端生产环境得到应用还需要时间。

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

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板