Rumah  >  Artikel  >  hujung hadapan web  >  Ikuti saya untuk mempelajari penyelesaian javascript untuk menyelesaikan kemahiran exceptions_javascript pengaturcaraan tak segerak

Ikuti saya untuk mempelajari penyelesaian javascript untuk menyelesaikan kemahiran exceptions_javascript pengaturcaraan tak segerak

WBOY
WBOYasal
2016-05-16 15:30:341254semak imbas

1. Dua kesukaran teras dalam pengaturcaraan tak segerak JavaScript

I/O tak segerak dan dipacu peristiwa mendayakan JavaScript satu benang untuk melaksanakan fungsi akses rangkaian dan fail tanpa menyekat UI dan mencapai prestasi yang lebih tinggi pada bahagian belakang. Walau bagaimanapun, gaya tak segerak juga menyebabkan beberapa masalah Masalah utama ialah:

1. Fungsi bersarang terlalu dalam

Panggilan tak segerak JavaScript adalah berdasarkan fungsi panggil balik Apabila berbilang transaksi tak segerak mempunyai kebergantungan berbilang peringkat, fungsi panggil balik akan membentuk sarang berbilang peringkat dan kod itu menjadi
. Struktur piramid. Ini bukan sahaja menjadikan kod itu hodoh dan sukar difahami, tetapi juga menjadikan proses penyahpepijatan dan pembinaan semula penuh dengan risiko.

2. Pengendalian pengecualian

Panggilan balik bersarang bukan sahaja mengeruhkan kod, tetapi juga menjadikan pengendalian ralat lebih rumit. Di sini kita bercakap tentang pengendalian pengecualian.

2. Pengendalian pengecualian

Seperti kebanyakan bahasa moden, JavaScript membenarkan pengecualian dilemparkan dan seterusnya ditangkap menggunakan blok cuba/tangkap. Jika pengecualian yang dilemparkan tidak ditangkap, kebanyakan persekitaran JavaScript menyediakan jejak tindanan yang berguna. Sebagai contoh, kod berikut membuang pengecualian kerana '{' ialah objek JSON yang tidak sah.

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)

Jejak tindanan bukan sahaja memberitahu kami di mana ralat itu dilemparkan, tetapi juga di mana ralat itu berlaku pada asalnya: baris 4 kod. Malangnya, mengesan asal ralat tak segerak dari atas ke bawah tidak selalunya mudah.

Terdapat dua situasi di mana ralat mungkin dilemparkan dalam pengaturcaraan tak segerak: ralat fungsi panggil balik dan ralat fungsi tak segerak.

1. Ralat fungsi panggil balik

Apakah yang berlaku jika ralat dilemparkan daripada panggilan balik tak segerak? Jom buat ujian dulu.

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

Hasil aplikasi di atas adalah jejak tindanan yang sangat singkat.

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

Tunggu, apa yang berlaku kepada A dan B? Mengapa ia tidak muncul dalam jejak tindanan? Ini kerana apabila C dijalankan, konteks fungsi tak segerak tidak lagi wujud, dan A dan B tiada dalam timbunan memori. Ketiga-tiga fungsi ini semuanya dijalankan terus dari baris gilir acara. Atas sebab yang sama, ralat yang dilemparkan daripada panggilan balik tak segerak tidak boleh ditangkap menggunakan blok cuba/tangkap. Di samping itu, pengembalian dalam fungsi panggil balik juga kehilangan maknanya.

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

Lihat soalan di sini? Blok cuba/tangkap di sini hanya menangkap ralat yang berlaku dalam fungsi setTimeout itu sendiri. Oleh kerana setTimeout menjalankan panggilan baliknya secara tidak segerak, ralat yang dilemparkan oleh aliran panggil balik terus ke aplikasi walaupun kelewatan ditetapkan kepada 0.

Secara umum, walaupun fungsi yang menggunakan panggil balik tak segerak dibalut dalam blok pernyataan cuba/tangkap, ia tidak berguna. (Kes khas ialah fungsi tak segerak benar-benar melakukan sesuatu secara serentak dan terdedah kepada ralat. Contohnya, fs.watch(fail,panggilan balik) Node ialah fungsi sedemikian, yang akan membuang ralat apabila fail sasaran tidak wujud. ) Oleh sebab itu, panggilan balik dalam Node.js hampir selalu menerima ralat sebagai hujah pertama mereka, membenarkan panggilan balik itu sendiri memutuskan cara mengendalikan ralat.

2. Ralat fungsi tak segerak

Memandangkan fungsi tak segerak kembali serta-merta, ralat yang berlaku dalam transaksi tak segerak tidak boleh ditangkap melalui cuba-tangkap dan hanya boleh diselesaikan oleh pemanggil yang menyediakan panggilan balik pengendalian ralat.

Sebagai contoh, fungsi biasa (err, ...) {...} fungsi panggil balik dalam Node ialah konvensyen untuk mengendalikan ralat dalam Nod: ralat dikembalikan sebagai parameter sebenar pertama bagi fungsi panggil balik. Contoh lain ialah fungsi onerror objek FileReader dalam HTML5, yang akan digunakan untuk mengendalikan ralat semasa pembacaan tak segerak fail.

Sebagai contoh, aplikasi Node di bawah cuba membaca fail secara tak segerak dan juga bertanggungjawab untuk mengelog sebarang ralat (seperti "Fail tidak wujud").

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

Pustaka JavaScript pihak pelanggan agak kurang konsisten, tetapi corak yang paling biasa ialah menentukan panggilan balik yang berasingan untuk kejayaan dan kegagalan. Kaedah Ajax jQuery mengikut corak ini.

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

Tidak kira apa rupa API, sentiasa ingat bahawa ralat tak segerak yang berpunca daripada panggilan balik hanya boleh dikendalikan di dalam panggilan balik.

3 Pengendalian pengecualian yang tidak ditangkap

Jika pengecualian dilemparkan daripada panggilan balik, orang yang memanggil panggilan balik bertanggungjawab untuk menangkap pengecualian. Tetapi apa yang berlaku jika pengecualian tidak pernah ditangkap? Pada masa ini, persekitaran JavaScript yang berbeza mempunyai peraturan permainan yang berbeza...

1. Dalam persekitaran penyemak imbas

Pelayar moden akan memaparkan pengecualian yang tidak ditangkap tersebut dalam konsol pembangun dan kemudian kembali ke baris gilir acara. Untuk mengubah suai tingkah laku ini, lampirkan pengendali pada window.onerror. Jika pengendali windows.onerror mengembalikan benar, tingkah laku pengendalian ralat lalai penyemak imbas boleh dihalang.

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

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

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn