从 setTimeout 到事件循环

回想起第一次使用 setTimeout 函数的时候,设置的回调函数并不是立即执行,而是过了一段定义好的时间之后才执行,这让当时刚知道 Javascript 是单线程的我无法理解,误以为“单线程”的定义是错的。过了段时间的学习之后才知道这玩意叫“异步”。

代码是从上往下一行一行地执行,为什么类似这样的异步的东西,却“违反常规”,执行到的时候会“忽略”,跳到最后的时刻才执行。换句话来说,同步代码执行完毕之后,异步的东西才开始执行。

结合堆栈来看在 Javascript 中代码是如何执行的

同步代码

function foo() {
  console.log( 'foo' )
}

function bar() {
  foo()
  console.log( 'bar )
}

bar()

/*
 * 结果:
 * 'foo'
 * 'bar'
 */

代码执行到 bar() 的时候,对应的堆栈情况为为下图:

bar()
foo()
console.log( 'foo' )

此时输出 'foo',console.log( 'foo' ) 执行完毕之后,从栈中弹出,此时 foo 也执行完毕,foo 也从栈中弹出,往下执行到 console.log( 'bar ):

console.log( 'bar' )

在这里执行 console.log( 'bar ) 之后输出 'bar',之后弹出,bar 执行完毕也弹出,栈空。

setTimeout 代码

function bar() {
  setTimeout( () => {
    console.log( 'foo' )
  }, 2000 )

  console.log( 'bar' )
}

bar()

/*
 * 结果:
 * 'bar'
 * 'foo'
 */

代码执行到 bar() 的时候,对应的堆栈情况为为下图:

bar()
setTimeout()

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 的交流

回到上面 setTimeout () 的调用那里继续,此时增加一个 handlers 和一个 queue队列。

setTimeout 调用过程

setTimeout()

setTimeout () 调用之后,会有一个 timer 对象被传递到 handlers 中的 timer_handler。

传递 timer

在 timer 传递完毕之后,对于 setTimeout 的调用结束,立即返回,setTimeout 出栈

setTimeout 出栈

与此同时,timer_handler 会对该 aTimer 不停地进行监控,看是否达到时间触发。由于之前设置了 2000ms,因此没那么快触发。handlers 与 stack 互不影响,互不阻塞。

接着执行到 console.log( 'bar' ),入栈。输出 'bar'

console.log( 'bar' )

之后 console.log( 'bar' ) 出栈,bar 调用结束,bar 出栈。与此同时,timer_handler 会对该 aTimer 不停地进行监控,看是否达到时间触发。由于之前设置了 2000ms,因此没那么快触发。handlers 与 stack 互不影响,互不阻塞。

bar 出栈

假设自 setTimeout 的调用开始到 bar 出栈,经历了 1000ms,那么对于 aTimer 的 2000ms 来说,还有 1000ms,因此 timer_handler 还会继续监控。

(1000ms过后)

timer_handler 监控到 aTimer 的时间到了,于是会将 aTimer 的 callback 推入 queue 中,然后将 aTimer 从 handler 中移除。

移除 aTimer,将 aCallback 推入 queue

此时 aCallback 处于 queue 中,而 Javascript 主线程又是处于空闲状态,因此 aCallback 会被立即出队,进入主线程执行。

aCallback 出队,进入主线程执行

console.log( 'foo' )执行,输出 'foo'

console.log( '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 的值。

(未完待续...)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 名词解释 "event-loop": 事件循环"non-blocking": 非堵塞"callback": 回调函...
    coolheadedY阅读 1,347评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 我个人感觉这次题目质量是可以的,很模拟现实渗透场景,从外网到内网到域控,到达一定阶段给个flag但是也有吐槽的点,...
    _阿烨_阅读 703评论 0 2
  • 很多年前,我喜欢向别人承诺永远。 因为那个时候我觉得,永远不是一个时间概念,而是一种表达我情感炽热的修饰。而那些曾...
    离合不骚阅读 196评论 0 0