JavaScript 运行机制详解:再谈Event Loop

作者: 阮一峰

日期: 2014年10月 8日

一年前,我写了一篇《什么是 Event Loop?》,谈了我对Event Loop的理解。

上个月,我偶然看到了Philip Roberts的演讲《Help, I'm stuck in an event-loop》。这才尴尬地发现,自己的理解是错的。我决定重写这个题目,详细、完整、正确地描述JavaScript引擎的内部运行机制。下面就是我的重写。

进入正文之前,插播一条消息。我的新书《ECMAScript 6入门》出版了(版权页内页1内页2),铜版纸全彩印刷,非常精美,还附有索引(当然价格也比同类书籍略贵一点点)。预览和购买点击这里

cover

2014年10月13日更新:本文已经做了较大修改,反映了我现在的认识。关于setTimeout的更多解释和示例,请参阅我正在写的《JavaScript标准参考教程》。)

2014年10月11日更新:朴灵老师对本文做了评注,详细得指出了文中存在的错误说法,建议阅读。)

一、为什么JavaScript是单线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

任务队列

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

三、事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。

Event Loop

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。请看下面这个例子。


    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。


    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。

五、定时器

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。


console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。


setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者@BusyRich)。

Node.js

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTicksetImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。


process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

现在,再看setImmediate。


setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。


setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!


process.nextTick(function foo() {
  process.nextTick(foo);
});

事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。

另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

(完)

留言(176条)

写的不错

补充:Cluster是node js 多进程的API,是提高js资源使用效率的一种方法。
API描述:A single instance of Node runs in a single thread. To take advantage of multi-core systems the user will sometimes want to launch a cluster of Node processes to handle the load.

简言之就是JS只有一个主线程,主线程执行完执行栈的任务后去检查异步的任务队列,如果异步事件触发,则将其加到主线程的执行栈。

在https://medium.com/@shijuvar/web-development-trends-for-2015-and-beyond-c2d3c1ef5718这篇文章中的关于nodejs的可维护性、可用性和高性能的论证不知道阮一峰大哥怎么看?

今天发现#atom-shell#做同步ipc调用时,执行栈是可能被挂起的,相当于主线程sleep了,等ipc返回后才会接着执行后续代码。如果挂起的时候同一段代码被event又一次触发,逻辑就可能出错。

也就是说执行栈可能不能在一个loop内被执行完,因为会被挂起,是否挂起取决于运行环境和执行的操作种类。被挂起后前面注册的callback还是有可能先于执行栈剩余的代码触发。

补充一下Timer相关的问题:
1. 其实并不是“有些浏览器规定了setTimeout 的最小时间间隔”,这个最小时间间隔是W3C在HTML标准中规定,规定要求低于4ms的时间间隔算为4ms。
2. process.nextTick在最新版nodejs中不被推荐使用,推荐使用setImmediate ,原因在于nextTick是在当前帧介绍后立即执行,会阻断IO并且有最大数量限制;而setImmediate不会阻断IO,更像是setTimeout(func, 0)

@rednax:

谢谢指出,我还不知道HTML5把4毫秒写入了标准,已经改过来了。

