欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

前端异步编程系列之何为异步编程(1/4)

程序员文章站 2022-05-08 11:26:44
...

1.什么是同步和异步

同步,也就是你在执行代码时,他会等待代码返回结果,不管这代码执行多久,只有代码返回结果了然后再代码才会继续往下执行。而异步指的是:我要执行一段代码A,我不等待他出结果,我会为他设置一个处理代码,当A出结果时,直接去调用那个处理代码去处理他,而我本身就不会再去管代码A了,代码会继续往下执行,等到A出结果了,直接让他执行之前设置好的处理代码就行了。比如,前端的请求Ajax接口就是一个异步操作。

所以同步和异步的不同之处就在于处理问题时流程上的不同。同步比较符合人们的线性思维,代码一步一步往下走,不会乱。而异步需要就需要把思维转化为事件驱动的思路上:我要做一件事,只是告诉计算机开始做这件事就行了,然后我就继续去做别的事了,而不是傻傻等着计算机做完。只要让计算机做完了这件事后,告诉我这件事做完了。我才继续回来去处理结果就行了。

所谓异步执行,不同于同步执行(程序的执行顺序与任务的排列顺序是一致的、同步的),每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的。

2.为什么要学习前端异步编程

JavaScript的执行环境是单线程的,单线程的好处是执行环境简单,不用去考虑诸如资源同步,死锁等多线程阻塞式编程等所需要面对的恼人的问题。但问题也很明显,如果一个任务运行时间很长,那么其他的任务就会一直等待。而最难受的是,客户端浏览器的UI渲染和js执行是共享一个线程的,如果js代码运行很长,那么UI就会假死,页面就会没有反应出现类似卡死的情况。这种用户体验肯定很差。

高性能JavaScript一书中曾总结过:如果脚本的执行时间超过100毫秒,那么用户会感到明显的卡顿,以为页面停止响应。在B/S模型(浏览器/服务器模型)中,网络速度的限制给网页的实时体验造成很大影响。

如果网页获取一个网络资源耗费很长时间,那么如果采用同步的方式加载,那么JavaScript将需要等待网络资源完全从服务器获取后才能继续执行。这期间UI将卡死,不会响应用户的交互行为(因为浏览器UI和js是共用一个线程的)这时候用户体验会很差。而采用异步请求,这是js和UI执行都不会处于等待状态,可以继续响应用户的交互行为,给用户一个鲜活的页面。这也是Ajax如此流行的主要原因之一。

3.异步编程有哪些问题呢?
说了这么多,异步编程带给我们的吸引力是足够大的,但不得不说的是,所有事物都具有两面性。我们只有去面对异步编程所面临的问题,并积极去解决他,我们才能真正享受异步编程所带来的优势。

而对于前端来说,浏览器的事件和ajax的回调函数的异步请求是前端最为广泛的异步编程的例子了。不过啊,如果仅仅是这两种的话,距离解决异步编程中的问题以及写出优美的异步编程代码来说还是不够的。那么我们就来说说异步编程都有哪些问题值得我们困扰:

1.异常处理
一般我们如果想要捕获异常,通常都是使用try catch来捕获的,但是对于异步编程来说不一定适用。为什么???比如:

function readFile(callback) {
    setTimeout(() => {
        callback("我是数据")
    },100)
}
// 执行(无法正确捕获异常,程序会报错,并且直接崩溃退出)
try {
    readFile((data) => {
        throw new Error();//假设出错了
        console.log(data);
    })
} catch(err) {
    conosle.log("捕获错误")
}


为啥看似我们把异常给包裹进去,但是程序确还是崩溃?因为异步操作通常包括两个步骤:1.发出异步请求,2.处理结果。

ps:关于js的事件循环调度:js有一个事件处理队列,用来存放需要完成的一个个的任务,js通过事件调度来一个一个的处理队列中的任务。一次js只能执行队列中的一个任务,只有当当前任务处理完了之后才开始执行队列中的下一个任务。

但是通常:

异步请求的发起和处理结果的处理任务通常都不在一个事件循环调度中(也就是指通常发出异步请求和处理结果在两个任务队列中,是分开执行的)。从上面代码来看:

代码中try他捕获的是执行readFile这个函数时抛出的错误。而readFIle这个函数执行的时候并没有抛出错误,所以这个catch当然不会捕获到了错误了,因为错误的抛出是在100ms后下一次事件任务执行callback时才被抛出的。(ps:setTimeout定时器他的任务不是在到达时间指定时间时立即执行回调函数,而是在到达指定时间时,向js事件处理队列中添加一个新的处理任务,所以即使setTimeout(() => {},0)也不会在本次事件任务处理时执行,而是下次任务时才执行)

而解决方法只能在回调函数中自己再try catch一次捕获一下异常,并且处理了。这样就有点难受了。

但是这里还可能会出现一个失误就是:切记不要捕获用户传入的callback回调函数,因为如果是node的风格的回调函数,在处理时,会把err传递给callback时(即只有一个回调函数的情况下),可能会这样写:

bad code:    

try {
    //其他操作。
    callback("我是数据")

}catch(err) {

    callback(err)

}

这段代码的本意是为了捕获其他操作时的错误,但却把callback也包括了进去。这时候,如果其他操作没有问题,而在callback中抛出了错误,那么callback会被执行两次。可能这不是想要的。正确的应该是:    

try {
    //其他操作。
}catch(err) {

    callback(err)

    return
}

callback("我是数据");

如果是只有一个回调函数,如同node中那样err和callback,那么callback中的错误,应该让也只能让定义这个callbac的人自己去在callback中捕获。但是如果是两个回调函数,一个成功一个失败,也可以考虑这样:

try {

    //其他操作。

    callback("我是数据")

}catch(err) {

    errCallback(err)
}

但是问题在于:你不知道到底是哪个地方抛出的错误,而且,代码的流程不够清晰,并且可能callback和errCallback同时调用了。而且应该也违背了一个执行异步操作时,是只有执行成功时调用成功回调,执行失败时才调用失败回调的准则。

这是异步回调的问题之一。

问题二:代码嵌套

当然:看了这么多的无聊东西,那么来娱乐一下,见识一下传说中的回调地狱:

前端异步编程系列之何为异步编程(1/4)

前端异步编程系列之何为异步编程(1/4)

当然了,现实中没这么夸张,真正写出和这图一样嵌套代码的,估计坟头草都和我一样高了。不过还是能说明问题的。像我之前微信小程序中的一个商城功能api的调用。就需要:

1.wx.login登陆获取code,

2.使用code请求openid,

3.使用openid获取用户绑定的门店,还需要通过openid获取这个用户的团购id(此处是为了举例)

4.再用通过门店和团购id,请求该门店的团购商品。

如果这异步回调真写出来也是有点难看的(事实当然不是真的一股脑全部嵌套的)。所以,嵌套过多,代码也是不好看的。但是,这不是最主要的问题,因为

前端异步编程系列之何为异步编程(1/4)

上面写出来的代码虽然难看,但是,问题是第3步中,获取门店和团购id他们都是两个不同的异步操作。但是,我的下一步4中的操作要依赖这两个异步的结果,那么一般最好最方便的写法自然就是把获取团购id的异步请求放在获取门店的异步回调中了,这样,本来两个不相干的异步操作,却需要串行起来进行,这无法利用异步操作代码的并行优势。这是异步的一个典型问题。

3.异步转同步

有时候,我们在编写代码时,习惯了异步编程时,可以从容的面对异步编程带来的回调函数以及业务分散等副产品。但有时候确实也会需要同步api来编写代码。比如小程序中,对于获取localhost来说,就有异步和同步的api,有时候节省了很多问题。所以,有时候如何将异步api转换为同步api就是我们需要解决的问题()。

4.其他
          前端的异步编程还有一些其他问题,比如多线程问题?提到这里,肯定知道说的就是js是单线程的,无法充分利用多核cpu。而HTML5提出了web workers。他在JavaScript单线程执行的基础上,开启一个子线程,进行程序处理,而不影响主线程的执行,当子线程执行完毕之后再回到主线程上,在这个过程中并不影响主线程的执行过程。可以用来承担js的一些比较大的计算任务。不过不能分担UI渲染任务。而且这一块由于我很少用,我也不太清楚,有兴趣的同志可以研究一下。

结语:

这第一篇文章主要是为了让大家了解什么是异步编程,以及异步编程的优劣,不过这篇文章主要还是为了后面异步编程文章做的铺垫,后续我会详细介绍该如何解决上述提出的关于异步编程的问题。