首先看一段大家应该很熟悉的例子。
for (var i = 0; i <= 5; i++) {
setTimeout( function timer(){
console.log( i );
}, 1000)
}
这个例子应该是初学时候大家都会碰到的,都会感到迷惑的例子。刚学了for循环,也学了定时器。兴致勃勃的想要结合使用一下,想要实现个带时间间隔的数字递增。最后得到的却是5个6。这时候你得到的解释是,setTimeout是个异步函数,得等for循环完了才会执行,所以不会得到0,1,2,3,4,5。(要输出这个可以用ES6新加的let申明,因为每个let都会创建一个块作用域)或许你还不太清楚,那就看下面这个例子。
function foo(){
let a = 2;
setTimeout( function(){
console.log(a)
},1000)
a++;
}
foo();//3
现在你应该明白异步函数的执行顺序跟我们通常写的语句是不一样的吧。
通常来说异步是个时间概念,就是现在与将来的概念。异步就是将来运行的那一块。
那么为什么要有异步呢?归根结底来说,就是JavaScript是单线程的。单线程就是一个时间只能做一件事,比如
f();
g();
h();
我在执行f()的时候就只执行f(),执行完了再去执行g()等等。我不会边执行f(),边执行g()。这就是单线程。可是单线程就带来了困扰,一般来说I/O在很多程序中都是速度很慢的那一部分,那么当JavaScript执行到I/O操作时,由于单线程原因就会引起阻塞。后面的程序等啊等,导致整个程序执行很慢。这时候异步操作就出来了,异步说:“后面的先执行吧,你先忙你的,I/O操作我来解决,解决完了告诉你啊!”这样程序就变成了,执行执行执行,碰到异步任务,丢给任务队列,在往下执行,等待主线程空闲的时候去任务队列取回调函数执行。
接下来看一段代码:
let data = ajax("url");
let title = data.title;//data还没有获取到!
console.log( title );
现在你应该明白我们为什么从来不写上面的ajax操作了吧,为什么我们把对ajax获取的数据操作放到ajax的回调函数里面了吧!
还有一个小常识我想我应该告诉你,一般定时器的“定时效果”是不精确的,因为他表示的是,多少时间后把异步任务加到任务队列中,你可别忘了主线程可不是你放了我就取的啊。
关于异步函数的执行顺序是不确定的。如下
function foo() {
....
}
function bar() {
...
}
ajax("url1", foo);
ajax("url2", bar);
foo()跟bar()表示“将来”执行的部分,但是“将来”谁先执行是不确定的!所以在foo()与bar()有共享变量的时候得特别注意,因为有时候程序往往不会按你的预期一步一步执行。
这里说道执行顺序的不确定性,我想我应该好好谈谈关于回调函数的执行顺序。我们应该意识到,我们写异步函数时程序执行顺序跟我们在大脑里模拟的是不一致的。比如我想写一段代码来模拟我的一天:
“早上去食堂买个包子,边走边吃,可能会接到妈妈的电话,这时我不得不停下吃包子,跟妈妈聊天,妈妈可能会问我什么时候有空回家,这时我会去查手机上的待办事项看看那天有空,再给妈妈回复。这时候可能会有一个路人来问路'XX大道在哪里?',我得摘下耳机,用手机地图帮他查询再耐心的告诉他。结束后继续跟妈妈聊天。”
现在我想用异步模拟出来,上面的事件听上去很异步啊,可是我模拟不出来。因为我们思考的顺序想转换成异步事件代码往往是非常非常非常困难的。
有点晕了?我们来聊点简单的:
//A
ajax("url",function() {
//B
})
//C
现在的你,应该知道执行顺序是A->C->B。如果你读到了这里,我得好好感谢你,没有被我上面混杂的逻辑气走,也好歹可能通过上面理解了“将来”概念。那么我们趁热打铁,继续看下面这个:
A( function() {
B();
C( function() {
D();
})
});
F();
我想你得好好想想这个顺序,带着“将来”“现在”的概念。
如果你想到的是AFBCED,那么恭喜你,你已经被我洗脑了。我可没说AC都是异步函数啊!不过还得恭喜你现在理解了异步函数的执行顺序。
写的感觉还是挺乱的,想到什么写什么。刚好今天重新看了ES6文档,看到胖剪头的绑定问题,里面涉及了些异步回调函数的小知识,就想到得好好回忆下异步回调,就算是“憋”出了这篇文章吧。