1. 区分同步和异步
JavaScript 引擎是单线程运行的,即一次只能做一件事。JavaScript 代码的执行也是从上往下顺序执行的,一行代码的执行过程被称作一次操作。
从操作花费的时长来看,如果一个操作需要的时间特别多,那分为两种情况,一是这个长时间的操作的确每分每秒都需要 JS 引擎参与,没有等待,比如死循环代码。另外一个情况是这个长时间操作调用了其他资源,其他资源的处理需要时间,JS 引擎大部分的时间都是在等待其他资源的处理完成,比如 ajax 通信。第一种情况下 JS 引擎的确一直都在工作,是没有问题的,但在第二种情况下,JS 引擎长时间处于等待状态,没有工作,这就会导致资源的浪费和效率的低下,对前端页面的交互也非常不好。
因此为了杜绝这种情况,将不能立马执行完成的有等待的操作归类为异步操作,用特殊的方式去执行。其余的能立马执行完成的操作归类为同步操作。
这就是异步和同步的区别,简单总结就是同步就是立马能拿到结果,异步就是立马拿不到结果,得等一会才能拿到结果。
2. 回调函数
既然划分出了异步操作,那么 JS 应该如何处理异步操作呢?处理异步操作的关键就是不让 JS 引擎等着呗。比如发送 ajax 请求,执行发送的代码后,不再等待结果的返回,就继续往下执行后面的代码。
var data = ajax('http://some.url.1')
console.log(data); // 显然拿不到结果
那问题出现了,那异步操作的结果怎么拿呢?还有一些操作必须在异步操作后才能执行怎么办呢?最简单的方法就是设置一个回调函数,这个回调函数里包含了对异步操作数据的处理或者必须在异步操作后执行的代码。那这个回调函数啥时候被调用执行呢?在那个异步操作完成后调用,由谁调用呢?由那个异步操作涉及的其他资源进程调用并传递相应的数据作为函数参数,毕竟只有它知道啥时候完成。
???至于这段回调函数具体是怎么执行的,是其他资源向当前进程发起类似于系统的中断,然后由当前 JS 引擎执行,还是说回调函数在外部就被运行了,这里不太清楚,且存有疑问,等以后看到了再补充。 ???
在 ES6 之前的异步操作基本都是以回调函数的形式实现的。
所以,根据回调函数可以处理异步操作的情况,以上代码修改如下:
ajax('http://some.url.1', function callback(data) {
console.log(data) // 能拿到结果
})
3. 回调函数存在的问题
回调函数在 ES6 之前一直都是js异步编程的主力军,但是它的确存在很多问题。
3.1 缺乏顺序性
人的大脑思考事情都是顺序思考的,当人脑去读懂一段代码时,一般是依次理解的。例如:
console.log('A')
function request(){
console.log('B')
}
request()
console.log('C')
这段同步代码的执行符合顺序性,我们的大脑毫不费力地就可以给出最终的输出A B C,且结果是准确唯一的。
但当我们看到如下代码时:
console.log('A')
function request(){
setTimeout(() => {
console.log('B')
}, 0)
}
request()
console.log('C')
这就得仔细分析一下才能得到真正的输出是 A C B。我们大脑惯性认为明明是 request() 调用在 console.log('C') 之前,request 函数的输出应该在前,但由于异步代码的原因,结果不符合我们大脑的顺序性直觉了,需要思考下才能推断出真正的输出顺序。
难以理解的代码是坏代码!
3.2 缺乏信任性
回调函数运用到异步操作中,其实质是控制反转,就是把自己程序的一部分的执行控制交给第三方。简单举例呢,就像是本来是你的事,你不去做,将做法写了下来,交给别人去做了,就是说,想法是你的,事是你交给别人做的。无论过程是怎样的,结果都是你想做的事做了,这就是你将实现想法的权利交给了别人,就是控制反转。
根据实际情况,想法虽然是你出的,但事情毕竟不是你实际做的啊,就会存在一个信任问题。比如你交代的第三方没能调用时传递正确的参数,或者调用了好多次等等都会导致你这个函数出错,函数出错了,你就会以为,咦,是不是我的问题,仔细追查一番发现,你这边没问题,是第三方调用的时候出了问题。
这就有点类似于,自己的事不是自己做的,还真有很多不放心的。
针对这种不信任问题,回调函数应该做一些对应的处理。比如在回调函数里写一些防御性代码,把该函数被调用过程中存在的各种情况提前做对应的处理。(情况很多,一一考虑并实现起来任务庞大)
3.3 不能用 try...catch 捕获错误
try...catch 是用于捕获你这里执行的代码出现的错误的捕获,也就是所谓的同步操作的错误捕获,异步操作时回调函数不在你这里执行的,是别人执行的,那就不知道具体执行时发生的情况了,就是说,catch 的代码都执行完了,你这异步操作的错误还没发生呢。因此 try...catch 对异步操作无能为力。
因此对于异步操作的错误捕获,通常是传入两个回调函数,一个是成功回调,一个是失败回调,通过传入失败回调来进行错误处理。
但其实,将 try...catch 写到回调函数里也是行的。
3.4 不能 return ,回调地狱,代码维护困难...
网上还有一些说是,不能 return ,会有回调地狱,代码维护困难等。嗯,这些没有理论支持啊,什么意思,为什么。首先不能 return ,为什么不能 return ,是个函数都能 return 。还有回调地狱,指的是函数体嵌套函数体的写法吗?将函数体抽离成函数再调用就不是回调地狱了吗?回调地狱怎么了呢?其实,实质还不是说导致代码可读性差,缺乏顺序性吗?
在《你不知道的 JavaScript》中卷的回调函数的缺点里,就介绍了前面第一第二点。我也觉得,第一第二点才是回调函数实质上的问题所在。