Heim  >  Artikel  >  Web-Frontend  >  Folgen Sie mir, um Javascript-Lösungen zur Lösung asynchroner Programmierausnahmen zu erlernen_Javascript-Fähigkeiten

Folgen Sie mir, um Javascript-Lösungen zur Lösung asynchroner Programmierausnahmen zu erlernen_Javascript-Fähigkeiten

WBOY
WBOYOriginal
2016-05-16 15:30:341270Durchsuche

1. Zwei Kernschwierigkeiten bei der asynchronen JavaScript-Programmierung

Asynchrone E/A und ereignisgesteuert ermöglichen es Single-Threaded-JavaScript, Netzwerk- und Dateizugriffsfunktionen auszuführen, ohne die Benutzeroberfläche zu blockieren, und eine höhere Leistung im Back-End zu erzielen. Der asynchrone Stil verursacht jedoch auch einige Probleme. Das Kernproblem ist:

1. Funktionen sind zu tief verschachtelt

Asynchrone JavaScript-Aufrufe basieren auf Rückruffunktionen. Wenn mehrere asynchrone Transaktionen mehrstufige Abhängigkeiten aufweisen, bilden die Rückruffunktionen eine mehrstufige Verschachtelung und der Code wird zu
Pyramidenstruktur. Dies macht den Code nicht nur hässlich und schwer verständlich, sondern birgt auch Risiken im Debugging- und Rekonstruktionsprozess.

2. Ausnahmebehandlung

Verschachtelte Rückrufe machen nicht nur den Code unübersichtlich, sondern machen auch die Fehlerbehandlung komplexer. Hier sprechen wir hauptsächlich über die Ausnahmebehandlung.

2. Ausnahmebehandlung

Wie viele moderne Sprachen ermöglicht JavaScript das Auslösen und anschließende Abfangen von Ausnahmen mithilfe eines Try/Catch-Blocks. Wenn die ausgelöste Ausnahme nicht abgefangen wird, bieten die meisten JavaScript-Umgebungen einen nützlichen Stacktrace. Der folgende Code löst beispielsweise eine Ausnahme aus, weil „{“ ein ungültiges JSON-Objekt ist.

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)

Der Stack-Trace verrät uns nicht nur, wo der Fehler ausgelöst wurde, sondern auch, wo er ursprünglich schief gelaufen ist: Zeile 4 des Codes. Leider ist es nicht immer einfach, den Ursprung eines asynchronen Fehlers von oben nach unten zu verfolgen.

Es gibt zwei Situationen, in denen Fehler bei der asynchronen Programmierung ausgelöst werden können: Rückruffunktionsfehler und asynchrone Funktionsfehler.

1. Rückruffunktionsfehler

Was passiert, wenn ein Fehler von einem asynchronen Rückruf ausgelöst wird? Machen wir zunächst einen Test.

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

Das Ergebnis der obigen Anwendung ist ein extrem kurzer Stack-Trace.

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

Moment, was ist mit A und B passiert? Warum erscheinen sie nicht im Stacktrace? Dies liegt daran, dass beim Ausführen von C der Kontext der asynchronen Funktion nicht mehr vorhanden ist und sich A und B nicht im Speicherstapel befinden. Diese drei Funktionen werden alle direkt aus der Ereigniswarteschlange ausgeführt. Aus dem gleichen Grund können von asynchronen Rückrufen ausgelöste Fehler nicht mithilfe von Try/Catch-Blöcken abgefangen werden. Darüber hinaus verliert auch die Rückgabe in der Callback-Funktion ihre Bedeutung.

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

Sehen Sie die Frage hier? Der try/catch-Block erfasst hier nur Fehler, die innerhalb der setTimeout-Funktion selbst auftreten. Da setTimeout seinen Rückruf asynchron ausführt, fließen vom Rückruf ausgelöste Fehler direkt an die Anwendung, selbst wenn die Verzögerung auf 0 gesetzt ist.

Im Allgemeinen ist eine Funktion, die einen asynchronen Rückruf verwendet, in einen Try/Catch-Anweisungsblock eingeschlossen, nutzlos. (Ein Sonderfall besteht darin, dass die asynchrone Funktion tatsächlich etwas synchron ausführt und fehleranfällig ist. Beispielsweise ist fs.watch(file,callback) von Node eine solche Funktion, die einen Fehler auslöst, wenn die Zieldatei nicht existiert. ) Aus diesem Grund akzeptieren Rückrufe in Node.js fast immer einen Fehler als erstes Argument, sodass der Rückruf selbst entscheiden kann, wie mit dem Fehler umgegangen wird.

2. Asynchroner Funktionsfehler

Da asynchrone Funktionen sofort zurückkehren, können Fehler, die bei asynchronen Transaktionen auftreten, nicht durch Try-Catch abgefangen werden und können nur durch den Aufrufer gelöst werden, der Rückrufe zur Fehlerbehandlung bereitstellt.

Zum Beispiel ist die Callback-Funktion der allgemeinen Funktion (err, ...) {...} in Node die Konvention für die Behandlung von Fehlern in Node: Der Fehler wird als erster tatsächlicher Parameter der Callback-Funktion zurückgegeben. Ein weiteres Beispiel ist die onerror-Funktion des FileReader-Objekts in HTML5, die zur Behandlung von Fehlern beim asynchronen Lesen von Dateien verwendet wird.

Zum Beispiel versucht die folgende Node-Anwendung, eine Datei asynchron zu lesen und ist auch für die Protokollierung etwaiger Fehler (z. B. „Datei existiert nicht“) verantwortlich.

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

Clientseitige JavaScript-Bibliotheken sind etwas weniger konsistent, aber das häufigste Muster besteht darin, einen separaten Rückruf für Erfolg und Misserfolg anzugeben. Die Ajax-Methoden von jQuery folgen diesem Muster.

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

Egal wie die API aussieht, denken Sie immer daran, dass asynchrone Fehler, die von Rückrufen herrühren, nur innerhalb des Rückrufs behandelt werden können.

3. Behandlung nicht erfasster Ausnahmen

Wenn durch einen Rückruf eine Ausnahme ausgelöst wird, ist die Person, die den Rückruf aufgerufen hat, für das Abfangen der Ausnahme verantwortlich. Aber was passiert, wenn die Ausnahme nie abgefangen wird? Derzeit gelten für verschiedene JavaScript-Umgebungen unterschiedliche Spielregeln ...

1. In der Browserumgebung

Moderne Browser zeigen diese nicht erfassten Ausnahmen in der Entwicklerkonsole an und kehren dann zur Ereigniswarteschlange zurück. Um dieses Verhalten zu ändern, fügen Sie einen Handler an window.onerror an. Wenn der Windows.onerror-Handler „true“ zurückgibt, kann das standardmäßige Fehlerbehandlungsverhalten des Browsers verhindert werden.

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

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

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn