在看完这篇文章后

https://juejin.im/post/5d37abf7e51d45108223fd49

我对async的异常混乱持怀疑态度,换句话说,并不是async处理异常混乱,而是开发人员无法准确理解async的使用从而导致的混乱,错不在async而在开发人员,async只是提供一种机制,你可以选择使用也可以选择不使用。

但是很显然,目前出现问题了,即开发人员在使用asynv作为异步的解决方案的时候无法很好的处理异常问题,我想说 这本质上是代码的组织问题而不是技术的选型问题,他只是把前端长期以来的忽视代码组织的问题给暴露了而已,为什么要发明promise?就是因为回调地狱的问题?前端总是希望通过某种技术来解决代码混乱问题,然而很多时候对于系统设计的理解和对于编程的思考则更为重要,而promise本质上是函数式编程的产物。所以async的异常处理混乱本质上是开发人员的头脑中并没有一个清晰的代码组织,这具体体现在:

系统异常与业务判断的混用

代码没有抽离

为了链式而链式

系统异常与业务判断的混用

  在这里我举一个例子,如果我们有一个纯函数,该函数的功能是完成string的trim方法:

function trim (v) {
  if (typeof v != 'string') {
    return '';
  }
  return v.replace(/(^\s*)|(\s*$)/g, '');
}

function trim (v) {
  if (typeof v != 'string') {
    throw new Error('v is not string');
  }
  return v.replace(/(^\s*)|(\s*$)/g, '');
}

 在上面的代码中,我们一般应该如何来处理数据的异常?是通过返回一个值来交给上面处理,还是直接抛出异常?这取决于我们如何理解异常以及我们如何理解纯函数,思考一个问题,当我们使用null.slice的时候 是不是会直接报错,这里为什么采用报错机制而不是采用返回一个带有提示性质的值呢?这就是纯函数的作用,纯函数只负责完成某项功能,相同的输入得到相同的输出,违法的输入得到异常,异常本事是错误,异常是一种反馈机制,对于纯函数而言 异常本身就是一种对于调用方的反馈机制,所以第二种方式才是规范的,试想一下 如果我们项目中充斥着同js api同级别的纯函数,并且采用同样的处理逻辑,那是不是就是说 我们为js api添加了自己的api?我们为js扩展了能力并且应用于我们的项目之中,这使得我们的代码更加清晰,

现在回过头来看promise,promise的初衷是解决异步的回调地狱问题,但是我们应该如何在promise中使用异常呢?是reject还是catch?因为promise的存在 函数无法向外抛出异常,因为这回被then里面的reject或者catch捕获,如果不存在reject和catch,则抛出异常,我们一般使用promise是在封装api请求的时候,这里就存在一个问题:对于api请求前的数据验证 我们是放在api里面 还是放在外面,即

const refuseCourse = (options) => {
  return new Promise((resolve,reject) => {
    if (!options) {
      reject('options is not defined')
    }
    axios.post("/autoCoursesArrangingService/refuseCourse",parm).then(data => {
      resolve(data);
    }).catch(message => {
      reject(message);
    });
  });
};

 在这段代码中,if (!options) 这句话是否应该存在?因为这并不违法纯函数,他只是在使用前 简单的判断一下数据是否存在而已,很多时候 我们都可以这样做,问题在于上游如何处理这个reject, 这个reject本身是业务的拒绝,而不是异常,如果上游使用了async方式来调用 很显然,需要加上try catch才能捕获这句异常,但是问题就在这里,如果上游的上游也需要使用async呢?难道也需要加上try catch?其实这不是加几层try catch的问题,这是你如何组织你的代码的问题,我把代码分为三个大部分,view层代码 model业务层代码 纯函数代码,其中像api封装 本地数据请求封装 某种功能的封装全部属于纯函数范畴,他们就像一个个积木一样零散的摆在那里,供谁调用呢?由model层调用,model层首先在拿到一段业务逻辑后,通过使用命令时代码调用了几个纯函数之后 完成了业务逻辑的处理,这个时候model层的上游就是view层,所以并不会存在很多层的嵌套,只会存在view层拿着model层 model层拿着纯函数,然后我们在model层加一个try catch处理所有的异常,然后统一返回给view层,或者我们model层不加try catch,而是只在view层加try catch,这样的话 对于异常的处理不是清晰了很多吗?同时我并不建议在上述的方法中添加options的校验,这会增加代码的冗余,纯函数的功能只有一个:在假定数据正常的情况下,完成指定的功能,他就好象数学函数一样,(想象一下,在高中的时候,你会在你的数学题里面 在创建一个函数的时候,回去判断x的存在吗?),而对于数据的判断应该放在业务层即model层来判断,如果多个方法存在同样的数据判断 则可以单独抽离一个地方保存这些判断。

关于异常与页码判断混用的情况:

举一个例子:

try {
var a = parseInt(b);
}

catch(e) {

throw new Error('b is not a number')

}

我们利用try catch机制来处理数据的校验,这本身没有问题,但是这导致我们无法获取其他异常信息了,他把var a = parseInt(b);这句话的异常统一归为数据不是一个数字,这回导致误判,我们不应该把业务逻辑的判断与异常混用,异常是一个好东西,它能够帮助我们了解系统存在哪些问题,但是我们看看上面,上面这种方式剥夺了我们通过异常查看系统存在那些问题的能力,它使用try将其他异常全部包裹住,然后只返回一个异常信息,这导致我们无法准确得知具体是什么问题,之所以说这个例子,是因为通常async处理异常混乱的情况出现是因为 开发人员将异常本身作为一种业务逻辑并纳入到代码之中进行判断,这本身就是不合法的,所以才会导致异常处理的混乱,试想:如果我们正确对待异常,即只要存在异常我们就抛出,所以最终我们只会在最上层抛出异常,在中间代码中根本就不会处理异常又怎么可能会出现异常处理混乱的机制呢?所以在一个方法中:如果你希望调用方认真的处理你的返回值(即把拒绝当作业务逻辑的一种情况来考虑)那你就应该通过return或者resolve来返回拒绝值,而参数的不正确本身属于函数调用异常,他应该抛出,要么使用方在使用函数前进行数据判断,要么弹出错误信息。

代码没有抽离

  代码冗余的原因通常是因为没有进行代码抽离,代码抽离的前提是准确理解系统的内部结构,只有将系统内部进行抽象化之后才有可能完成代码的抽离,最经典的是mvc,同样在前端我们依然可以继续抽象化,如增加api层,增加业务函数层,增加服务层,抽离的越细致,则代码的可维护性越高,否则后面维护将变得困难,而一旦将代码抽离之后,数据的校验将变得困难,难道我们要在每一个抽离的方法中进行数据校验吗?然而这里我们要搞清楚 数据本身也是一种物体,当我们在代码抽离的时候我们肯定针对不同的功能进行抽离的,具体的是区分业务与技术,即view层只关心界面不关心业务逻辑,api层只关心数据请求,不关心业务逻辑,业务函数库 只关心某一个特定功能的实现,同样不关心业务逻辑,同时我们增加Model层,专门负责业务逻辑的处理,这个时候数据本身也是业务逻辑的一种,不同的业务场景 数据本身是不同的,即数据的类型 大小 存在与否与业务场景是息息相关的,所以我们理应把数据的校验全部拿到业务层中来,这样的话其他模块如api层 业务函数库 view层 只需要关心自己功能的实现即可。

为了链式而链式

  promise的普及以及函数式开发的火热,带动了链式调用,链式调用的本意在于语义化,然而并不是所有场景都适合使用链式调用,链式调用在处理分支判断上很难做到完美,他虽然提出了Either函子,但是如果存在if else这种方便好用的东西 我们为什么还要创造出另外一个同样的东西?仅仅是因为它符合函数式吗?换句话说 我们可以仅仅使用函数式开发即声明式代码就可以完成所有的系统开发吗?命令式代码有其存在的理由,声明式代码与函数式代码互为补充,我们喜欢函数式开发中的纯函数概念,因为他使得代码变得易于测试,同样函数式中组合的概念使得不同的传函数可以组合出完成更加复杂功能的纯函数,map函子解决了链式调用中 this问题,但是在switch和if else这种场景中 我还是会选择使用命令式代码,因为他直观,很多时候编写代码不是为了炫酷,而是为了合作和易于理解以及产生业务价值。

01-25 20:58
查看更多