JS的事件环

栈(Stack)

栈是一种遵循后进先出(LIFO)的数据集合,新添加或待删除的元素都保存在栈的末尾,称作栈顶,另一端称作栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底

感觉说起来并不是很好理解,我们举个例子,比如有一个乒乓球盒,我们不停的向球盒中放进乒乓球,那么最先放进去的乒乓球一定是在最下面,最后放进去的一定是在最上面,那么如果我们想要把这些球取出来是不是就必须依次从上到下才能拿出来,这个模型就是后进先出,就是我们后进入球盒的球反而最先出来。
栈的概念其实在我们js中十分的重要,大家都知道我们js是一个单线程语言,那么他单线程在哪里呢,就在他的主工作线程,也就是我们常说的执行上下文,这个执行上下文就是栈空间,我们来看一段代码:
console.log(‘1‘);
function a(){
console.log(‘2‘);
function b(){
console.log(‘3‘)
}
b()
}
a()

复制代码我们知道函数执行的时候会将这个函数放入到我们的执行上下文中,当函数执行完毕之后会弹出执行栈,那么根据这个原理我们就能知道这段代码的运行过程是

首先我们代码执行的时候会有一个全局上下文,此时代码运行,全局上下文进行执行栈,处在栈底的位置
我们遇到console.log(‘1‘),这个函数在调用的时候进入执行栈,当这句话执行完毕也就是到了下一行的时候我们console这个函数就会出栈,此时栈中仍然只有全局上下文
接着运行代码,这里注意的是我们遇到的函数声明都不会进入执行栈,只有当我们的函数被调用被执行的时候才会进入,这个原理和我们执行栈的名字也就一模一样,接着我们遇到了a();这句代码这个时候我们的a函数就进入了执行栈,然后进入到我们a的函数内部中,此时我们的函数执行栈应该是 全局上下文 —— a
接着我运行console.log(‘2‘),执行栈变成 全局上下文——a——console,接着我们的console运行完毕,我们执行栈恢复成全局上下文 —— a
接着我们遇到了b();那么b进入我们的执行栈,全局上下文——a——b,
接着进入b函数的内部,执行console.log(‘3‘)的时候执行栈为全局上下文——a——b——console,执行完毕之后回复成全局上下文——a——b
然后我们的b函数就执行完毕,然后就被弹出执行栈,那么执行栈就变成全局上下文——a
然后我们的a函数就执行完毕,然后就被弹出执行栈,那么执行栈就变成全局上下文
然后我们的全局上下文会在我们的浏览器关闭的时候出栈

我们的执行上下文的执行过程就是这样,是不是清楚了很多~
通过上面的执行上下文我们可以发现几个特点:

执行上下文是单线程
执行上下文是同步执行代码
当有函数被调用的时候,这个函数会进入执行上下文
代码运行会产生一个全局的上下文,只有当浏览器关闭才会出栈

队列(Queue)

队列是一种遵循先进先出(FIFO)的数据集合,新的条目会被加到队列的末尾,旧的条目会从队列的头部被移出。

这里我们可以看到队列和栈不同的地方是栈是后进先出类似于乒乓球盒,而队列是先进先出,也就是说最先进入的会最先出去。
同样我们举个例子,队列就好比是我们排队过安检,最先来到的人排在队伍的首位,后来的人接着排在队伍的后面,然后安检员会从队伍的首端进行安检,检完一个人就放行一个人,是不是这样的一个队伍就是先进先出的一个过程。
队列这里我们就要提到两个概念,宏任务(macro task),微任务(micro task)。
任务队列
Js的事件执行分为宏仁务和微任务

宏仁务主要是由script(全局任务),setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
微任务主要是process.nextTick, Promise.then, Object.observer, MutationObserver.

浏览器事件环
js执行代码的过程中如果遇到了上述的任务代码之后,会先把这些代码的回调放入对应的任务队列中去,然后继续执行主线程的代码知道执行上下文中的函数全部执行完毕了之后,会先去微任务队列中执行相关的任务,微任务队列清空之后,在从宏仁务队列中拿出任务放到执行上下文中,然后继续循环。

执行代码,遇到宏仁务放入宏仁务队列,遇到微任务放入微任务队列,执行其他函数的时候放入执行上下文
执行上下文中全部执行完毕后,执行微任务队列
微任务队列执行完毕后,再到宏仁务队列中取出第一项放入执行上下文中执行
接着就不停循环1-3的步骤,这就是浏览器环境中的js事件环

//学了上面的事件环 我们来看一道面试题
setTimeout(function () {
  console.log(1);
}, 0);

Promise.resolve(function () {
  console.log(2);
})

new Promise(function (resolve) {
  console.log(3);
});

console.log(4);

//上述代码的输出结果是什么???

复制代码思考思考思考思考~~~
正确答案是3 4 1,是不是和你想的一样?我们来看一下代码的运行流程
// 遇到setTimeout 将setTimeout回调放入宏仁务队列中
setTimeout(function () {
console.log(1);
}, 0);
// 遇到了promise,但是并没有then方法回调 所以这句代码会在执行过程中进入我们当前的执行上下文 紧接着就出栈了
Promise.resolve(function () {
console.log(2);
})
// 遇到了一个 new Promise,不知道大家还记不记得我们上一篇文章中讲到Promise有一个原则就是在初始化Promise的时候Promise内部的构造器函数会立即执行 因此 在这里会立即输出一个3,所以这个3是第一个输入的
new Promise(function (resolve) {
console.log(3);
});
// 然后输入第二个输出4 当代码执行完毕后回去微任务队列查找有没有任务,发现微任务队列是空的,那么就去宏仁务队列中查找,发现有一个我们刚刚放进去的setTimeout回调函数,那么就取出这个任务进行执行,所以紧接着输出1
console.log(4);
复制代码看到上述的讲解,大家是不是都明白了,是不是直呼简单~
那我们接下来来看看node环境中的事件执行环
NodeJs 事件环
浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv标准,因此呢在事件的执行中就会有一定的差异,大家都知道nodejs其实是js的一种runtime,也就是运行环境,那么在这种环境中nodejs的api大部分都是通过回调函数,事件发布订阅的方式来执行的,那么在这样的环境中我们代码的执行顺序究竟是怎么样的呢,也就是我们不同的回调函数究竟是怎么分类的然后是按照什么顺序执行的,其实就是由我们的libuv所决定的。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
复制代码我们先来看下这六个任务是用来干什么的

timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
pending callbacks: 上一轮循环中有少数的 I/O callback会被延迟到这一轮的这一阶段执行。
idle, prepare: 仅内部使用。
poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段
check: 执行setImmediate()设定的回调。
close callbacks: 执行比如socket.on(‘close‘, ...)的回调。

我们再来看网上找到的一张nodejs执行图,我们能看到图中有六个步骤 ,当代码执行中如果我们遇到了这六个步骤中的回调函数,就放入对应的队列中,然后当我们同步人物执行完毕的时候就会切换到下一个阶段,也就是timer阶段,然后timer阶段执行过程中会把这个阶段的所有回调函数全部执行了然后再进入下一个阶段,需要注意的是我们在每次阶段发生切换的时候都会先执行一次微任务队列中的所有任务,然后再进入到下一个任务阶段中去,所以我们就能总结出nodejs的事件环顺序

同步代码执行,清空微任务队列,执行timer阶段的回调函数(也就是setTimeout,setInterval)
全部执行完毕,清空微任务队列,执行pending callbacks阶段的回调函数
全部执行完毕,清空微任务队列,执行idle, prepare阶段的回调函数
全部执行完毕,清空微任务队列,执行poll阶段的回调函数
全部执行完毕,清空微任务队列,执行check阶段的回调函数(也就是setImmediate)
全部执行完毕,清空微任务队列,执行close callbacks阶段的回调函数
然后循环1-6阶段

那我们来练练手~~~
// 我们来对着我们的执行阶段看看
let fs = require(‘fs‘);
// 遇到setTimeout 放入timer回调中
setTimeout(function(){
Promise.resolve().then(()=>{
console.log(‘then1‘);
})
},0);
// 放入微任务队列中
Promise.resolve().then(()=>{
console.log(‘then2‘);
});
// i/o操作 放入pending callbacks回调中
fs.readFile(‘./text.md‘,function(){
// 放入check阶段
setImmediate(()=>{
console.log(‘setImmediate‘)
});
// 放入微任务队列中
process.nextTick(function(){
console.log(‘nextTick‘)
})
});
复制代码首先同步代码执行完毕,我们先清空微任务,此时输出then2,然后切换到timer阶段,执行timer回调,输出then1,然后执行i/o操作回调,然后清空微任务队列,输出nextTick,接着进入check阶段,清空check阶段回调输出setImmediate

原文地址:http://blog.51cto.com/13507333/2157172

时间: 2024-10-11 13:42:39

JS的事件环的相关文章

Node.js知识点整理之----基础知识----事件处理机制及事件环机制

