• 你不知道的JavaScript--Item27 异步编程异常解决方案


    1、JavaScript异步编程的两个核心难点

    异步I/O、事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络、文件访问功能,且使之在后端实现了较高的性能。然而异步风格也引来了一些麻烦,其中比较核心的问题是:

    1、函数嵌套过深

    JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成
    金字塔型结构。这不仅使得代码变难看难懂,更使得调试、重构的过程充满风险。

    2、异常处理

    回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂。这里主要讲讲异常处理。

    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 形态像什么,始终要记住的是,只能在回调内部处理源于回调的异步错误。

    3、未捕获异常的处理

    如果是从回调中抛出异常的,则由那个调用了回调的人负责捕获该异常。但如果异常从未被捕获,又会怎么样?这时,不同的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 语句生动了很多。不管在浏览器端还是服务器端,全局的异常处理器都应被视作最后一根救命稻草。请仅在调试时才使用它。

    4、几种解决方案

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

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

    参考:

    版权声明:本文为小平果原创文章,转载请注明:http://blog.csdn.net/i10630226

  • 相关阅读:
    Awk by Example--转载
    Linux sed Examples--转载
    EM算法——有隐含变量时,极大似然用梯度法搞不定只好来猜隐含变量期望值求max值了
    SVM最通俗的解读
    SVM中的线性分类器
    KD树——k=1时就是BST,里面的数学原理还是有不明白的地方,为啥方差划分?
    梯度下降法——得到的结果可能是局部最优值,如果凸函数则可保证梯度下降得到的是全局最优值
    搜索引擎——用户搜索意图的理解及其难点解析,本质是利用机器学习用户的意图分类
    深入浅出时序数据库之预处理篇——批处理和流处理,用户可定制,但目前流行influxdb没有做
    FreeWheel基于Go的实践经验漫谈——GC是大坑(关键业务场景不用),web框架尚未统一,和c++性能相比难说
  • 原文地址:https://www.cnblogs.com/dingxiaoyue/p/4948171.html
Copyright © 2020-2023  润新知