我查了Node.js 10.0的发布说明( http://blog.nodejs.org/2013/03/11/node-v0-10-0-stable/ ),对于process和setImmediate的差异,还在消化当中。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
不同任务会分给不同线程?不是直接调用相关操作系统接口?

我想转载这篇博客,可以吗?

我觉得单线程的异步没什么意义。你看一下这个文章:

http://cnn237111.blog.51cto.com/2359144/1556987

requestAnimFrame应该是requestAnimationFrame吧。

单线程效率高是假,简化开发是真。
跟GUI相关的框架的都采用这种模型,QT、GTK、MFC、Android UI框架、这里的JS都是这样的。

setImmediate的例子有问题吧,触发A之后并不是直接输出1,而是执行另一个setImmediate,这个setImmediate会把B添加到任务队列尾部,在执行B之前会触发setTimeout的回调,所以例子的输出顺序应该是:

// TIMEOUT FIRED
// 1
// 2

@numbbbbb:

谢谢指出,你说得对,例子有错,已经改过来了。

引用万能小新的发言:

requestAnimFrame应该是requestAnimationFrame吧。

谢谢指出,改过来了。

setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// TIMEOUT FIRED
// 2
===========
同样的这段代码, 为什么后边的说 Immediate 和 Timeout 的先后顺序不确定呢? 求解释...

https://app.yinxiang.com/shard/s8/sh/b72fe246-a89d-434b-85f0-a36420849b84/59bad790bdcf6b0a66b8b93d5eacbead

朴灵对您文章的评注。语言虽然略显犀利,但是个人觉得评注的应该是对的。

@Leo:

谢谢指出,我修改文章时,脑子糊涂了,已经改过来了。

门都还没入就来写科普

打人不打脸,绝交。。

引用mamamiya的发言:

https://app.yinxiang.com/shard/s8/sh/b72fe246-a89d-434b-85f0-a36420849b84/59bad790bdcf6b0a66b8b93d5eacbead

朴灵对您文章的评注。语言虽然略显犀利,但是个人觉得评注的应该是对的。

看了一下不觉得评注对到哪里去,只有吹毛求疵之感。 比如同步异步介绍,本来就无大错;比如node图里面的OS operation,推敲一下就可以猜到那是指同步操作(自然不走event loop了);至于watcher啥的,显然只是实现上的特色,即使用同一个queue实现也未尝不可。

我去,被朴灵狠劈,现在更乱了

请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。

引用rednax的发言:


看了一下不觉得评注对到哪里去,只有吹毛求疵之感。
比如同步异步介绍,本来就无大错;比如node图里面的OS operation,推敲一下就可以猜到那是指同步操作(自然不走event loop了);至于watcher啥的,显然只是实现上的特色,即使用同一个queue实现也未尝不可。

多看几眼还真是

"setImmediate的另一个重要特点:一次"事件循环"只能触发一个由setImmediate指定的回调函数。"
这个说法其实也不准确,准确的说是因为setImmediate是在事件循环结束后触发,

setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
当第一个loop结束后触发第一次setImmediate,此时setImmediate调用之后开始将回调函数加入队列等待下一个loop结束触发, 而不是一次"事件循环"只能触发一个由setImmediate指定的回调函数。多个只要在loop前已经执行掉同样会在一次"事件循环"中一起触发。
setImmediate(function A() {
console.log(1);
});
setImmediate(function B(){console.log(2);});
实际上,上面调用2次setImmediate会形成一个immediateQueue链表,loop结束后会执行改链表里所有的函数~

引用Leo的发言:

setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// TIMEOUT FIRED
// 2
===========
同样的这段代码, 为什么后边的说 setImmediate 和 Timeout 的先后顺序不确定呢? 求解释...

@Leo , node中的底层loop有3种运行模式,DEFAULT, UV_RUN_ONCE,UV_RUN_NOWAIT具体差异详见libuv文档 。 刚启动node解析执行脚本时以UV_RUN_ONCE的模式执行也就是说第一次loop是UV_RUN_ONCE模式,在每一个loop中先检测timers,然后进入io_poll此时会阻塞等待事件发生,事件发生后执行setImmediate中的回调,当以UV_RUN_ONCE模式执行的时候会再次检测timers 。照这个原理来说,setImmediate应该比setTimeout先执行。

setTimeout(fn,0) 底层实际执行的是 setTimeout(fn,1), 此时会向loop队列中注册一个超时事件,假设注册时当前时间戳是1000,它的过期时间就是1001。按上面的说法1ms 第一次loop执行完后先执行setImmediate回调,再执行timer回调。但是有一点别忘了,您的计算机此时还有其他任务在处理,cpu不是全部用来跑你的node进程的,也就是说虽然cpu很快,但执行指令总得花点时间吧,从setTimeout(fn,0)注册一个1ms的超时事件到第一次执行timers可能就已经花了1ms的时间,此时系统时间已经到1001了,那么对应的timer回调当然就会先于setImmediate执行,所以说Immediate 和 Timeout 的先后顺序不确定。

引用淘杰的发言:

当第一个loop结束后触发第一次setImmediate,此时setImmediate调用之后开始将回调函数加入队列等待下一个loop结束触发, 而不是一次"事件循环"只能触发一个由setImmediate指定的回调函数。多个只要在loop前已经执行掉同样会在一次"事件循环"中一起触发。

谢谢指出,确实如此,已经改正了。

究竟谁对谁错呢?现在有些混乱了

引用crazy的发言:

究竟谁对谁错呢?现在有些混乱了

普通应用没必要这么吹毛球疵,只要了解一下 process.nextTick 和 setImmediate 的区别就好了,官方的 API 说明已经解释得很好,带实际应用示例。阮先生没有把官方介绍的 process.nextTick 实际用例放上来直接硬剥是有点无趣了。

我们由此得到了process.nextTick和setImmediate的一个重要区别……事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了……

不知道哪里来的「事实上」,官方示例已经明确地说「This is not a simple alias to setTimeout(fn, 0)」了,当然也更不可能原本作为 setImmediate 的替代,或者反过来。process.nextTick 是有特殊用途的,并还是单单脱离原有的 event loop。

引用Jak Wings的发言:

不知道哪里来的「事实上」,官方示例已经明确地说「This is not a simple alias to setTimeout(fn, 0)」了,当然也更不可能原本作为 setImmediate 的替代,或者反过来。process.nextTick 是有特殊用途的,并还是单单脱离原有的 event loop。

囧,抱歉,才看了官方发布说明,看来 process.nextTick 是被滥用后才加上 setImmediate 的。不过我还是觉得 API 说明页面的说明比发布那时的好,setImmediate 是和 setTimeout 使用同一个 event loop 的,应该凸出了和 process.nextTick 的不同了,应该也是后来提防大家滥用才这么设定的。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

我刚刚用 v0.10.32 和 v0.11.14测试过了,这个也没有保证。我能得到 1-T...-2 和 1-2-T... 两种结果,甚至是 T...-1-2。信不信由你。

引用Jak Wings的发言:


我刚刚用 v0.10.32 和 v0.11.14测试过了,这个也没有保证。我能得到 1-T...-2 和 1-2-T... 两种结果,甚至是 T...-1-2。信不信由你。

我测的结果,一直都是1-T-2。难道这个也不确定…… 

@淘杰:

谢谢解释~

引用淘杰的发言:

setTimeout(fn,0) 底层实际执行的是 setTimeout(fn,1), 此时会向loop队列中注册一个超时事件,假设注册时当前时间戳是1000,它的过期时间就是1001。按上面的说法1ms 第一次loop执行完后先执行setImmediate回调,再执行timer回调。但是有一点别忘了,您的计算机此时还有其他任务在处理,cpu不是全部用来跑你的node进程的,也就是说虽然cpu很快,但执行指令总得花点时间吧,从setTimeout(fn,0)注册一个1ms的超时事件到第一次执行timers可能就已经花了1ms的时间,此时系统时间已经到1001了,那么对应的timer回调当然就会先于setImmediate执行,所以说Immediate 和 Timeout 的先后顺序不确定。

这么解释的确合理,定时器的原理都是如此,告诉cpu有个任务在某个时间应该执行。但也只有cpu有空的时候,才能尽快执行才可。

读完正文觉得很多东西是纯理论的理解,有些可能来源于猜测。

这篇文章更该是一个学习笔记,文中提到的很多event其实和Javascript语言完全无关,纯粹是浏览器的处理和行为。

对于任何一个不基于浏览器且支持多线程的程序来讲(比如随便一个C实现的游戏),会发现同样的事件处理比如鼠标操作、键盘快捷键等等,是需要去自己单独实现的,那么也许实现中使用了Event loop,但这一部分从未集成到语言中,对吧?

理解这个事情更好的办法是真的去实现一个类似的程序。

看来我需要买书好好学学了。

引用阮一峰的发言:

我测的结果,一直都是1-T-2。难道这个也不确定…… 

对了,V8 引擎能代表所有实现了 ES6 的浏览器的具体实现么?

引用Jak Wings的发言:

V8 引擎能代表所有实现了 ES6 的浏览器的具体实现么?

问题是Node采用了V8

的确有些说法不太严谨~~~

引用牛牛的发言:

请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。

请问你一个问题,你的主业是专利局小职员,为什么还花这么多时间在物理上?即使你明知道不管你如何努力地来提高自己的数学水平,也无法改变业余的地位,也得不到什么实际的回报。

引用爱因斯坦的发言:

请问你一个问题,你的主业是专利局小职员,为什么还花这么多时间在物理上?即使你明知道不管你如何努力地来提高自己的数学水平,也无法改变业余的地位,也得不到什么实际的回报。

因为热爱 所以热爱

阮大侠你就别再祸害程序员了,建议先补一下计算机组成原理,编译原理,算法导论,C语言基础再来出书,不然你出了也没人敢看吧。另外起码要实践一点儿项目。

var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};

