先上传统文件加载方式代码,传统方式在处理多层嵌套时代码比较混乱

js异常处理总结

async /
await是ES7的重要特性之一,也是目前社区里公认的优秀异步解决方案。目前,async
/ await这个特性已经是stage
3的建议,可以看看TC39的进度,本篇文章将分享async /
await是如何工作的,阅读本文前,希望你具备Promise、generator、yield等ES6的相关知识。

const fs = require('fs')  //引入文件系统 

function readFile (cb) {
    fs.readFile('./package.json',(err,data) => {
        if(err) return console.log(err)     
        cb(null,data)
    })
}
//回调函数
readFile((err, data) => {
    if(!err) {
        data = JSON.parse(data)
        console.log(data.name)
    }
})

先看最基础的情况

function children() {
    throw new Error("子报错");
}

function parent() {
    children(); //有异常抛出 函数中断执行
}
parent();
console.log("cccccccc");

try catch 单层嵌套

function children() {
    throw new Error("子报错");
}
function parent() {
    //可以在上一层函数捕获下层函数的异常
    try {
        children(); //有异常抛出 函数中断执行
    } catch(error) {
        console.log(error);
    }
}
parent();

多级嵌套,捕获下面的异常

function children() {
    throw new Error("子报错");
}

function parent() {
    children(); //有异常抛出 函数中断执行
}

//多级嵌套也是没问题的, 异常回层层往上抛
try {
    parent();
} catch (error) {
    console.log(error);
}
  1. 预期异常:参数不合法,前提条件不符合,通常直接throw
  2. 非预期异常: js运行时异常,来着依赖库异常
  3. 可以直接在异常上面提供一下附加属性来提供上下文

function children() {
    var err =  new Error("子报错");
    err.statusCode = 404;  //附加的属性 提供上下文
    throw err;
}

function parent() {
    children(); //有异常抛出 函数中断执行
}

try {
    parent();
} catch (error) {
    console.log(error.statusCode); //404
    console.log(error);
}

在详细介绍async /
await之前,先回顾下目前在ES6中比较好的异步处理办法。下面的例子中数据请求用Node.js中的request模块,数据接口采用Github
v3 api文档提供的repo代码仓库详情API作为例子演示。

第二阶段 promsie 新建一个promise对象读取文件成功是返回 resolve(data)
失败是返回 rejext, promise.then里可以得到返回结果

异步回调异常处理

function asyncCallbackError(callback) {

    setTimeout(() => {
        throw new Error("异步出现了异常");
        callback();
    }, 0);
}

function callAsync() {
    asyncCallbackError(function() {
    });
}

//能不能在callAsync外面嵌套一个 try catch 处理异常呢?

try {
    callAsync();
} catch(error) {
    console.log(error);
}

上面的代码是不行的,执行栈里面的try catch 无法捕获异步队列中抛出的异常

为啥 执行栈中的try catch无法捕获到异步队列的异常?
执行栈都执行完了,异步队列才开始执行,所以执行栈无法捕获异步
函数抛出的异常

所以对于异步函数,我们对异常的处理原则为, 异步函数里面使用自己的try catch 
自处理异常,然后通过它的回调函数的参数 callback(error, value) 在上一层的调用
中判断是否异步出现了异常,如果出现的话, error即第一个参数不为空

function asyncCallbackError(callback) {

    setTimeout(() => {

        //异步函数必须自己处理自己的异常
        try {

            //if (发送了异常) {
            throw new Error("异步出现了异常");
            //}
            //else  没有异常 {
            //callback(null, value);  无异常的话 第一个参数设置为null 第二个自己的值
            //}
        } catch(error) {
            callback(error);
        }
    }, 0);
}

function callAsync() {

    //callback 判断获取异步异常值
    asyncCallbackError((error, value) => {
        if (error) { //如果异步出现异常
            console.log(error);
        } else {
        }
    });
}

callAsync();

Promise对异步的处理

function readfileAsync (path) {
    return new Promise((resolve,reject) => {  
        fs.readFile(path,(err,data) => {
            if(err){
                 reject(err)
            } else {
                 resolve(data)
            }         
        })
    })
}

readfileAsync('./package.json').then(data => {
    data = JSON.parse(data)
    console.log(data.name)
})
.catch(err => {
    console.log(err);
})

promise 异常

promise本身只是一个 基于事件分发的状态管理器
它不为我们管理异常,所以promise里面的异常必须我们自己try catch
当 发生异常的时候 就设置当前 promise的状态为 reject

十分注意一点是: 如果一个 reject状态的promise没有进行处理,那么它

var p = new Promise((resolve, reject) => {
    throw new Error("异常发生了");
    resolve();
});

console.log(p);

上面的代码
会直接抛出一个异常,程序无法执行,因为promsie不会为我们自动处理异常

var p = new Promise((resolve, reject) => {
    try {
        throw new Error("异常发生了");
    } catch(error) {

        //不让它的状态立即改变,防止reject进入异步队列,这样可以让then注册的回调不是立即执行
        //tips: 一个promise状态确定后,通过then注册的回调 会立即执行
        setTimeout(() => {
            reject(error); //内部直接主动管理异常
        }, 0);
    }
});

p.then(() => {console.log("resolve")}, (error) => { console.log(error)});

promise内部发生异常,统一我们自己在内部try catch
处理,并设置它的状态为reject
然后通过then 注册reject回调处理,即promise异常处理通过reject状态处理,
而且这里
再次强调一下,如果一个promise的reject状态没得到处理的话,会抛出一个异常

var p = new Promise(function (resolve, reject) {
    setTimeout(reject, 0);
  });

p.then(() => {});

上面的代码报异常没有捕获,promise状态为reject的话,需要处理reject情况
改成 p.then(()=>{}).catch(()=> {});就可以了

注意:
目前node里面Promise的reject没有处理,抛的异常不会阻止程序的执行,但是
未来这种情况会中断node的执行

虽然Node.js的异步IO带来了对高并发的良好支持,同时也让“回调”成为灾难,很容易造成回调地狱。传统的方式比如使用具名函数,虽然可以减少嵌套的层数,让代码看起来比较清晰。但是会造成比较差的编码和调试体验,你需要经常使用用ctrl
+
f去寻找某个具名函数的定义,这使得IDE窗口经常上下来回跳动。使用Promise之后,可以很好的减少嵌套的层数。另外Promise的实现采用了状态机,在函数里面可以很好的通过resolve和reject进行流程控制,你可以按照顺序链式的去执行一系列代码逻辑了。下面是使用Promise的一个例子:

co + generator function

generator的异常

如果在一个generator 函数体内抛出一个异常 它会怎么样呢?

var g = function* () {
throw new Error(“异常发生了”);
yield console.log(‘yielding’);
};

var i = g();
console.log(i.next());

结果程序遇到异常 也直接不执行了,所以generator也不会帮我们处理异常
我们需要自己手动处理

var g = function* () {
    try {
        throw new Error("异常发生了");
    } catch(error) {
        yield console.log('yielding');
    }

};

var i = g();
console.log(i.next());

generator 函数体内抛出的异常 还能在函数体外捕获
但是注意捕获的时间,哪个next 执行会抛出异常,就在哪次上门捕获
当然也可以 try catch 包含多个next

var g = function* () {
    throw new Error("异常发生了");
    yield console.log('yielding');
};

var i = g();
try {
    console.log(i.next());
} catch(error) {
    console.log(error);
}

嵌套多个next 可能抛出的异常 异常

var g = function* () {
    yield console.log('yielding');
    throw new Error("异常发生了");
};

var i = g();
try {
    i.next();
    i.next(); //第二个才会抛出异常
} catch(error) {
    console.log(error);
}

yield 自带异常api Generator.prototype.throw()

Generator函数返回的遍历器对象,都有一个throw方法,
可以在函数体外抛出错误,然后在Generator函数体内捕获。

应该在 函数体里面的哪里捕获呢? 想想 函数体外利用throw抛异常,
如何函数体内可以接受这个异常的话,那是不是说明现在的函数执行流程应该有
跑回到 generator函数里面,而generator会从上一个的yeild
左表达式开始执行,
所以,为了捕获异常,我们应该 try catch 前一个yield

当然 它也是可以直接try catch 多个yeild
如果不确定那个抛出的话,只要在对应的
抛出异常的外传yeild就可以了

var g = function* () {
    try {
        yield console.log('yielding'); //从这里断开的 需要从左表达式处接受异常
    } catch(error) {
        console.log(error);
    }
};

var i = g();
i.next();
i.throw(new Error("外部异常")); //throw 传递的参数可以传递到generator里面

如果连续利用多个 generator.throw
那么如果异常无法在generator函数体内进行捕获,
那么它就会在函数generator体外抛出这个异常 我们可以在体外处理

注意: throw的话 相当于 一个next 并且 同时throw Error
所以它会对生成器函数内部迭代一次

var g = function* () {
    try {
        yield console.log('yielding'); //从这里断开的 需要从左表达式处接受异常
    } catch(error) {
        console.log(error);
    }
};

var i = g();
i.next();
i.throw(new Error("外部异常")); //throw 传递的参数可以传递到generator里面
i.throw(new Error("外部异常2"));  //这个异常无法在generator函数里面捕获,所以它往外面抛 

//所以改写为
var i = g();
i.next();
i.throw(new Error("外部异常")); //throw 传递的参数可以传递到generator里面
try {
    i.throw(new Error("外部异常2"));  //这个异常无法在generator函数里面捕获,所以它往外面抛 
} catch (error) {
    console.log(error);
}

所以对于生成器函数来说 我们使用 它的 .throw函数抛出异常
在生成器函数里面使用 try catch 捕获 yield异常,
内部无法捕获的话,使用 try catch在对应的next处捕获

const request = require('request');
// 请求的url和header
const options = {
 url: 'https://api.github.com/repos/cpselvis/zhihu-crawler',
 headers: {
  'User-Agent': 'request'
 }
};
// 获取仓库信息
const getRepoData = () => {
 return new Promise((resolve, reject) => {
  request(options, (err, res, body) => {
   if (err) {
    reject(err);
   }
   resolve(body);
  });
 });
};

getRepoData()
 .then((result) => console.log(result);)
 .catch((reason) => console.error(reason););

// 此处如果是多个Promise顺序执行的话,如下:
// 每个then里面去执行下一个promise
// getRepoData()
//  .then((value2) => {return promise2})
//  .then((value3) => {return promise3})
//  .then((x) => console.log(x))

Generator
(*)函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态,每次调用

必发88官网,await async异常

先直接在async里面抛出异常看看

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log(v),
  e => console.log(e)
)

上面并不会报错,
不会终止程序的执行
,因为async函数是一个自执行的generator,它里面会捕获异常,
然后返回一个reject的promise,
所以我们可以在async里面把异常转为promise的reject

所以async无论如何都会返回一个promise,如果内部有异常,那么返回一个reject的promise,
value为异常error,
如果返回一个值,那么返回一个resolve的promise,value为这个值,如果
返回一个promise,那么async就直接返回这个promise

只要有一个await后面的promise是reject,
那么async就会中断,并且返回一个reject的promise,
(正如之前说过promise为reject的话,而且未处理,那么它会抛出一个异常)

那 如果我想 即使
await后面是一个reject的promise,我如何还能让它往下执行呢?
可以用一个 try catch 将对应的await
包裹住,这样的话,它就可以捕获promise未处理抛出的异常
或者把这个promise给处理了

async 处理promise reject异常的方法
try catch 方法

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

把未处理的 promise给处理了 即利用.catch 这样的话
返回一个resolve的promise

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world

为何async内部会帮我们处理异常? 如何帮的?

