回想起第一次使用 setTimeout
函数的时候,设置的回调函数并不是立即执行,而是过了一段定义好的时间之后才执行,这让当时刚知道 Javascript 是单线程的我无法理解,误以为“单线程”的定义是错的。过了段时间的学习之后才知道这玩意叫“异步”。
代码是从上往下一行一行地执行,为什么类似这样的异步的东西,却“违反常规”,执行到的时候会“忽略”,跳到最后的时刻才执行。换句话来说,同步代码执行完毕之后,异步的东西才开始执行。
结合堆栈来看在 Javascript 中代码是如何执行的
同步代码
function foo() {
console.log( 'foo' )
}
function bar() {
foo()
console.log( 'bar )
}
bar()
/*
* 结果:
* 'foo'
* 'bar'
*/
代码执行到 bar()
的时候,对应的堆栈情况为为下图:
此时输出 'foo'
,console.log( 'foo' ) 执行完毕之后,从栈中弹出,此时 foo 也执行完毕,foo 也从栈中弹出,往下执行到 console.log( 'bar ):
在这里执行 console.log( 'bar ) 之后输出 'bar',之后弹出,bar 执行完毕也弹出,栈空。
setTimeout 代码
function bar() {
setTimeout( () => {
console.log( 'foo' )
}, 2000 )
console.log( 'bar' )
}
bar()
/*
* 结果:
* 'bar'
* 'foo'
*/
代码执行到 bar()
的时候,对应的堆栈情况为为下图:
setTimeout() 执行之前,先说明 setTimeout 有关的东西。
理解 setTimeout
setTimeout 是属于 window 上的一个方法,在 ECMA-262规范 中找不到关于 setTimeout 的定义,但在 HTML规范 中找到了有关定义。所以 setTimeout 是一个 DOM API ,是浏览器对其内部进行实现的(可能是 webkit),而非 js引擎(例如 V8)实现,然后挂在 window 全局属性(window.setTimeout)上供 Javascript 访问调用。
具体来说是浏览器实现了一个 timer_handler,叫做“定时器观察者”这么一个东西。其实浏览器实现了几个 handler,比如发起 ajax 请求的 xhr_handler 等......浏览器对这个观察者向 Javascript 提供了一个 API(setTimeout) 用来传递被观察的对象进去。也就是说 Javascript 在调用 setTimeout 的时候,是往 timer_handler 传递了一个 timer 对象进去,timer_handler 在收到这个对象之后,Javascript 对 setTimeout 的同步调用已经结束,立即返回。可以说是一次 V8 到 webkit 的过程,即 Javascript 到 DOM API 的过程。
被传递的观察者类似于 timer = { time: 2000ms, callback: function() {...} }
,Javascript 通过 setTimeout 将 timer 传递给 timer_handler。
观察者在收到 timer 之后,就会一直监控时间,如果到达触发的条件,就会将 timer 推入一个队列中,等待 Javascript 主线程空闲之后执行
回到上面 setTimeout () 的调用那里继续,此时增加一个 handlers 和一个 queue队列。
setTimeout 调用过程
setTimeout () 调用之后,会有一个 timer 对象被传递到 handlers 中的 timer_handler。
在 timer 传递完毕之后,对于 setTimeout 的调用结束,立即返回,setTimeout 出栈
与此同时,timer_handler 会对该 aTimer 不停地进行监控,看是否达到时间触发。由于之前设置了 2000ms,因此没那么快触发。handlers 与 stack 互不影响,互不阻塞。
接着执行到 console.log( 'bar' ),入栈。输出 'bar'
之后 console.log( 'bar' ) 出栈,bar 调用结束,bar 出栈。与此同时,timer_handler 会对该 aTimer 不停地进行监控,看是否达到时间触发。由于之前设置了 2000ms,因此没那么快触发。handlers 与 stack 互不影响,互不阻塞。
假设自 setTimeout 的调用开始到 bar 出栈,经历了 1000ms,那么对于 aTimer 的 2000ms 来说,还有 1000ms,因此 timer_handler 还会继续监控。
(1000ms过后)
timer_handler 监控到 aTimer 的时间到了,于是会将 aTimer 的 callback 推入 queue 中,然后将 aTimer 从 handler 中移除。
此时 aCallback 处于 queue 中,而 Javascript 主线程又是处于空闲状态,因此 aCallback 会被立即出队,进入主线程执行。
console.log( 'foo' )执行,输出 'foo'
执行完毕,console.log( 'foo' ) 出栈,aCallback() 出栈。栈空。
以上就是事件循环的一个过程。事件循环不是某个函数或者部分 而是一套机制 这套机制的总称叫事件循环。
在此过程中,handlers会一直监控名下所有的handler,只要达到触发条件,就会形成一个任务推入 queue 中。而在 queue 中,会有一个类似 while循环 一直读取 queue 中的任务,也一直监控主线程是否空闲,如果空闲,而且 queue 中存在任务,就会取出队列头的任务出来,推入主线程执行。若主线程忙碌,就会等待主线程空闲。
通俗解释
小明要做一件事,就是要走完一条10米长的路,这条路上他需要做一些事。他从0米开始起步。走到2米处的时候,看到一个绿色呼啦圈,他停下来了,拿起绿色的呼啦圈开始转了起来,转了一段时间后,他继续走。走到了5米处的时候,他看到了一个“待办事项”的牌子,上面写有一些任务,于是他停下来,拿起笔和纸,在纸上写下了一段字:“时间:5秒后,任务:大喊一声‘旺旺’,次数:1。”他把纸放到了路边的一个人的手里,这个人穿着黑色的衣服,衣服上写着“定时管理”,不在小明走的这条路中,而在路的旁边。交到这个人手里之后,小明开始迈开脚步,继续往前走。走到9米处的时候,看到一个红色呼啦圈,他停下来转了一段时间后又继续走。走着就就走到了第10米,完成了走路。
而刚才那个黑衣人自打收到纸条之后,就开始计算时间。从收到纸条那一刻开始,0秒、1秒、2秒......5秒。好了,5秒的时间到了,这个黑衣人该准备做点事情了。只见这个黑衣人站起身,走到一个盒子旁边,将纸条放到盒子里面。放进去之后,黑衣人手里就什么都没有了。
然而,还有一个人,他负责管理这个盒子,他也不在那条路上,也是在路旁边,他只做一件事,就是不停地看着那个盒子。如果盒子里面有若干张纸条,他就会拿出最早放进去的那张;如果只有一张纸条,当然他就会拿出仅有的那一张。现在,那个黑衣人往盒子里面放了一张纸条,那他就会拿出那张纸条,然后观察小明是否走完了那段10米长的路。当他看到小明走完那段路,在终点空闲下来了之后,他就会马上把纸条递给小明,小明就会按照纸条上的“任务”项去做事,大声喊出了“旺旺”。
在这件事中,黑衣人和管理盒子的人跟小明是互不干涉,只有交流。也就是说,小明走小明的路,那两个人做他们自己的事,井水不犯河水,岁月静好。当然,那两个人也不属于“道路管理协会”的管理中,而小明是受到监管的。
延伸1
function fn() {
setTimeout( () => {
console.log( 'hahaha' )
}, 1000)
console.log( 'fn' )
while( true ) {}
}
在这段代码中,setTimeout 调用返回之后,到 console.log( 'fn' ) 输出 'fn',往下执行到一个 while( true ) 的循环。此时主线程陷入死循环,没有空闲的时间,即使 timer_handler 监控到 timer 触发,推入 queue ,也无法执行,因为主线程一直繁忙。所以 'hahaha' 一直都不会输出。
延伸2
在 MDN 中学习 async/await 的时候,看到了一个例子:
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add1(x) {
var a = resolveAfter2Seconds(20);
var b = resolveAfter2Seconds(30);
return x + await a + await b;
}
add1(10).then(v => {
console.log(v); // prints 60 after 2 seconds.
});
async function add2(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add2(10).then(v => {
console.log(v); // prints 60 after 4 seconds.
});
突然对 add1函数 2秒输出和 add2函数 4秒输出产生了疑问,于是自己仔细琢磨了一下,认为:
首先,async/await函数 可以看做是 generator函数 的一个进化版,只不过返回的是一个 promise,await 有暂停函数的功能,也相当于一个执行器,看做自动执行 then 或者 catch,自动拿出 promise 中 resolve 或者 reject 的值。
(未完待续...)