你说这俩等价,等价个毛线啊,req.send();之后如果浏览器有缓存这个函数会立即返回,也就是说不管你是onload还是onerror都不会执行。要先绑定回调再去执行send(),这样才没bug。

不过瘾

Js原来还有这么深奥的功能

引用牛牛的发言:

请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。

阮老师已经是程序员了……

不正确的文章为什么不删掉

想不到阮老师去了阿里巴巴做前端阿

很长一段时间,google日历无法访问,但网上又找不到好用的在线日历软件,偶然间看到您开发的这个简易版google日历,发自内心的高兴,但所有的浏览器都用了,就是无法联接,联的过程中,显示一个进度条,等到100%之后,马上出现一个空白页,提示无法连接。郁闷中!! 如果才能正常联接,期待您的指点。我的邮箱为:[email protected],手机:13501282988.如果可能能话,我愿意为您长期进行测试各项功能

欢迎兔哥加入大阿里!

强烈建议博主开个微信公众号!

引用reducm的发言:

想不到阮老师去了阿里巴巴做前端阿

是为了和朴灵PK才来的么?

好久没更新博客了,甚是想念。

听说阮先生,要去蚂蚁金服了...
阮先生好久不更新了,要记得常常更新哦。

@腊月乘凉:

确实是这样的

有没有办法查看Function Object的internal property?

有一个问题,事件执行的粒度是多大?是一个函数还是一个语句块?
在大量的异步方法同时操作一个变量的时候,会不会出现类似线程的问题呢?

var start = new Date();
console.log(start);

var print = setTimeout(function() {
var end = new Date();
console.log('从开始到执行这个setTimeout函数花掉的时间为', end - start, "ms");
}, 1000);

var test = [];
for(var i = 0; i test.push(i);
}
var middle = new Date();
console.log('执行数组操作所花掉的时间为:',middle - start, 'ms');

我想请教一下这里的setTimeout会在什么时候执行?我再node里面和浏览器里面分别跑了几次。得到的结果是:
node:
执行数组操作所花掉的时间为: 379 ms
从开始到执行这个setTimeout函数花掉的时间为: 1030ms
按照您的说法,setTimeout应该是尽可能在能执行的时候执行,这里运算操作只是379ms,没有超过初始设定的1000ms,按道理1030ms不应该是1001ms么?
浏览器:
执行数组操作所花掉的时间为: 8016 ms
从开始到执行这个setTimeout函数花掉的时间为 8017 ms
这个结果中,运算时间很长,远远超过了1000ms,所以setTimeout不是在1001ms执行的,而是等到元算结束之后立即执行,那么结果奇怪了,这个setTimeout时间设定成1000ms是从什么时候开始的?我的理解是一开始就计时,但是1000ms无法执行,等到数组操作完成之后才能执行,这样计时操作似乎没有存在的意义了。

原来水平也不过如此。有点水平的人都看得懂朴灵的批注比较准确。

从这篇文章看得出,阮老师不是科班出身的
不过非科班的也有非科班的好,希望能够继续有文章看

阮老师,那如果在ajax的回调函数中,存在setTimeOut函数,这个时候回调函数中还是按照Event loop的机制吗?就是说回调函数相当于主线程,把setTimeOut函数放在任务对象,先执行回调函数的代码再执行执行setTimeout函数?

阮老师,你的站点统计工具:specificclick好像不工作耶,请须知。

对您的网站有一些小提议,供您参考。
1、给页面右下角或者右侧加一个按钮,可以直接跳到最顶层,这样对读者来说很方便,因为一直拖动滚动条很麻烦。同时,这个按钮相对于浏览器窗口是固定的。
2、给标题下面加上您文章的写作日期。作为读者我感觉这个时间挺重要的。

嗯,还是比较实用的内容呢,我会长期关注的。

请不要说你在淘宝工作好吗?淘宝不都是大牛吗?

淘宝的人都喜欢出书吗?竟然都敢出书?

引用腊月乘凉的发言:

阮大侠你就别再祸害程序员了,建议先补一下计算机组成原理,编译原理,算法导论,C语言基础再来出书,不然你出了也没人敢看吧。另外起码要实践一点儿项目。

var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};

你说这俩等价,等价个毛线啊,req.send();之后如果浏览器有缓存这个函数会立即返回,也就是说不管你是onload还是onerror都不会执行。要先绑定回调再去执行send(),这样才没bug。

