javascript中异步操作的异常怎么处理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了javascript中异步操作的异常怎么处理相关的知识,希望对你有一定的参考价值。

一、javascript异步编程的两个核心难点
异步I/O、事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络、文件访问功能,且使之在后端实现了较高的性能。然而异步风格也引来了一些麻烦,其中比较核心的问题是:
1、函数嵌套过深
JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成

金字塔型结构。这不仅使得代码变难看难懂,更使得调试、重构的过程充满风险。
2、异常处理
回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂。这里主要讲讲异常处理。
二、异常处理
像很多时髦的语言一样,JavaScript 也允许抛出异常,随后再用一个try/catch
语句块捕获。如果抛出的异常未被捕获,大多数JavaScript环境都会提供一个有用的堆栈轨迹。举个例子,下面这段代码由于\'\'为无效JSON
对象而抛出异常。
?

12345678

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、回调函数错误
如果从异步回调中抛出错误,会发生什么事?让我们先来做个测试。
?

1234567

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

上述应用的结果是一条极其简短的堆栈轨迹。
?

12

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

等等,A 和B 发生了什么事?为什么它们没有出现在堆栈轨迹中?这是因为运行C 的时候,异步函数的上下文已经不存在了,A 和B 并不在内存堆栈里。这3
个函数都是从事件队列直接运行的。基于同样的理由,利用try/catch
语句块并不能捕获从异步回调中抛出的错误。另外回调函数中的return也失去了意义。
?

1234567

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 应用尝试异步地读取一个文件,还负责记录下任何错误(如“文件不存在”)。
?

1234567

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
方法就遵循了这个模式。
?

1234

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

不管API 形态像什么,始终要记住的是,只能在回调内部处理源于回调的异步错误。
三、未捕获异常的处理
如果是从回调中抛出异常的,则由那个调用了回调的人负责捕获该异常。但如果异常从未被捕获,又会怎么样?这时,不同的JavaScript环境有着不同的游戏规则……
1. 在浏览器环境中
现代浏览器会在开发人员控制台显示那些未捕获的异常,接着返回事件队列。要想修改这种行为,可以给window.onerror
附加一个处理器。如果windows.onerror 处理器返回true,则能阻止浏览器的默认错误处理行为。
?

123

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

在成品应用中, 会考虑某种JavaScript 错误处理服务, 譬如Errorception。Errorception
提供了一个现成的windows.onerror 处理器,它向应用服务器报告所有未捕获的异常,接着应用服务器发送消息通知我们。
2. 在Node.js 环境中
在Node 环境中,window.onerror 的类似物就是process 对象的uncaughtException 事件。正常情况下,Node
应用会因未捕获的异常而立即退出。但只要至少还有一个uncaughtException 事件处理
器,Node 应用就会直接返回事件队列。
?

123

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

但是,自Node 0.8.4 起,uncaughtException 事件就被废弃了。据其文档所言,对异常处理而言,uncaughtException
是一种非常粗暴的机制,请勿使用uncaughtException,而应使用Domain 对象。
Domain 对象又是什么?你可能会这样问。Domain 对象是事件化对象,它将throw 转化为\'error\'事件。下面是一个例子。
?

123456789

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

12345678910111213141516

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库来做:
?

12345678910111213141516171819202122

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的实现:
?

12345678910111213141516171819202122

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:
?

12345678910

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

这个函数可以把符合下述规则的异步函数转换为返回Promise的函数:
回调函数的第一个参数用于传递错误,第二个参数用于传递正常的结果。接着就可以进行操作了:
?

123456789101112131415

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结合,可以进一步将异步代码转化为同步风格:
?

1234567891011

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的函数:
?

123456789101112131415161718192021222324252627282930

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即可:
?

1234567

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,我们可以将之前的代码写成这样:
?

12345678910111213

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,想在前端生产环境得到应用还需要时间。
以上就是本文的全部内容,希望对大家的学习有所帮助。
参考技术A 什么异常,可以把错误代码贴一下吗

浅谈JavaScript中的异步处理

  • 在 JavaScript 的世界中,所有代码都是单线程执行的
  • 由于这个“缺陷”,导致 JavaScript 的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现
  • 异步操作会在将来的某个时间点触发一个函数调用
  • 主流的异步处理方案主要有:回调函数 (CallBack) 、 Promise 、 Generator 函数、 async/await 。

一、回调函数(CallBack)

  • 这是异步编程最基本的方法
  • 假设我们有一个 getData 方法,用于异步获取数据,第一个参数为请求的 url 地址,第二个参数是回调函数,如下:
function getData(url, callBack){
    // 模拟发送网络请求
    setTimeout(()=> {
        // 假设 res 就是返回的数据
        var res = {
            url: url,
            data: Math.random()
        }
        // 执行回调,将数据作为参数传递
        callBack(res)
    }, 1000)
}
  • 我们预先设定一个场景,假设我们要请求三次服务器,每一次的请求依赖上一次请求的结果,如下:
getData(‘/page/1?param=123‘, (res1) => {
    console.log(res1)
    getData(`/page/2?param=${res1.data}`, (res2) => {
        console.log(res2)
        getData(`/page/3?param=${res2.data}`, (res3) => {
            console.log(res3)
        })
    })
})
  • 通过上面的代码可以看出,第一次请求的 url 地址为: /page/1?param=123 ,返回结果为 res1 。

  • 第二个请求的 url 地址为: /page/2?param=${res1.data} ,依赖第 一次请求的 res1.data ,返回结果为 res2`。

  • 第三次请求的 url 地址为: /page/3?param=${res2.data} ,依赖第二次请求的 res2.data ,返回结果为 res3 。

  • 由于后续请求依赖前一个请求的结果,所以我们只能把下一次请求写到上一次请求的回调函数内部,这样就形成了常说的:回调地狱。

二、发布/订阅

我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”( publish )一个信号,其他任务可以向信号中心”订阅”( subscribe )这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)

  • 这个模式有多种实现,下面采用的是Ben Alman的 Tiny Pub/Sub ,这是 jQuery 的一个插件
  • 首先, f2 向”信号中心” jQuery 订阅” done “信号
jQuery.subscribe("done", f2);
  • f1进行如下改写
function f1(){
    setTimeout(function () {
      // f1的任务代码
      jQuery.publish("done");
    }, 1000);
}
  • jQuery.publish("done")的意思是,f1执行完成后,向”信号中心"jQuery发布"done"信号,从而引发f2的执行。 此外,f2完成执行后,也可以取消订阅(unsubscribe
jQuery.unsubscribe("done", f2);
  • 这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

三、Promise

  • Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大
  • 所谓 Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说, Promise 是一个对象,从它可以获取异步操作的消息。 Promise 提供统一的 API ,各种异步操作都可以用同样的方法进行处理
  • 简单说,它的思想是,每一个异步任务返回一个 Promise 对象,该对象有一个 then 方法,允许指定回调函数。
  • 现在我们使用 Promise 重新实现上面的案例,首先,我们要把异步请求数据的方法封装成 Promise
function getDataAsync(url){
    return new Promise((resolve, reject) => {
        setTimeout(()=> {
            var res = {
                url: url,
                data: Math.random()
            }
            resolve(res)
        }, 1000)
    })
}
  • 那么请求的代码应该这样写
getDataAsync(‘/page/1?param=123‘)
    .then(res1=> {
        console.log(res1)
        return getDataAsync(`/page/2?param=${res1.data}`)
    })
    .then(res2=> {
        console.log(res2)
        return getDataAsync(`/page/3?param=${res2.data}`)
    })
    .then(res3=> {
        console.log(res3)
    })
  • then 方法返回一个新的 Promise 对象, then 方法的链式调用避免了 CallBack 回调地狱
  • 但也并不是完美,比如我们要添加很多 then 语句, 每一个 then 还是要写一个回调。
  • 如果场景再复杂一点,比如后边的每一个请求依赖前面所有请求的结果,而不仅仅依赖上一次请求的结果,那会更复杂。 为了做的更好, async/await 就应运而生了,来看看使用 async/await 要如何实现

四、async/await

  • getDataAsync 方法不变,如下

 function getDataAsync(url){
    return new Promise((resolve, reject) => {
        setTimeout(()=> {
            var res = {
                url: url,
                data: Math.random()
            }
            resolve(res)
        }, 1000)
    })
}
  • 业务代码如下
async function getData(){
    var res1 = await getDataAsync(‘/page/1?param=123‘)
    console.log(res1)
    var res2 = await getDataAsync(`/page/2?param=${res1.data}`)
    console.log(res2)
    var res3 = await getDataAsync(`/page/2?param=${res2.data}`)
    console.log(res3)
}
  • 可以看到使用 async\await 就像写同步代码一样
  • 对比 Promise 感觉怎么样?是不是非常清晰,但是 async/await 是基于 Promise 的,因为使用 async 修饰的方法最终返回一个 Promise , 实际上, async/await 可以看做是使用 Generator 函数处理异步的语法糖,我们来看看如何使用 Generator 函数处理异步

五、Generator

  • 首先异步函数依然是
function getDataAsync(url){
    return new Promise((resolve, reject) => {
        setTimeout(()=> {
            var res = {
                url: url,
                data: Math.random()
            }
            resolve(res)
        }, 1000)
    })
}
  • 使用 Generator 函数可以这样写
function*getData(){
    var res1 = yield getDataAsync(‘/page/1?param=123‘)
    console.log(res1)
    var res2 = yield getDataAsync(`/page/2?param=${res1.data}`)
    console.log(res2)
    var res3 = yield getDataAsync(`/page/2?param=${res2.data}`)
    console.log(res3))
}
  • 然后我们这样逐步执行
var g = getData()
g.next().value.then(res1=> {
    g.next(res1).value.then(res2=> {
        g.next(res2).value.then(()=> {
            g.next()
        })
    })
})
  • 上面的代码,我们逐步调用遍历器的 next() 方法,由于每一个 next() 方法返回值的 value 属性为一个 Promise 对象
  • 所以我们为其添加 then 方法, 在 then 方法里面接着运行 next 方法挪移遍历器指针,直到 Generator 函数运行完成,实际上,这个过程我们不必手动完成,可以封装成一个简单的执行器
function run(gen){
    var g = gen()

    function next(data){
        var res = g.next(data)
        if (res.done) return res.value
        res.value.then((data) => {
            next(data)
        })
    }

    next()

}

run 方法用来自动运行异步的 Generator 函数,其实就是一个递归的过程调用的过程。这样我们就不必手动执行 Generator 函数了。 有了 run 方法,我们只需要这样运行 getData 方法

run(getData)

这样,我们就可以把异步操作封装到 Generator 函数内部,使用 run 方法作为 Generator 函数的自执行器,来处理异步。其实我们不难发现, async/await 方法相比于 Generator 处理异步的方式,有很多相似的地方,只不过 async/await 在语义化方面更加明显,同时 async/await 不需要我们手写执行器,其内部已经帮我们封装好了,这就是为什么说 async/await 是 Generator 函数处理异步的语法糖了

以上是关于javascript中异步操作的异常怎么处理的主要内容,如果未能解决你的问题,请参考以下文章

Effective JavaScript Item 63 注意异步调用中可能会被忽略的异常

C#多线程开发-处理异步操作中的异常

异步操作的使用场景

谈一谈几种处理 JavaScript 异步操作的办法

react常见面试题

浅谈JavaScript中的异步处理