在event模块中,定义了EventEmitter类,所有触发事件的对象都是继承了这个类的子类的实例对象. addListener(event,listener) 对指定事件绑定事件处理函数 on(event,listener) 对指定事件绑定事件处理函数(addListener方法的别名) var http = require('http'); var server = http.createServer(); server.on('request',function(req,res){ co

我已经迷失在事件环(event-loop)中了【Nodejs篇】

我第一次看到他事件环(event-loop)的时候,我是一脸懵,这是什么鬼,是什么循环吗,为什么event还要loop,不是都是一次性的吗? 浏览器中和nodejs环境中的事件环是有一些区别的,这里我只研究了nodejs环境,小黑框情况下的事件环. 这里的事件环并不是指单独一件事件的循环,而是我们写的很多很多的事件按照一定地规则排着队去执行,然后队列清空后继续排队,就是事件环. 事件环很复杂,这里我只有能力解释事件环中的几个点: node.js中对于事件环的解释 宏任务(macro-task),

js阻止浏览器、元素的默认事件与js阻止事件冒泡、阻止事件流

嵌套的div元素,如果父级和子元素都绑定了一些事件,那么在点击最内层子元素时可能会触发父级元素的事件,下面介绍一下js阻止默认事件与js阻止事件冒泡示例,大家参考使用吧 1. event.preventDefault();  -- 阻止元素的默认事件.注:a元素的点击跳转的默认事件 , button,radio等表单元素的默认事件 , div 元素没有默认事件 例: 复制代码代码如下: <a href="http://www.baidu.com" target="_bl

Js 冒泡事件阻止

原文:Js 冒泡事件阻止 1. 事件目标 现在,事件处理程序中的变量event保存着事件对象.而event.target属性保存着发生事件的目标元素.这个属性是DOM API中规定的,但是没有被所有浏览器实现 .jQuery对这个事件对象进行了必要的扩展,从而在任何浏览器中都能够使用这个属性.通过.target,可以确定DOM中首先接收到事件的元素(即实际被单击的元素).而且,我们知道this引用的是处理事件的DOM元素,所以可以编写下列代码:$(document).ready(function

用js onselectstart事件鼠标禁止选中文字

IE&&Chrome中适用此方法 document.onselectstart=function(){return false;} onselectstart是防止内容被选中默认状态是true <body onselectstart=return(event.srcElement.type=='text')> 选不中 <input type="text" name="" value="来选吧"> </

JS(原生)事件委托:为动态创建的节点绑定事件

项目开发中经常需要为动态创建的节点绑定事件, 比如需要创建一个动态列表:在li的数量非常少的时候,为每一个li绑定事件不会存在太多性能方面的问题,但是当列表非常的长,长到上百上千甚至上万的时候(假设),为每个li绑定事件就会对页面性能产生很大的影响.当有大量元素需要绑定相同事件的时候可采用事件委托,将在目标元素上要处理的事件委托给父元素或者祖先元素 优点    事件委托对于web应用程序的性能有如下几个优点:    1.需要管理的函数变少了    2.占用的内存少了    3.javascrip

Atitit. &#160;Js 冒泡事件阻止&#160;事件捕获&#160;&#160;&#160;事件传递 &#160;事件代理

Atitit.  Js 冒泡事件阻止 事件捕获   事件传递  事件代理   1. 事件冒泡1 2. 事件捕获1 3. 同时支持了事件捕获阶段和事件冒泡阶段ddEventListener的第三个参数1 4. 事件代理3 5. 冒泡还是捕获?3 6. Js 冒泡事件阻止3 6.1. 返回false5 7. 事件冒泡 使处理函数有范围较大的触发面积,在“拖拽效果”脚本中是必须的5 8. refe6 8.1.1. 浅谈事件冒泡与事件捕获 - ac黄博客精选 - SegmentFault6   1. 事

原生js阻止事件冒泡代码实例

原生js阻止事件冒泡代码实例:关于什么是事件冒泡这里就不多介绍了,可以参阅javascript事件冒泡简单介绍一章节,任何现象都是双刃剑,有时候利用事件冒泡能够带来便利性,但是有时候也会带来不便,下面就通过带来实例介绍一下如何阻止事件冒泡现象.代码实例如下: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="author" conte

JS之事件监听

一 如果事件监听类似于如下写法,则最终只会执行最后一个事件监听,其他监听都会被覆盖掉. window.onload=funtion(){console.log(1);}; window.onload=funtion(){console.log(2);}; window.onload=funtion(){console.log(3);}; //最终只会输出:"3" 二 如果事件监听类似于如下写法,则每个事件监听都会被执行,其他监听都不会被覆盖掉. --是否冒泡或捕获都不会影响输出结果的次