擦,能不能不要绑定宿主环境。

感觉文章的表述有些乱,比如“不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。”,这句让人理解的意思是“任务队列”主动通知“主线程”。而后面所表达的却是“主线程”在执行栈清空后才会到任务队列中取任务。前后主被动关系矛盾啊。

还有,“(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。”。这时异步任务还在队列里没执行呢,怎么就会有了运行结果呢?

烦请阮老师在方便的时候不吝赐教。同时,感谢阮老师每次耐心细致的讲解和分享!

定时器执行流程还是没讲清楚。。。。

能说说这个网站是怎么建 的么

不管扑灵和你都写的云里雾里的。。。不透彻。。

老师你好,请教个问题,如果是如图示Execution Context Stack所展示的那样,岂不是后来的Context先执行?
那为何
console.log('a');
console.log('b');
可以得到我们所要的执行顺序呢?

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2,这时函数A一定在timeout前面触发。

结果不总是1--TIMEOUT FIRED--2 还可能是TIMEOUT--1--2。

node 版本v0.12.2 windows 64位系统


@腊月乘凉:

的确,这个地方阮老师的说法是不对的。两者并不等价。

问题图片丢失。

执行栈(execution context stack)清空的时机是什么?

引用简陋的以数据的发言:

感觉文章的表述有些乱,比如“不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。”,这句让人理解的意思是“任务队列”主动通知“主线程”。而后面所表达的却是“主线程”在执行栈清空后才会到任务队列中取任务。前后主被动关系矛盾啊。

还有,“(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。”。这时异步任务还在队列里没执行呢,怎么就会有了运行结果呢?

烦请阮老师在方便的时候不吝赐教。同时,感谢阮老师每次耐心细致的讲解和分享!

同惑啊,“异步任务有了运行结果,就在'任务队列'之中放置一个事件”,这句不大理解呢,文中不是说同步任务清空之后才执行任务队列的么?前面这个异步的结果是哪里的?求解~~

window.onload = function(){

document.onmousedown = function(){
var oInput = document.createElement("input");
oInput.value = "输入内容";
document.body.appendChild(oInput);
oInput.focus();
oInput.select();
}
}

为什么把onmousedowm换成onclick在ie与chrome下显示效果不同

大神我有个问题,如何判断主线程结束,可以进入队列。什么时候能第二次进入主线程。

技术学术自然有需要严谨的地方,老师自然不可能触类旁引,将各个面都涉及的东西讲透,所以有些语言带过,也有确实考虑不周的地方。同样是做技术的,我觉有人跟进技术潮流,出来讲就很好了,至少人家在治学,敢于发表,听取意见。

文中提到 “如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。”,
提到“setImmediate方法则是在当前"任务队列"的尾部添加事件”,
又提到“Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面”。

这三块东西逻辑矛盾了,肯定有谁出错了,求指点。

所有windows平台的软件都基于消息队列循环方式。浏览器只是普通的windows本地程序,和winform,mfc等都是同样的机制

读了3遍,还是有一些不太理解的地方。
比如:同步任务执行完毕之后,才会去执行异步任务。
那会不会出现这样一种情况,a,b,c,d,e是同步任务。1,2,3是异步任务。
当a,b,c,d,e 执行完毕之后,主线程立马就去将异步任务1,2,3全部读入执行栈,还是按顺序只读取第一个?
那如果,a,b,c,d,e执行完毕之后,会不会在某种情况下,c又要重新执行一次?那此时的c算同步任务还是异步任务?
还有,到底何种任务叫同步任务,何种任务又要异步任务?
我初浅的理解是:一般浏览器的默认执行行为,比如 dom加载,css渲染之类的都叫同步任务。
click,onload之类的都叫异步任务。
理由是:前者在浏览器整个生命周期中,都算一种比较固定的步骤,每一次都要执行。且绝大多数任务都需要等待这些任务执行完毕之后才能执行。也就是说,前者是后者的载体。
异步任务:说实话,我不太理解。比较耗时的任务?不属于那种生命周期必须执行的任务(比如click)?

所以,当一个浏览器打开源代码给我看,我不太能指出哪些是同步任务,哪一些又是异步任务。

但我得到的收获是:javascript是单线程的,开浏览器的时候,会有一些默认每次都要执行的代码属于主任务,它们放在执行堆栈上,当它们执行完毕之后,主线程会去异步队列去找那一些准备好了的任务接着执行。前者执行完毕,循环的主要是后者那些异步队列里的任务了。

看得我肚子饿了

这文章误导人啊

引用简陋的以数据的发言:

感觉文章的表述有些乱,比如“不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。”,这句让人理解的意思是“任务队列”主动通知“主线程”。而后面所表达的却是“主线程”在执行栈清空后才会到任务队列中取任务。前后主被动关系矛盾啊。

还有,“(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。”。这时异步任务还在队列里没执行呢,怎么就会有了运行结果呢?

烦请阮老师在方便的时候不吝赐教。同时,感谢阮老师每次耐心细致的讲解和分享!

第一个问题阮老师表述可能有问题,但是意思还是执行完js主线程的代码才会去看浏览器任务队列中的事件,再执行js代码中该事件对应的代码。
第二个问题我的理解是,任务队列里放的是ajax这类的任务,是交给浏览器发起HTTP请求去执行的,当有了返回结果就会在任务队列中增加一个事件表示该ajax请求已经返回了结果,任务队列里的任务和js主线程是同时执行的。 不影响js是单线程的这个结论,只能说浏览器还会提供接口来供js实现异步操作。

引用青苔的发言:

老师你好,请教个问题,如果是如图示Execution Context Stack所展示的那样,岂不是后来的Context先执行?
那为何
console.log('a');
console.log('b');
可以得到我们所要的执行顺序呢?

Execution Context Stack 存放的是调用关系,你的例子的顺序是console.log('a'); 入栈直接运行后出栈,然后console.log('b');入栈,执行后出栈;
如果例子是这样:

function log() {
console.log(arguments);
}

log('a');

那么出入栈顺序是,log('a'); 入栈,console.log('a');入栈,执行console.log('a');出栈,log('a');出栈;
如果觉得我说的不是很清楚,建议看看这个视频:http://vimeo.com/96425312

引用LeeSir的发言:

第一个问题阮老师表述可能有问题,但是意思还是执行完js主线程的代码才会去看浏览器任务队列中的事件,再执行js代码中该事件对应的代码。
第二个问题我的理解是,任务队列里放的是ajax这类的任务,是交给浏览器发起HTTP请求去执行的,当有了返回结果就会在任务队列中增加一个事件表示该ajax请求已经返回了结果,任务队列里的任务和js主线程是同时执行的。 不影响js是单线程的这个结论,只能说浏览器还会提供接口来供js实现异步操作。

还是你解释的比较清楚。

setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});

阮老师, 上面的例子, 在测试中也有下面的情况:
TIMEOUT FIRED
1
2

我的node版本v5.11.0
关于 setTimeout感觉它会在插队"任务队列"(时间点到了,就优先插队到任务队列队首)

发现我们培训老师讲的课应该是借鉴了阮老师的文章。。。有的原话都一样。。

“挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。”
这句话我有一点不解,这个异步任务是要等所有同步任务都执行完毕了之后进入主线程呢?还是只要有IO设备返回结果就立马进入主线程,可以插队到已在主线程的同步任务前面?

自定义的回调函数会放到消息队列中吗?

关于setImmediate和setTimeout的执行顺序,确实是不定的:

setImmediate(function (){
setTimeout(function timeout() {
console.log('同一loop中的setTimeout方法执行,输出:TIMEOUT FIRED');
}, 0);
console.log('和 setTimeout、setImmediate 同一层次的代码执行');
setImmediate(function A() {
** console.log('同一loop中的setImmediate 方法执行, 输出:1' );
setImmediate(function B(){console.log('下一loop中的setImmediate 方法执行, 输出:2');});
});
});

执行结果:
和 setTimeout、setImmediate 同一层次的代码执行
同一loop中的setTimeout方法执行,输出:TIMEOUT FIRED
同一loop中的setImmediate 方法执行, 输出:1
下一loop中的setImmediate 方法执行, 输出:2

如果注释掉标记为**的那行代码:
setImmediate(function (){
setTimeout(function timeout() {
console.log('同一loop中的setTimeout方法执行,输出:TIMEOUT FIRED');
}, 0);
// console.log('和 setTimeout、setImmediate 同一层次的代码执行');
setImmediate(function A() {
console.log('同一loop中的setImmediate 方法执行, 输出:1' );
setImmediate(function B(){console.log('下一loop中的setImmediate 方法执行, 输出:2');});
});
});
执行结果:
同一loop中的setImmediate 方法执行, 输出:1
同一loop中的setTimeout方法执行,输出:TIMEOUT FIRED
下一loop中的setImmediate 方法执行, 输出:2

上面的**标记错了,应该是上边那句console:

setImmediate(function (){
setTimeout(function timeout() {
console.log('同一loop中的setTimeout方法执行,输出:TIMEOUT FIRED');
}, 0);
** console.log('和 setTimeout、setImmediate 同一层次的代码执行');
setImmediate(function A() {
console.log('同一loop中的setImmediate 方法执行, 输出:1' );
setImmediate(function B(){console.log('下一loop中的setImmediate 方法执行, 输出:2');});
});
});

文章写得很好,书也写得好!
http://es6.ruanyifeng.com/#docs/async

setImmediate(function (){
setImmediate(function A() {
console.log('1');
setImmediate(function B(){console.log('2');});
});

setTimeout(function timeout() {
console.log('3');
}, 0);
});

上面这个代码的输出顺序实际上也是随机的。可能是 1,3,2;也可能是3,1,2.

在 I/O 操作的回调里,他们的顺序才不是随机的:

fs.readFile('../../README.md', () => {
setTimeout(() => {
console.log('1')
}, 0);

setImmediate(() => {
console.log('2')
});
});

总是先输出2, 再输出1. 文档提到的:
The main advantage to using setImmediate() over setTimeout() is setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.
—— https://github.com/nodejs/node/blob/master/doc/topics/event-loop-timers-and-nexttick.md

呀 朴灵老师批注找不到了

如果执行栈(主线程)在执行完正要loop任务队列前,主线程的代码又被触发执行了一次会怎么样?比如两次点击,是会先执行任务队列还是主线程的代码

阮老师是一位不错的践行者,我前端典范。

引用fans的发言:

大神我有个问题,如何判断主线程结束,可以进入队列。什么时候能第二次进入主线程。

`event queue`中新增加event(比如点击) 和 `任务队列`中任务完成触发回调都会再次进入主线程。

根据MDN上官方的描述:
> The event loop got its name because of how it's usually implemented, which usually resembles:

while (queue.waitForMessage()) {
queue.processNextMessage();
}

queue.waitForMessage waits synchronously for a message to arrive if there is none currently.

这篇文章中大量的关于主线程`主动获取任务事件结束回调`的描述是错误的。回调会让`queue.waitForMessage()`返回`true`,激活新的event loop,从而及时处理回调。

setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2

阮老师,我测试的这个运行结果。也是不确定的,还有可能是TIMEOUT FIRED--1--2

一直有一个疑问未能解开,Event Loop这个机制是不是由一个单独的(浏览器/node)线程控制,比如setTimeout(()=>{}, 1000)方法,延迟1000ms,是谁来监控这1000ms后加入消息queue的,是浏览器的某个(事件处理)线程么?

引用yyf的发言:

一直有一个疑问未能解开,Event Loop这个机制是不是由一个单独的(浏览器/node)线程控制,比如setTimeout(()=>{}, 1000)方法,延迟1000ms,是谁来监控这1000ms后加入消息queue的,是浏览器的某个(事件处理)线程么?

对的 之前我也很疑惑 如果是单线程只能做一件事 执行到setTimeout 或ajax之后 把他们放入"事件池" 再去执行后续的代码 这个时候就会出现问题 说好的单线程只能做一件事的引擎怎么一边监控事件池的状态一边执行后续的代码呢? 后来偶然看到一位大牛的博客翻译了一篇墙外的文章才突然顿悟(文章地址找不到了)。因为浏览器是多线程啊 当浏览器监控到"事件池"状态更新时会通知改变js引擎 这时候js引擎会在空闲的时候停下来去执行"事件池"里面的回调 所谓空闲的时候是指的是当前引擎执行的语句块上下文执行完毕时的正要执行下一个语句块时的状态 比如你在 setTimeout(()=>{console.log(1000 end)}, 1000) 后面写个for的死循环 因为单线程 你永远也无法得到输出 或是在setTimeout后面写一个函数让其循环1300后它就会在1300后去回调输出而不是1000后

作为NodeJS的初学者,这是我看到过的最好的NodeJS事件循环机制的解析教程,终于弄懂了,谢谢老师!

好像挺简单的, 为什么搞得好像很深奥的样子呢

文章中的这段话:但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

引起了我很大的疑惑,请楼主参考一下
http://ejohn.org/blog/how-javascript-timers-work/

真心搞不懂了。。回调那个对吗?

> 令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

实际上, 应该是只发生在异步 I/O 回调中. 这句话下面给的例子, 我在本地测试输出是有两种可能结果: TIMEOUT FIRED - 1 - 2 和 1 - TIMEOUT FIRED - 2.

如果是下面的例子

```js
const fs = require('fs')
fs.readdir(__dirname, () => {
setImmediate(() => console.log(1))
setTimeout(() => console.log(2), 0)
})
setImmediate(() => console.log('A'))
setTimeout(() => console.log('A'), 0) // setTimeout 和 setImmediate 结果可对换, 因此都输出 A 就好
```

上面例子结果只有两种可能, 一种是当异步回调在当前事件循环被执行时结果为 A 1 A 2. 一种是异步回调在下一个事件循环被执行, 结果是 A A 1 2. 1 永远先于 2 输出. 2 永远在最后输出. 屡试不爽.

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
这句话说错了吧?
function a(callback){
callback();
while(true){

}
}
a(function(){
alert("触发了,没有进入异步队列")
});
能不能解释一下您说的回调进入异步队列,为什么alert还是会执行?

@邢坤:

。。。叫callback就是回调函数了?

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。
个人感觉这里的说法不靠谱,个人理解的回调函数就是在一个函数中调用另外一个函数,只是说异步里使用回调函数的场景很多,但并不能这样("所谓"回调函数"(callback),就是那些会被主线程挂起来的代码")来解释回调函数吧?

任务队列里面放的应该是回调函数,而非事件吧?那上面写的是callback queue,而且逻辑上这样也更通顺吧?

似乎还是有问题啊,例子2执行和推理矛盾

HTML中的自定义事件和Node中的自定义事件是否是异步的?

引用Hiccup的发言:

不正确的文章为什么不删掉

确实啊,不删掉就算了,也不在之前的文章标个大标头提示一下。看了之前那篇,结果再看这篇,开头就是说之前那篇理解有误。能不让人说什么呢?感觉这位博主写的很多不严谨,好多东西都只是引出处再略微表示一下自己的理解,都没有去验证的。不知道为啥名气这么高哦???以后不敢看此人的博客了,生命有限...

有类似动态动画描述下 Node 的两个方法?

引用wuliao的发言:

确实啊,不删掉就算了,也不在之前的文章标个大标头提示一下。看了之前那篇,结果再看这篇,开头就是说之前那篇理解有误。能不让人说什么呢?感觉这位博主写的很多不严谨,好多东西都只是引出处再略微表示一下自己的理解,都没有去验证的。不知道为啥名气这么高哦???以后不敢看此人的博客了,生命有限...

对于我们这些小白来说,不看阮老师的文章会走更多错误的路,看了阮老师的文章,少走很多弯路。
而且,阮老师的文章 通俗易懂,有错误也正常啊。
你自己理解,估计连犯错误的机会都没有吧。能看懂吗?就你这智商?

牛人多了,能让我们这些小白受益的又有几个?
天下之大,免费的有几样?除了阳光?空气?还有什么?
如果你觉得自己非常聪明,那可以不看对吧?

引用wuliao的发言:

确实啊,不删掉就算了,也不在之前的文章标个大标头提示一下。看了之前那篇,结果再看这篇,开头就是说之前那篇理解有误。能不让人说什么呢?感觉这位博主写的很多不严谨,好多东西都只是引出处再略微表示一下自己的理解,都没有去验证的。不知道为啥名气这么高哦???以后不敢看此人的博客了,生命有限...

其实一直以来,阮老师就表明他写的文章只是自己的认识罢了,没有强迫任何一个人认为他如何如何,也没有因为博客的事情和别人撕逼。他只是安安静静自己学习而已。名气大与不大并不是他自己的原因,更不存在故意恶意引导。 不喜欢可以不看,要增强自我辨识能力才是最根本的

引用twang的发言:

我想转载这篇博客,可以吗?

再次找相关资料,来到这里,看到三年多之前自己的评论,有点感慨;
最伤的是,再次感觉对我有帮助。。。

这里面的挂起的任务队列的先进先出是不是有问题,还是我的理解有问题,如果这个异步队列是按先进后出,为什么不是谁先有结果而先输出谁,入可以按着先进 出为什么还是按着栈堆的方式 这不是会很有问题吗 很多没完成的任务被输出了一次

还有为啥用栈的方式 而不是堆的方式 栈的方式是先进后出 如果用堆呢 挂起的时候

个人感觉说的不全面,并不是只简单地分为主任务和task queue吧,起码还有microtask与macrotask,这几者之间也是有执行顺序的,并且同时影响页面刷新的时机

看过阮老师好多文章了,这篇文章有点浅了~

看了文章,你说的主线程、堆、栈、任务队列我都能理解,还是不明白event loop到底是指什么,文章标题是说event loop,但是没看到内容中专门对其的概念性解释说明。因为event loop算是个专业术语,不是只在js里才有,所以能否直接说明下如何理解event loop

看晃神了,所以定义是“主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。”这句话吧

引用人称文武贝的发言:

这里面的挂起的任务队列的先进先出是不是有问题,还是我的理解有问题,如果这个异步队列是按先进后出,为什么不是谁先有结果而先输出谁,入可以按着先进 出为什么还是按着栈堆的方式 这不是会很有问题吗 很多没完成的任务被输出了一次

没问题的。当栈执行完同步任务后,执行异步任务采取的策略是依次执行,一次仅执行一个异步任务的事件,并不是一次性全部读取所有的事件放进栈中,不然就会造成长时间未响应状态

建议初学者不要看ruan大神的文章,有的时候容易被误导。还是去看mdn文档靠谱一些。

感觉有点模糊啊 建议还是Google去看英文吧

引用fans的发言:

大神我有个问题,如何判断主线程结束,可以进入队列。什么时候能第二次进入主线程。

引用manxisuo的发言:

给大家推荐一篇,把Event Loop讲得非常清楚的文章:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
中文:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop

先看的这个,但是感觉 文件写的有些精炼啊,才来的这里

“... 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 ...”

简直胡说八道啊,简直难以置信。

nodejs的eventLoop机制,和android下的handler、looper、message师出同门。
有java语言基础的推荐查看鸿洋大神的博客,源码很简单,理解很透彻。

这里我简要概括一下eventLoop机制的运作原理:

所谓的事件循环机制,就是提供一个线程(例如nodejs线程,例如android下的主线程)专门用于接收和缓存消息(于消息队列)、循环提取(于loop循环)和回调消息,而另外一些线程则用于执行具体的耗时任务,执行完了,再发送消息(缓存到消息队列)以图谋事件回调。

阮老师,你好,我发现本文有一个明显的笔误:
> setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
setTimeout() 可以接受三个参数,第三个是 回调函数所需的参数。

setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});

