一年前,我写了一篇《什么是 Event Loop?》,谈了我对Event Loop的理解。
上个月,我偶然看到了Philip Roberts的演讲《Help, I'm stuck in an event-loop》。这才尴尬地发现,自己的理解是错的。我决定重写这个题目,详细、完整、正确地描述JavaScript引擎的内部运行机制。下面就是我的重写。
进入正文之前,插播一条消息。我的新书《ECMAScript 6入门》出版了(版权页,内页1,内页2),铜版纸全彩印刷,非常精美,还附有索引(当然价格也比同类书籍略贵一点点)。预览和购买点击这里。
(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)主线程之外,还存在一个"任务队列"(taskqueue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
下图就是主线程和任务队列的示意图。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
三、事件和回调函数
"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
四、Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an 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的运行机制如下。
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责NodeAPI的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。
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指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。
(完)
ceclinux 说:
写的不错
2014年10月 8日 21:28 | # | 引用
wuwenjie 说:
补充: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.
2014年10月 8日 21:50 | # | 引用
Tim 说:
简言之就是JS只有一个主线程,主线程执行完执行栈的任务后去检查异步的任务队列,如果异步事件触发,则将其加到主线程的执行栈。
2014年10月 8日 22:24 | # | 引用
jruif 说:
在https://medium.com/@shijuvar/web-development-trends-for-2015-and-beyond-c2d3c1ef5718这篇文章中的关于nodejs的可维护性、可用性和高性能的论证不知道阮一峰大哥怎么看?
2014年10月 8日 23:27 | # | 引用
jare 说:
今天发现#atom-shell#做同步ipc调用时,执行栈是可能被挂起的,相当于主线程sleep了,等ipc返回后才会接着执行后续代码。如果挂起的时候同一段代码被event又一次触发,逻辑就可能出错。
2014年10月 9日 00:41 | # | 引用
jare 说:
也就是说执行栈可能不能在一个loop内被执行完,因为会被挂起,是否挂起取决于运行环境和执行的操作种类。被挂起后前面注册的callback还是有可能先于执行栈剩余的代码触发。
2014年10月 9日 00:43 | # | 引用
rednax 说:
补充一下Timer相关的问题:
1. 其实并不是“有些浏览器规定了setTimeout 的最小时间间隔”,这个最小时间间隔是W3C在HTML标准中规定,规定要求低于4ms的时间间隔算为4ms。
2. process.nextTick在最新版nodejs中不被推荐使用,推荐使用setImmediate ,原因在于nextTick是在当前帧介绍后立即执行,会阻断IO并且有最大数量限制;而setImmediate不会阻断IO,更像是setTimeout(func, 0)
2014年10月 9日 01:06 | # | 引用
阮一峰 说:
@rednax:
谢谢指出,我还不知道HTML5把4毫秒写入了标准,已经改过来了。
我查了Node.js 10.0的发布说明( http://blog.nodejs.org/2013/03/11/node-v0-10-0-stable/ ),对于process和setImmediate的差异,还在消化当中。
2014年10月 9日 06:31 | # | 引用
nimeiz 说:
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
不同任务会分给不同线程?不是直接调用相关操作系统接口?
2014年10月 9日 08:37 | # | 引用
twang 说:
我想转载这篇博客,可以吗?
2014年10月 9日 09:11 | # | 引用
roast_soul 说:
我觉得单线程的异步没什么意义。你看一下这个文章:
http://cnn237111.blog.51cto.com/2359144/1556987
2014年10月 9日 09:37 | # | 引用
万能小新 说:
requestAnimFrame应该是requestAnimationFrame吧。
2014年10月 9日 09:39 | # | 引用
chptx 说:
单线程效率高是假,简化开发是真。
跟GUI相关的框架的都采用这种模型,QT、GTK、MFC、Android UI框架、这里的JS都是这样的。
2014年10月 9日 12:02 | # | 引用
numbbbbb 说:
setImmediate的例子有问题吧,触发A之后并不是直接输出1,而是执行另一个setImmediate,这个setImmediate会把B添加到任务队列尾部,在执行B之前会触发setTimeout的回调,所以例子的输出顺序应该是:
// TIMEOUT FIRED
// 1
// 2
2014年10月 9日 22:52 | # | 引用
阮一峰 说:
@numbbbbb:
谢谢指出,你说得对,例子有错,已经改过来了。
2014年10月 9日 23:11 | # | 引用
阮一峰 说:
谢谢指出,改过来了。
2014年10月 9日 23:39 | # | 引用
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
===========
同样的这段代码, 为什么后边的说 Immediate 和 Timeout 的先后顺序不确定呢? 求解释...
2014年10月10日 17:38 | # | 引用
mamamiya 说:
https://app.yinxiang.com/shard/s8/sh/b72fe246-a89d-434b-85f0-a36420849b84/59bad790bdcf6b0a66b8b93d5eacbead
朴灵对您文章的评注。语言虽然略显犀利,但是个人觉得评注的应该是对的。
2014年10月10日 19:03 | # | 引用
阮一峰 说:
@Leo:
谢谢指出,我修改文章时,脑子糊涂了,已经改过来了。
2014年10月10日 19:22 | # | 引用
csu 说:
https://app.yinxiang.com/shard/s8/sh/b72fe246-a89d-434b-85f0-a36420849b84/59bad790bdcf6b0a66b8b93d5eacbead阮老师,有一位高手(前端专家朴灵)对你的这篇文章提出了不少看法,你可以看看。
2014年10月10日 20:22 | # | 引用
Dean 说:
https://app.yinxiang.com/shard/s8/sh/b72fe246-a89d-434b-85f0-a36420849b84/59bad790bdcf6b0a66b8b93d5eacbead
阮老师 您被打脸了
2014年10月11日 09:32 | # | 引用
SO 说:
门都还没入就来写科普
2014年10月11日 09:39 | # | 引用
BlueiceQ 说:
https://app.yinxiang.com/shard/s8/sh/b72fe246-a89d-434b-85f0-a36420849b84/59bad790bdcf6b0a66b8b93d5eacbead
阮老师,看了没?
2014年10月11日 10:51 | # | 引用
金戈铁马 说:
打人不打脸,绝交。。
2014年10月11日 14:50 | # | 引用
rednax 说:
2014年10月12日 16:39 | # | 引用
zzm 说:
我去,被朴灵狠劈,现在更乱了
2014年10月12日 19:48 | # | 引用
牛牛 说:
请问你一个问题,你的主业是金融,为什么还花这么多时间在编程上?即使你明知道不管你如何努力地来提高自己的编程水平,也无法改变业余的地位,也得不到什么实际的回报。
2014年10月12日 20:03 | # | 引用
zzm 说:
多看几眼还真是
2014年10月12日 20:09 | # | 引用
淘杰 说:
"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结束后会执行改链表里所有的函数~
2014年10月12日 22:47 | # | 引用
淘杰 说:
@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 的先后顺序不确定。
2014年10月12日 23:08 | # | 引用
阮一峰 说:
谢谢指出,确实如此,已经改正了。
2014年10月13日 09:11 | # | 引用
crazy 说:
究竟谁对谁错呢?现在有些混乱了
2014年10月13日 09:31 | # | 引用
Jak Wings 说:
普通应用没必要这么吹毛球疵,只要了解一下 process.nextTick 和 setImmediate 的区别就好了,官方的 API 说明已经解释得很好,带实际应用示例。阮先生没有把官方介绍的 process.nextTick 实际用例放上来直接硬剥是有点无趣了。
2014年10月13日 09:39 | # | 引用
Jak Wings 说:
不知道哪里来的「事实上」,官方示例已经明确地说「This is not a simple alias to setTimeout(fn, 0)」了,当然也更不可能原本作为 setImmediate 的替代,或者反过来。process.nextTick 是有特殊用途的,并还是单单脱离原有的 event loop。
2014年10月13日 09:46 | # | 引用
Jak Wings 说:
囧,抱歉,才看了官方发布说明,看来 process.nextTick 是被滥用后才加上 setImmediate 的。不过我还是觉得 API 说明页面的说明比发布那时的好,setImmediate 是和 setTimeout 使用同一个 event loop 的,应该凸出了和 process.nextTick 的不同了,应该也是后来提防大家滥用才这么设定的。
2014年10月13日 09:56 | # | 引用
Jak Wings 说:
我刚刚用 v0.10.32 和 v0.11.14测试过了,这个也没有保证。我能得到 1-T...-2 和 1-2-T... 两种结果,甚至是 T...-1-2。信不信由你。
2014年10月13日 10:14 | # | 引用
阮一峰 说:
我测的结果,一直都是1-T-2。难道这个也不确定……
2014年10月13日 14:06 | # | 引用
Leo 说:
@淘杰:
谢谢解释~
2014年10月13日 14:44 | # | 引用
林夕轩 说:
这么解释的确合理,定时器的原理都是如此,告诉cpu有个任务在某个时间应该执行。但也只有cpu有空的时候,才能尽快执行才可。
2014年10月14日 11:10 | # | 引用
猴子 说:
读完正文觉得很多东西是纯理论的理解,有些可能来源于猜测。
这篇文章更该是一个学习笔记,文中提到的很多event其实和Javascript语言完全无关,纯粹是浏览器的处理和行为。
对于任何一个不基于浏览器且支持多线程的程序来讲(比如随便一个C实现的游戏),会发现同样的事件处理比如鼠标操作、键盘快捷键等等,是需要去自己单独实现的,那么也许实现中使用了Event loop,但这一部分从未集成到语言中,对吧?
理解这个事情更好的办法是真的去实现一个类似的程序。
2014年10月14日 16:37 | # | 引用
陈佳 说:
看来我需要买书好好学学了。
2014年10月16日 01:51 | # | 引用
Jak Wings 说:
对了,V8 引擎能代表所有实现了 ES6 的浏览器的具体实现么?
2014年10月16日 15:09 | # | 引用
阮一峰 说:
问题是Node采用了V8
2014年10月16日 17:33 | # | 引用
stackvoid 说:
的确有些说法不太严谨~~~
2014年10月16日 17:36 | # | 引用
爱因斯坦 说:
请问你一个问题,你的主业是专利局小职员,为什么还花这么多时间在物理上?即使你明知道不管你如何努力地来提高自己的数学水平,也无法改变业余的地位,也得不到什么实际的回报。
2014年10月18日 03:45 | # | 引用
朴素博客 说:
2014年10月18日 17:18 | # | 引用
腊月乘凉 说:
阮大侠你就别再祸害程序员了,建议先补一下计算机组成原理,编译原理,算法导论,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。
2014年10月22日 00:09 | # | 引用
jiangjunzhangas 说:
不过瘾
2014年10月22日 22:02 | # | 引用
ARM Linux 说:
Js原来还有这么深奥的功能
2014年10月28日 13:53 | # | 引用
小雪 说:
阮老师已经是程序员了……
2014年10月30日 19:38 | # | 引用
Hiccup 说:
不正确的文章为什么不删掉
2014年10月31日 11:54 | # | 引用
reducm 说:
想不到阮老师去了阿里巴巴做前端阿
2014年10月31日 13:53 | # | 引用
渠德通 说:
很长一段时间,google日历无法访问,但网上又找不到好用的在线日历软件,偶然间看到您开发的这个简易版google日历,发自内心的高兴,但所有的浏览器都用了,就是无法联接,联的过程中,显示一个进度条,等到100%之后,马上出现一个空白页,提示无法连接。郁闷中!! 如果才能正常联接,期待您的指点。我的邮箱为:[email protected],手机:13501282988.如果可能能话,我愿意为您长期进行测试各项功能
2014年10月31日 15:10 | # | 引用
alex 说:
欢迎兔哥加入大阿里!
2014年10月31日 21:38 | # | 引用
陈明 说:
强烈建议博主开个微信公众号!
2014年11月 2日 09:30 | # | 引用
Byron 说:
是为了和朴灵PK才来的么?
2014年11月 3日 10:20 | # | 引用
花生大仙 说:
好久没更新博客了,甚是想念。
2014年11月 3日 14:01 | # | 引用
赵秀芳 说:
听说阮先生,要去蚂蚁金服了...
阮先生好久不更新了,要记得常常更新哦。
2014年11月 6日 11:05 | # | 引用
Steven 说:
@腊月乘凉:
确实是这样的
2014年11月11日 00:14 | # | 引用
雁南飞 说:
有没有办法查看Function Object的internal property?
2014年11月25日 11:45 | # | 引用
felix 说:
有一个问题,事件执行的粒度是多大?是一个函数还是一个语句块?
在大量的异步方法同时操作一个变量的时候,会不会出现类似线程的问题呢?
2014年11月25日 11:57 | # | 引用
袁梓民 说:
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无法执行,等到数组操作完成之后才能执行,这样计时操作似乎没有存在的意义了。
2014年11月26日 14:14 | # | 引用
stubbornleaf 说:
原来水平也不过如此。有点水平的人都看得懂朴灵的批注比较准确。
2014年12月 2日 07:13 | # | 引用
rufushuang 说:
从这篇文章看得出,阮老师不是科班出身的
不过非科班的也有非科班的好,希望能够继续有文章看
2014年12月11日 14:52 | # | 引用
水中月镜中花 说:
阮老师,那如果在ajax的回调函数中,存在setTimeOut函数,这个时候回调函数中还是按照Event loop的机制吗?就是说回调函数相当于主线程,把setTimeOut函数放在任务对象,先执行回调函数的代码再执行执行setTimeout函数?
2014年12月17日 14:59 | # | 引用
欧文 说:
阮老师,你的站点统计工具:specificclick好像不工作耶,请须知。
2015年1月26日 14:28 | # | 引用
linda 说:
对您的网站有一些小提议,供您参考。
1、给页面右下角或者右侧加一个按钮,可以直接跳到最顶层,这样对读者来说很方便,因为一直拖动滚动条很麻烦。同时,这个按钮相对于浏览器窗口是固定的。
2、给标题下面加上您文章的写作日期。作为读者我感觉这个时间挺重要的。
2015年1月28日 15:43 | # | 引用
Taomax 说:
嗯,还是比较实用的内容呢,我会长期关注的。
2015年2月 5日 20:48 | # | 引用
stupid 说:
请不要说你在淘宝工作好吗?淘宝不都是大牛吗?
2015年3月10日 11:22 | # | 引用
stupid 说:
淘宝的人都喜欢出书吗?竟然都敢出书?
2015年3月10日 11:24 | # | 引用
1024bit 说:
擦,能不能不要绑定宿主环境。
2015年3月31日 17:16 | # | 引用
简陋的以数据 说:
感觉文章的表述有些乱,比如“不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。”,这句让人理解的意思是“任务队列”主动通知“主线程”。而后面所表达的却是“主线程”在执行栈清空后才会到任务队列中取任务。前后主被动关系矛盾啊。
还有,“(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。”。这时异步任务还在队列里没执行呢,怎么就会有了运行结果呢?
烦请阮老师在方便的时候不吝赐教。同时,感谢阮老师每次耐心细致的讲解和分享!
2015年4月 5日 11:28 | # | 引用
oceanstick 说:
定时器执行流程还是没讲清楚。。。。
2015年4月 7日 19:49 | # | 引用
timdus 说:
能说说这个网站是怎么建 的么
2015年4月 9日 01:44 | # | 引用
云霄 说:
不管扑灵和你都写的云里雾里的。。。不透彻。。
2015年7月 6日 20:05 | # | 引用
青苔 说:
老师你好,请教个问题,如果是如图示Execution Context Stack所展示的那样,岂不是后来的Context先执行?
那为何
console.log('a');
console.log('b');
可以得到我们所要的执行顺序呢?
2015年7月12日 16:28 | # | 引用
andy 说:
结果不总是1--TIMEOUT FIRED--2 还可能是TIMEOUT--1--2。
node 版本v0.12.2 windows 64位系统
2015年8月25日 17:43 | # | 引用
manxisuo 说:
@腊月乘凉:
的确,这个地方阮老师的说法是不对的。两者并不等价。
2015年9月 6日 13:45 | # | 引用
manxisuo 说:
给大家推荐一篇,把Event Loop讲得非常清楚的文章:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
中文:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
2015年9月14日 13:56 | # | 引用
harole 说:
问题图片丢失。
2015年9月23日 10:20 | # | 引用
shzle 说:
执行栈(execution context stack)清空的时机是什么?
2015年9月26日 12:30 | # | 引用
kirito 说:
同惑啊,“异步任务有了运行结果,就在'任务队列'之中放置一个事件”,这句不大理解呢,文中不是说同步任务清空之后才执行任务队列的么?前面这个异步的结果是哪里的?求解~~
2015年10月 9日 15:17 | # | 引用
夏爽 说:
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下显示效果不同
2015年10月17日 21:40 | # | 引用
fans 说:
大神我有个问题,如何判断主线程结束,可以进入队列。什么时候能第二次进入主线程。
2015年11月 8日 21:44 | # | 引用
alex 杰 说:
技术学术自然有需要严谨的地方,老师自然不可能触类旁引,将各个面都涉及的东西讲透,所以有些语言带过,也有确实考虑不周的地方。同样是做技术的,我觉有人跟进技术潮流,出来讲就很好了,至少人家在治学,敢于发表,听取意见。
2015年12月 5日 14:40 | # | 引用
Lion 说:
文中提到 “如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。”,
提到“setImmediate方法则是在当前"任务队列"的尾部添加事件”,
又提到“Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面”。
这三块东西逻辑矛盾了,肯定有谁出错了,求指点。
2015年12月23日 16:13 | # | 引用
tom 说:
所有windows平台的软件都基于消息队列循环方式。浏览器只是普通的windows本地程序,和winform,mfc等都是同样的机制
2015年12月31日 14:16 | # | 引用
just coding 说:
读了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是单线程的,开浏览器的时候,会有一些默认每次都要执行的代码属于主任务,它们放在执行堆栈上,当它们执行完毕之后,主线程会去异步队列去找那一些准备好了的任务接着执行。前者执行完毕,循环的主要是后者那些异步队列里的任务了。
2016年1月 3日 20:26 | # | 引用
k 说:
看得我肚子饿了
2016年3月11日 17:44 | # | 引用
a 说:
这文章误导人啊
2016年4月13日 22:03 | # | 引用
LeeSir 说:
第一个问题阮老师表述可能有问题,但是意思还是执行完js主线程的代码才会去看浏览器任务队列中的事件,再执行js代码中该事件对应的代码。
第二个问题我的理解是,任务队列里放的是ajax这类的任务,是交给浏览器发起HTTP请求去执行的,当有了返回结果就会在任务队列中增加一个事件表示该ajax请求已经返回了结果,任务队列里的任务和js主线程是同时执行的。 不影响js是单线程的这个结论,只能说浏览器还会提供接口来供js实现异步操作。
2016年6月28日 12:26 | # | 引用
sweetvvck 说:
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
2016年7月25日 21:51 | # | 引用
helloworld 说:
还是你解释的比较清楚。
2016年9月12日 17:22 | # | 引用
winder 说:
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感觉它会在插队"任务队列"(时间点到了,就优先插队到任务队列队首)
2016年10月18日 10:55 | # | 引用
amber 说:
发现我们培训老师讲的课应该是借鉴了阮老师的文章。。。有的原话都一样。。
2016年10月23日 20:06 | # | 引用
叮当卡丁 说:
“挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。”
这句话我有一点不解,这个异步任务是要等所有同步任务都执行完毕了之后进入主线程呢?还是只要有IO设备返回结果就立马进入主线程,可以插队到已在主线程的同步任务前面?
2016年11月 4日 17:32 | # | 引用
Allen 说:
自定义的回调函数会放到消息队列中吗?
2016年11月11日 10:28 | # | 引用
Jim 说:
关于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
2016年11月24日 15:50 | # | 引用
Jim 说:
上面的**标记错了,应该是上边那句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');});
});
});
2016年11月24日 15:52 | # | 引用
Jim 说:
文章写得很好,书也写得好!
http://es6.ruanyifeng.com/#docs/async
2016年11月24日 15:57 | # | 引用
王子龙 说:
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
2016年12月30日 15:02 | # | 引用
losymear 说:
呀 朴灵老师批注找不到了
2016年12月30日 16:37 | # | 引用
kevin 说:
如果执行栈(主线程)在执行完正要loop任务队列前,主线程的代码又被触发执行了一次会怎么样?比如两次点击,是会先执行任务队列还是主线程的代码
2017年1月 9日 14:56 | # | 引用
和中堂 说:
阮老师是一位不错的践行者,我前端典范。
2017年1月18日 14:11 | # | 引用
goodyboy6 说:
2017年2月 7日 10:59 | # | 引用
goodyboy6 说:
根据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,从而及时处理回调。
2017年2月 7日 11:38 | # | 引用
hank 说:
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
2017年2月19日 17:43 | # | 引用
yyf 说:
一直有一个疑问未能解开,Event Loop这个机制是不是由一个单独的(浏览器/node)线程控制,比如setTimeout(()=>{}, 1000)方法,延迟1000ms,是谁来监控这1000ms后加入消息queue的,是浏览器的某个(事件处理)线程么?
2017年2月27日 12:15 | # | 引用
杨守科 说:
对的 之前我也很疑惑 如果是单线程只能做一件事 执行到setTimeout 或ajax之后 把他们放入"事件池" 再去执行后续的代码 这个时候就会出现问题 说好的单线程只能做一件事的引擎怎么一边监控事件池的状态一边执行后续的代码呢? 后来偶然看到一位大牛的博客翻译了一篇墙外的文章才突然顿悟(文章地址找不到了)。因为浏览器是多线程啊 当浏览器监控到"事件池"状态更新时会通知改变js引擎 这时候js引擎会在空闲的时候停下来去执行"事件池"里面的回调 所谓空闲的时候是指的是当前引擎执行的语句块上下文执行完毕时的正要执行下一个语句块时的状态 比如你在 setTimeout(()=>{console.log(1000 end)}, 1000) 后面写个for的死循环 因为单线程 你永远也无法得到输出 或是在setTimeout后面写一个函数让其循环1300后它就会在1300后去回调输出而不是1000后
2017年3月19日 16:13 | # | 引用
CRONWMMM 说:
作为NodeJS的初学者,这是我看到过的最好的NodeJS事件循环机制的解析教程,终于弄懂了,谢谢老师!
2017年3月29日 13:40 | # | 引用
s 说:
好像挺简单的, 为什么搞得好像很深奥的样子呢
2017年4月12日 15:33 | # | 引用
tiny 说:
文章中的这段话:但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
引起了我很大的疑惑,请楼主参考一下
http://ejohn.org/blog/how-javascript-timers-work/
2017年4月21日 11:10 | # | 引用
哈哈 说:
真心搞不懂了。。回调那个对吗?
2017年4月27日 14:32 | # | 引用
瑞铭 说:
> 令人困惑的是,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 永远在最后输出. 屡试不爽.
2017年6月10日 21:55 | # | 引用
邢坤 说:
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
这句话说错了吧?
function a(callback){
callback();
while(true){
}
}
a(function(){
alert("触发了,没有进入异步队列")
});
能不能解释一下您说的回调进入异步队列,为什么alert还是会执行?
2017年6月29日 15:30 | # | 引用
light 说:
@邢坤:
。。。叫callback就是回调函数了?
2017年7月 2日 16:22 | # | 引用
素弥 说:
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。
个人感觉这里的说法不靠谱,个人理解的回调函数就是在一个函数中调用另外一个函数,只是说异步里使用回调函数的场景很多,但并不能这样("所谓"回调函数"(callback),就是那些会被主线程挂起来的代码")来解释回调函数吧?
2017年7月10日 10:32 | # | 引用
薛皓月 说:
任务队列里面放的应该是回调函数,而非事件吧?那上面写的是callback queue,而且逻辑上这样也更通顺吧?
2017年7月12日 22:15 | # | 引用
kaimi 说:
似乎还是有问题啊,例子2执行和推理矛盾
2017年8月 9日 11:18 | # | 引用
Lemon 说:
HTML中的自定义事件和Node中的自定义事件是否是异步的?
2017年8月24日 17:53 | # | 引用
wuliao 说:
确实啊,不删掉就算了,也不在之前的文章标个大标头提示一下。看了之前那篇,结果再看这篇,开头就是说之前那篇理解有误。能不让人说什么呢?感觉这位博主写的很多不严谨,好多东西都只是引出处再略微表示一下自己的理解,都没有去验证的。不知道为啥名气这么高哦???以后不敢看此人的博客了,生命有限...
2017年9月25日 12:28 | # | 引用
殷敏峰 说:
有类似动态动画描述下 Node 的两个方法?
2017年10月12日 17:19 | # | 引用
aaa 说:
对于我们这些小白来说,不看阮老师的文章会走更多错误的路,看了阮老师的文章,少走很多弯路。
而且,阮老师的文章 通俗易懂,有错误也正常啊。
你自己理解,估计连犯错误的机会都没有吧。能看懂吗?就你这智商?
牛人多了,能让我们这些小白受益的又有几个?
天下之大,免费的有几样?除了阳光?空气?还有什么?
如果你觉得自己非常聪明,那可以不看对吧?
2017年11月 9日 11:17 | # | 引用
Jeff 说:
其实一直以来,阮老师就表明他写的文章只是自己的认识罢了,没有强迫任何一个人认为他如何如何,也没有因为博客的事情和别人撕逼。他只是安安静静自己学习而已。名气大与不大并不是他自己的原因,更不存在故意恶意引导。 不喜欢可以不看,要增强自我辨识能力才是最根本的
2017年11月14日 10:17 | # | 引用
twang 说:
再次找相关资料,来到这里,看到三年多之前自己的评论,有点感慨;
最伤的是,再次感觉对我有帮助。。。
2018年1月14日 02:06 | # | 引用
人称文武贝 说:
这里面的挂起的任务队列的先进先出是不是有问题,还是我的理解有问题,如果这个异步队列是按先进后出,为什么不是谁先有结果而先输出谁,入可以按着先进 出为什么还是按着栈堆的方式 这不是会很有问题吗 很多没完成的任务被输出了一次
2018年1月19日 16:59 | # | 引用
人称文武贝 说:
还有为啥用栈的方式 而不是堆的方式 栈的方式是先进后出 如果用堆呢 挂起的时候
2018年1月19日 17:19 | # | 引用
longnit 说:
个人感觉说的不全面,并不是只简单地分为主任务和task queue吧,起码还有microtask与macrotask,这几者之间也是有执行顺序的,并且同时影响页面刷新的时机
2018年1月23日 09:55 | # | 引用