摘要:上代码代码可以看出,不仅函数比指定的回调函数先执行,而且函数也比先执行。这是因为后一个事件进入的时候,事件环可能处于不同的阶段导致结果的不确定。这是因为因为执行完后,程序设定了和,因此阶段不会被阻塞进而进入阶段先执行,后进入阶段执行。
JavaScript(简称JS)是前端的首要研究语言,要想真正理解JavaScript就绕不开他的运行机制--Event Loop(事件环)
JS是一门单线程的语言,异步操作是实际应用中的重要的一部分,关于异步操作参考我的另一篇文章js异步发展历史与Promise原理分析 这里不再赘述。
堆、栈、队列 堆(heap)堆(heap)是指程序运行时申请的动态内存,在JS运行时用来存放对象。
栈(stack)栈(stack)遵循的原则是“先进后出”,JS种的基本数据类型与指向对象的地址存放在栈内存中,此外还有一块栈内存用来执行JS主线程--执行栈(execution context stack),此文章中的栈只考虑执行栈。
队列(queue)队列(queue)遵循的原则是“先进先出”,JS中除了主线程之外还存在一个“任务队列”(其实有两个,后面再详细说明)。
Event LoopJS的单线程也就是说所有的任务都需要按照一定的规则顺序排队执行,这个规则就是我们要说明的Event Loop事件环。Event Loop在不同的运行环境下有着不同的方式。
浏览器环境下的Event Loop先上图(转自Philip Roberts的演讲《Help, I"m stuck in an event-loop》)
当主线程运行的时候,JS会产生堆和栈(执行栈)
主线程中调用的webaip所产生的异步操作(dom事件、ajax回调、定时器等)只要产生结果,就把这个回调塞进“任务队列”中等待执行。
当主线程中的同步任务执行完毕,系统就会依次读取“任务队列”中的任务,将任务放进执行栈中执行。
执行任务时可能还会产生新的异步操作,会产生新的循环,整个过程是循环不断的。
从事件环中不难看出当我们调用setTimeout并设定一个确定的时间,而这个任务的实际执行时间可能会由于主线程中的任务没有执行完而大于我们设定的时间,导致定时器不准确,也是连续调用setTimeout与调用setInterval会产生不同效果的原因(此处就不再展开,有时间我会多带带写一篇文章)。
接下来上代码:
console.log(1); console.log(2); setTimeout(function(){ console.log(3) setTimeout(function(){ console.log(6); }) },0) setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); }) },0) console.log(5)
代码中的setTimeout的时间给得0,相当于4ms,也有可能大于4ms(不重要)。我们要注意的是代码输出的顺序。我们把任务以其输出的数字命名。
先执行的一定是同步代码,先输出1,2,5,而3任务,4任务这时会依次进入“任务队列中”。同步代码执行完毕,队列中的3会进入执行栈执行,4到了队列的最前端,3执行完后,内部的setTimeout将6的任务放入队列尾部。开始执行4任务……
最终我们得到的输出为1,2,5,3,4,6,7。
宏任务与微任务任务队列中的所有任务都是会乖乖排队的吗?答案是否定的,任务也是有区别的,总是有任务会有一些特权(比如插队),就是任务中的vip--微任务(micro-task),那些没有特权的--宏任务(macro-task)。
我们看一段代码:
console.log(1); setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log("promise") }) }) setTimeout(function(){ console.log(3); })
按照“队列理论”,结果应该为1,2,3,promise。可是实际结果事与愿违输出的是1,2,promise,3。
明明是3先进入的队列 ,为什么promise会排在前面输出?这是因为promise有特权是微任务,当主线程任务执行完毕微任务会排在宏任务前面先去执行,不管是不是后来的。
换句话说,就是任务队列实际上有两个,一个是宏任务队列,一个是微任务队列,当主线程执行完毕,如果微任务队列中有微任务,则会先进入执行栈,当微任务队列没有任务时,才会执行宏任务的队列。
微任务包括: 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver, MessageChannel;
宏任务包括:setTimeout, setInterval, setImmediate, I/O;
Node环境下的Event Loop┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
node中的时间循环与浏览器的不太一样,如图:
timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
I/O callbacks 阶段: 执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
idle, prepare 阶段: 仅node内部使用;
poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
check 阶段: 执行setImmediate() 设定的callbacks;
close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行。
每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,
node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,
event loop会转入下一下阶段。
process.nextTick方法不在上面的事件环中,我们可以把它理解为微任务,它的执行时机是当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行。上代码:
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
代码可以看出,不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
setTimeout 和 setImmediate二者非常相似,但是二者区别取决于他们什么时候被调用.
setImmediate 设计在poll阶段完成时执行,即check阶段;
setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行
其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的。
setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
这是因为后一个事件进入的时候,事件环可能处于不同的阶段导致结果的不确定。当我们给了事件环确定的上下文,事件的先后就能确定了。
var fs = require("fs") fs.readFile(__filename, () => { setTimeout(() => { console.log("timeout") }, 0) setImmediate(() => { console.log("immediate") }) })
$ node timeout_vs_immediate.js immediate timeout
这是因为因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/93496.html
摘要:主线程在任务队列中读取事件,这个过程是循环不断地,所以这种运行机制叫做事件循环是在执行栈同步代码结束之后,下一次任务队列执行之前。 单线程 javascript为什么是单线程语言,原因在于如果是多线程,当一个线程对DOM节点做添加内容操作的时候,另一个线程要删除这个DOM节点,这个时候,浏览器应该怎么选择,这就造成了混乱,为了解决这类问题,在一开始的时候,javascript就采用单线...
摘要:中线程运行机制详解对于我们都知道,他是个单线程语言,但是准确来说它是拥有一个执行程序主线程,和消息队列辅线程,以及各个真正处理异步操作的工作线程。 JavaScript中线程运行机制详解 对于JavaScript我们都知道,他是个单线程语言,但是准确来说它是拥有一个执行程序主线程,和消息队列辅线程(Event Loop),以及各个真正处理异步操作的工作线程。当主线程执行JS程序的时候,...
摘要:机制详解与中实践应用归纳于笔者的现代开发语法基础与实践技巧系列文章。事件循环机制详解与实践应用是典型的单线程单并发语言,即表示在同一时间片内其只能执行单个任务或者部分代码片。 JavaScript Event Loop 机制详解与 Vue.js 中实践应用归纳于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文依次介绍了函数调用栈、MacroTask 与 Micr...
摘要:曾经的理解首先,是单线程语言,也就意味着同一个时间只能做一件事,那么为什么不是多线程呢这样还能提高效率啊假定同时有两个线程,一个线程在某个节点上编辑了内容,而另一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪个操作呢所以,设计者把 Event Loop曾经的理解 首先,JS是单线程语言,也就意味着同一个时间只能做一件事,那么 为什么JavaScript不是多线程呢?这样还能提...
摘要:主线程要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。主线程循环即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。以上参考资料详解中的事件循环机制中的事件循环运行机制详解再谈 showImg(https://segmentfault.com/img/remote/1460000015317437?w=1920&h=1080); 前言 大家都...
阅读 3322·2021-11-22 12:04
阅读 2704·2019-08-29 13:49
阅读 481·2019-08-26 13:45
阅读 2237·2019-08-26 11:56
阅读 997·2019-08-26 11:43
阅读 586·2019-08-26 10:45
阅读 1265·2019-08-23 16:48
阅读 2157·2019-08-23 16:07