//代码来自阮一峰es6

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    var gen = genF();
    function step(nextF) {

      //可以看到 每次迭代都将nextF用try catch 包裹起来, 
      //当生成器执行过程中抛出异常时,就可以在外部捕获异常,并且立即返回 reject(e)      
      try {  
        var next = nextF();
      } catch(e) {
        return reject(e); //有异常的话 直接返回一个reject promise
      }
      //需要理解的是 为啥在这里捕获异常??? 前面我们已经说过,生成器是一个迭代器,按照next的流程执行,
      // 只有next执行的时候,generator才会开始执行,执行才可能发生异常,而且生成   
      // 器在next执行期间,内部发生的
      //异常没有被捕获的话,可以往外抛,即再对应的next函数处,我们可以捕获异常
      // nextF()的执行 恰好就是执行generator的next函数

      if(next.done) {
        return resolve(next.value);  //async的状态直到 generator全部执行完 才确定
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        //发生错误,往生成器里面抛异常,如果这个异常在生成器内部没有被捕获,那么
       //它可以往外抛
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

不过Promise仍然存在缺陷,它只是减少了嵌套,并不能完全消除嵌套。举个例子,对于多个promise串行执行的情况,第一个promise的逻辑执行完之后,我们需要在它的then函数里面去执行第二个promise,这个时候会产生一层嵌套。另外,采用Promise的代码看起来依然是异步的,如果写的代码如果能够变成同步该多好啊!

解释generator函数执行的简单方法

Generator对异步的处理

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

谈到generator,你应该不会对它感到陌生。在Node.js中对于回调的处理,我们经常用的TJ
/
Co就是使用generator结合promise来实现的,co是coroutine的简称,借鉴于python、lua等语言中的协程。它可以将异步的代码逻辑写成同步的方式,这使得代码的阅读和组织变得更加清晰,也便于调试。

co.js 保证 *函数中的
yield方法轮循执行,每次执行均返回的是promise对象,这里同时使用
node中的util方法中的promisify 代替传统的promise,nodejs8.0以上

const co = require('co');
const request = require('request');

const options = {
 url: 'https://api.github.com/repos/cpselvis/zhihu-crawler',
 headers: {
  'User-Agent': 'request'
 }
};
// yield后面是一个生成器 generator
const getRepoData = function* () {
 return new Promise((resolve, reject) => {
  request(options, (err, res, body) => {
   if (err) {
    reject(err);
   }
   resolve(body);
  });
 });
};

co(function* () {
 const result = yield getRepoData;
 // ... 如果有多个异步流程,可以放在这里,比如
 // const r1 = yield getR1;
 // const r2 = yield getR2;
 // const r3 = yield getR3;
 // 每个yield相当于暂停,执行yield之后会等待它后面的generator返回值之后再执行后面其它的yield逻辑。
 return result;
}).then(function (value) {
 console.log(value);
}, function (err) {
 console.error(err.stack);
});
const util = require('util')

const co = require('co')

co(function *() { 
    let data = yield util.promisify(fs.readFile)('./package.json') //使用node util 中的promisify实例化 fs.readFile方法同时直接返回结果
    data = JSON.parse(data)
    console.log(data.name)
})

async / await对异步的处理

async 加载方式 nodejs7.6以上版本 用async await
把异步加载方式同步的写法实现,实际上是对 promsie的封装

虽然co是社区里面的优秀异步解决方案,但是并不是语言标准,只是一个过渡方案。ES7语言层面提供async
/ await去解决语言层面的难题。目前async / await 在 IE
edge中已经可以直接使用了,但是chrome和Node.js还没有支持。幸运的是,babel已经支持async的transform了,所以我们使用的时候引入babel就行。在开始之前我们需要引入以下的package,preset-stage-3里就有我们需要的async/await的编译文件。

const util = require('util')
const readAsync = util.promisify(fs.readFile)

async function init () {
    let data = await readAsync('./package.json')

    data = JSON.parse(data)
    console.log(data.name)
}

init()

无论是在Browser还是Node.js端都需要安装下面的包。

 

$ npm install babel-core --save
$ npm install babel-preset-es2015 --save
$ npm install babel-preset-stage-3 --save

这里推荐使用babel官方提供的require
hook方法。就是通过require进来后,接下来的文件进行require的时候都会经过Babel的处理。因为我们知道CommonJs是同步的模块依赖,所以也是可行的方法。这个时候,需要编写两个文件,一个是启动的js文件,另外一个是真正执行程序的js文件。

启动文件index.js

require('babel-core/register');
require('./async.js');

真正执行程序的async.js

const request = require('request');

const options = {
 url: 'https://api.github.com/repos/cpselvis/zhihu-crawler',
 headers: {
  'User-Agent': 'request'
 }
};

const getRepoData = () => {
 return new Promise((resolve, reject) => {
  request(options, (err, res, body) => {
   if (err) {
    reject(err);
   }
   resolve(body);
  });
 });
};

async function asyncFun() {
 try {
  const value = await getRepoData();
  // ... 和上面的yield类似,如果有多个异步流程,可以放在这里,比如
  // const r1 = await getR1();
  // const r2 = await getR2();
  // const r3 = await getR3();
  // 每个await相当于暂停,执行await之后会等待它后面的函数(不是generator)返回值之后再执行后面其它的await逻辑。
  return value;
 } catch (err) {
  console.log(err);
 }
}

asyncFun().then(x => console.log(`x: ${x}`)).catch(err => console.error(err));

注意点:

  1. async用来申明里面包裹的内容可以进行同步的方式执行,await则是进行执行顺序控制,每次执行一个await,程序都会暂停等待await返回值,然后再执行之后的await。
  2. await后面调用的函数需要返回一个promise,另外这个函数是一个普通的函数即可,而不是generator。
  3. await只能用在async函数之中,用在普通函数中会报错。
  4. await命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把
    await 命令放在 try…catch 代码块中。

其实,async /
await的用法和co差不多,await和yield都是表示暂停,外面包裹一层async 或者
co来表示里面的代码可以采用同步的方式进行处理。不过async /
await里面的await后面跟着的函数不需要额外处理,co是需要将它写成一个generator的。

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

您可能感兴趣的文章:

  • JavaScript中使用Async实现异步控制
  • async/await与promise(nodejs中的异步操作问题)
  • JavaScript无阻塞加载和defer、async详解
  • JavaScript中的await/async的作用和用法
  • 关于Javascript中defer和async的区别总结
  • JS中script标签defer和async属性的区别详解
  • 浏览器环境下JavaScript脚本加载与执行探析之defer与async特性
  • 理解javascript
    async的用法

相关文章