异步三部曲之回调
Posted 馒头加梨子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了异步三部曲之回调相关的知识,希望对你有一定的参考价值。
概述
这是我看你不知道的JavaScript(中卷)的读书笔记,供以后开发时参考,相信对其他人也有用。
异步机制
分块的程序:我们写的代码有一部分是{现在运行的},其余的则是{将来运行的}。
我们不把它们分开写,因为它们是有联系的,比如{将来运行的代码}需要部分{现在运行的代码}的变量,那么怎么使这些变量在{现在运行的代码}运行结束后仍然存在并且能被{将来运行的代码}调用?答案很简单,就是闭包,我们把{将来运行的代码}放在一个函数作用域中,使它能够使用外部作用域的变量,而且,即使外部作用域被销毁,这些变量也一直存在。而产生这个闭包的函数就被称为回调函数。
我们写的代码中,可能不止一个地方需要在将来运行,一般的情况是,js的主线程运行完{现在运行的代码}之后,继续运行{将来运行的代码1},运行完之后继续运行{将来运行的代码2}。。。所以当运行{现在运行的代码}的时候,{将来运行的代码1},{将来运行的代码2}。。。这些将来运行的代码放在哪儿?答案是放在一个队列里面,这个队列被称为任务队列。
于是,主线程在运行完{现在运行的代码}之后,会拿出任务队列中的{将来运行的代码1}运行,运行完之后继续拿出任务队列中的{将来运行的代码2}运行。。。这种主线程不断拿出任务队列中的代码运行的机制被称为事件循环。
需要注意的是,可能有这么一个情况,在运行{将来运行的代码1}的时候,又发现了一个{将来运行的代码x},这个时候会重新创建一个任务队列2,并且把{将来运行的代码x}塞进去,等之前的任务队列中的代码运行完之后再来运行任务队列2中的代码。
需要注意的第二点是,{将来运行的代码1},{将来运行的代码2},,,{将来运行的代码x}的运行顺序并不一定是队列中先进先出的顺序,通常情况是,各自满足一定条件之后才运行,比如多少秒之后,或者接收到某个数据之后。
需要注意的第三点是,在es6之前,这个任务队列并不是js创建的,而是浏览器实现的,它一般被用来运行settimeout和ajax等异步操作。
在es6,js规范了任务队列,这个任务队列叫做microtask,而之前的任务队列被叫做macrotask,microtask用来运行promise等异步操作,并且运行在同一个事件循环的macrotask之前。
并发
由于js的异步机制,导致js在运行的时候能看起来好像同时处理多个任务,这种同时发生的情况就叫做并发。实现并发还有另一个机制,就是多个进程或线程同时运行,这种多个进程或线程同时运行的情况,就叫做并行。
在并发时候有一个很重要的情况,就是未来执行的代码的执行先后顺序会对最终结果产生影响。示例如下,块2和块3执行顺序的不同会造成a,b最后取值的不同。这种代码运行顺序的不确定性就被称为竞态条件。
//块 1:
var a = 1;
var b = 2;
//块 2( foo() ):
a++;
b = b * a;
a = b + 3;
//块 3( bar() ):
b--;
a = 8 + b;
b = a * 2;
一个很现实的异步竞态条件例子如下:
var a, b;
function foo(x) {
a = x * 2;
baz();
}
function bar(y) {
b = y * 2;
baz();
}
function baz() {
console.log(a + b);
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
怎么处理这种竞态条件呢?方法是加一个判断(所以判断在异步中非常常用)。
var a, b;
function foo(x) {
a = x * 2;
if (a && b) {
baz();
}
}
function bar(y) {
b = y * 2;
if (a && b) {
baz();
}
}
function baz() {
console.log( a + b );
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
这种需要2个异步同时完成就叫做门(gate),另一种是我们只需要最先完成的异步的数据,这种情况就叫做闩(latch),实例如下:
var a;
function foo(x) {
if (!a) {
a = x * 2;
baz();
}
}
function bar(x) {
if (!a) {
a = x / 2;
baz();
}
}
function baz() {
console.log( a );
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
回调函数
我们上面说了,我们一般把未来执行的代码包裹在一个回调函数里面,等满足某个条件之后再执行,比如下列代码:
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} )
初看之下,回调函数貌似看起来非常清晰,但是这只是表面的,再来看下面这段伪代码,其中doABCDEF都是异步函数。
doA( function(){
doB();
doC( function(){
doD();
} )
doE();
} );
doF();
实际运行顺序并不是ABDEF,而是AFBCED。当嵌套更多的时候,会更加复杂,需要看半天才能知道执行顺序。这就是著名的回调地狱。(注意,回调地狱并不是说嵌套太多了由于缩进写起来不方便,而是嵌套多了之后可读性很差。)
信任问题
回调地狱只是回调问题的一部分,还有一些更加深入的问题需要考虑。比如下面这个例子:
// A
ajax( "..", function(..){
// C
} );
// B
执行A和B以后我们会执行异步代码块C。就是说,现在异步代码块C获得了程序的全部控制权,可以控制作用域中的全部变量和方法。
这个时候,我们有理由担心:
- 异步代码块C根本不执行怎么办?
- 异步代码块C调用所需要的变量也是异步的没拿到怎么办?(调用过早)
- 异步代码块C调用太晚了怎么办?
- 异步代码块C获得的ajax数据不符合规范怎么办?
- 异步代码块C执行的时间太长怎么办?可能永久执行?
- 错误被吞掉怎么办?
更一般的情况是,我们有时候执行的这个代码块C是一个第三方函数,我们看不见。这个时候由于代码块C能够调用作用域中的全部变量和方法,如果这个第三方函数对这些变量乱改怎么办?
上面就是回调函数带来的信任问题,根源是我们把控制权交给了回调函数C。
当然,上面的问题有补救方法,但是要处理所有这些问题依然非常麻烦。细心的人可能看出来了,上面有部分问题是由于异步和同步同时进行导致的,而这也引出了一个非常有效的建议:永远异步调用回调,即使在事件循环的下一轮。比如下面的代码:
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", result );
a++;
如果ajax获得数据的速度比console.log的IO端口读写更快的话(cache储存),会打印0,否则会打印1。
需要说明的是,即使我们在回调函数中遇到了这么多问题呢,但是在小项目中,我们实际遇到的问题会少很多,所以用回调还是很安全的。
以上是关于异步三部曲之回调的主要内容,如果未能解决你的问题,请参考以下文章