这段代码我测试了一直都是输出1-2-TIMEOUT FIRED,跟您的有出入

引用人称文武贝的发言:

这里面的挂起的任务队列的先进先出是不是有问题,还是我的理解有问题,如果这个异步队列是按先进后出,为什么不是谁先有结果而先输出谁,入可以按着先进 出为什么还是按着栈堆的方式 这不是会很有问题吗 很多没完成的任务被输出了一次

【主线程的任务队列先进先出的,其实是异步操作完成后的回调函数,而不是那个异步操作】
文章里面有些东西没讲清楚。假如有异步操作(例:ajax,setTimeout等),这个异步操作将由浏览器来处理,等到浏览器处理完成后,会将与这个异步操作绑定的回调函数推入主线程的任务队列中,待主线程的执行栈清空后,主线程会从任务队列中读出最前的一个回调函数并将该回调函数推入主线程的执行栈中,来进行同步执行。

你好阮老师,谢谢你的文章,get到了好多姿势。
这里面有一小段不是很懂。

“异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。”

既然异步不会阻塞,然而js只有一个线程(或者说主进程),那异步执行的那部分的进程在哪?是不是最终这段异步给到主进程的只有一个返回值?或者是cpu重新去开启一个进程吗还是?(像类似于C的中断操作是会增加程序执行的总时间的,但是js明显不会)但是感觉矛盾了啊?这样不是就多进程了?
另外类似于setInterval(程序执行时间>呼叫间隔)这样的它的程序执行还会继续吗?就是CPU还会把那部分未完成的执行吗?(如果是类似于返回程序执行结果给到主进程的某个栈,这时候是不是就嵌不回主进程里面去了?这样是创建>回收,会不会导致系统cpu最终卡死?假设一直轮询下去的话?)

