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指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

(完)

珠峰培训

简寻

留言(91条)

写的不错

补充: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%之后,马上出现一个空白页,提示无法连接。郁闷中!! 如果才能正常联接,期待您的指点。我的邮箱为:qudetong@www5a.com,手机: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实现异步操作。

我要发表看法

«-必填

«-必填,不公开

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