不好意思我是一枚菜鸟。

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。“因为他们属于执行栈的一部分”这里是不是写错了? 应该是属于任务队列啊?

阮老师我有两个疑问:
1、同步任务:主线程上排队执行的任务。是不是说明主线程生成的是一个队列?
下面提到:主线程生成的是执行栈
两种说法不是冲突了嘛!是我哪里理解错误了吗?
2、onload 和onerror方法是异步任务,那么它应该属于“任务队列”的一部分,那么文章中怎么说成了是执行栈的一部分,且执行完它们才会去读取“任务队列”。

引用施耐庵的发言:

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。“因为他们属于执行栈的一部分”这里是不是写错了? 应该是属于任务队列啊?

我也觉得,res.send()应该是同步操作,onload,onerror才是异步的操作,也不知道有没有人回复。

setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});

阮老师,我在node 下执行了一遍,执行结果是:

TIMEOUT FIRED

1

2

是因为setImmediate 和 setTimeout(()=>{},0) 执行顺序可能是随机的吗

二、任务队列中的运行机制,(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

这意思是说异步任务进入任务队列后,就不能执行了,一定要等执行栈中的所有同步任务执行完成才执行异步任务?

看到第四节的event loop的示例图,不知道为什么要放这样一张图,用heap和stack指代主流程,很迷惑人,容易让人以为是执行栈,再者,js中没有可以让程序员分配内存的功能(指的是heap)!

还有说任务队列的事件会自动进入主流程,然后下文又说主流程调用时间队列的回调函数~

不是说了事件轮询吗?浏览器以特定周期去检测任务队列是否有可以执行的任务,有则执行,没有则等待下一次检测

实际上,process.nextTick 是被放在一个叫微任务(microtask)的队列。
与之并列的还有一个叫宏任务(macrotask)的队列。
Event loop 每次循环的过程是这样的:
1.从宏任务队列取一个task执行
2.执行 microtask check point
3.必要时执行UI渲染
4.结束本次循环

第2步其实就是按顺序把微任务队列的microtask依次执行完。

setImmediate,setTimeout 是宏任务,nextTick 是微任务。

这样也许会更好理解一些。

我觉得如果大家意见都这么多,麻烦给小白我推荐一份正确的、容易理解的文章,谢谢!不要只吐槽不产出啊。


setImmediate(function (){
console.log(0);
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});

============
输出:
E:\Projects\nodeProj\test1>node 1.js
0
1
TIMEOUT FIRED
2

E:\Projects\nodeProj\test1>node 1.js
0
TIMEOUT FIRED
1
2

E:\Projects\nodeProj\test1>node 1.js
0
TIMEOUT FIRED
1
2

=============
结论:A并不一定早于setTimeout。

=============
node -v
v10.5.0

引用牛牛的发言:

请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。

18年看到你这条评论,不知道如何再来评论了

setImmediate(function (){
    setImmediate(function A() {
      console.log(1);
      setImmediate(function B(){console.log(2);});
    });
  
    setTimeout(function timeout() {
      console.log('TIMEOUT FIRED');
    }, 0);
  });
  // 1
  // TIMEOUT FIRED
  // 2
这段代码有问题,执行顺序并不是固定的,还是会偶尔变化的。 我在 node v10.13.0 上测试, 还会出现 //TIMEOUT FIRED //1 //2 这种顺序

有个问题:

在执行 setTimeout 的时候,到底是下面那种情况:

1、等待 setTimeout,当时间到达之后,将回调函数插入任务队列。主线程读取执行

2、直接将 setTimeout 的回调函数插入任务队列,主线程读取的时候如果这个任务的执行时间到了 才会去读取执行

软老师,请问全局上下文和函数执行上下文是不是就是 Even Loop 中的执行栈哦??

引用牛牛的发言:

请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。

19年看到你这条评论,不知道如何再来评论了

我是从 libuv中文教程 里面看到这篇文章的,发现很多人都在发表意见,关于event loop的看法,其实看原文就明白了

================

等一下!为什么我们要聊线程?事件循环(event loop)不应该是用来做web编程的方法吗?。哦,不不。线程依旧是处理器完成任务的重要手段。线程因此有可能会派上用场,虽然会使得你不得不艰难地应对各种原始的同步问题。

线程会在内部使用,用来在执行系统调用时伪造异步的假象。libuv通过线程还可以使得程序异步地执行一个阻塞的任务。方法就是大量地生成新线程,然后收集线程执行返回的结果。

================

其实就是 loop per thread + io multiplexing (也即reactor,处理异步操作) + thread pool(处理同步操作)

引用nimeiz的发言:

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
不同任务会分给不同线程?不是直接调用相关操作系统接口?

应该是libuv自己对操作系统封装了一层,libuv本身是多线程的

某些杠精,就知道抬杠,心理变态有问题啊。有本事自己去写一些有影响力的权威的文章来,有本事就把自己的知识和观点亮出来。
感谢阮老师对前端行业做的贡献!

setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2

阮老师,这段代码在 node环境下(v10.16.0)执行结果貌似也是不确定的两种结果:
(1)
// 1
// TIMEOUT FIRED
// 2
(2)
// TIMEOUT FIRED
// 1
// 2

setTimeout 与 Promise 与 Async Await 的对比信息,需要补充一下(have examples is better)

当看到滚动条这么一点小的时候,我就做好了要花点时间来看完这篇文章的打算了,结果看到差不多五分之一的位置就完了。。。总之看评论也受益匪浅,感谢大佬的整理。

朴灵老师对本文做了评注 这个印象笔记打不开了

引用牛牛的发言:

请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。

生命不息,学习不止

时隔几年,再回来看这个事情,觉得 扑灵 说的过了。

太感谢了!受教了。向您学习

引用阮一峰的发言:

我测的结果,一直都是1-T-2。难道这个也不确定…… 

应该是不确定的吧,回调函数中的setTimeout和setImmediate应该是被加到下一次事件循环中,所以执行循序不一定吧

有一点不明白事件循环是执行在主线程中还是新起的线程中啊

搞不懂执行栈,如果执行栈要清空后才能执行任务队列,那么异步请求就都是排队到所有同步操作之后了,是不是这样理解?

阮老师,文章头部提到的《JavaScript标准参考教程》以及朴灵老师的评注,链接都404了,虽然此文已过去多年,但依然有读者,希望更正,谢谢!

为什么timeout和A在同一次Loop,timeout不应该有最小延迟吗,要是A执行的时间延迟没过,不应该和B在同一次Loop吗

刚看完什么是eventloop(哭了

2014 年写的老文章, 不太全面。更全面的,可以在网上找其他文章

阮老师,可否更新一下此篇文章,还有宏任务微任务没有讲呢

为啥有人说.onload与send()存在关联呢?
不是浏览器事件(onload)冒泡才会执行匿名函数吗?
那么send()在事件触发器上面或者是下面有什么关系?

“比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?”
难道不是以最后执行的线程为准?

想问一下宏任务与微任务以及他们和任务队列或者和事件循环中的关系

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接