美文网首页ITBOX码无界前端WEB开发秘籍
前端基础进阶(十二):深入核心,详解事件循环机制

前端基础进阶(十二):深入核心,详解事件循环机制

作者: 这波能反杀 | 来源:发表于2017-03-23 09:25 被阅读19095次
Event Loop

JavaScript的学习零散而庞杂,因此很多时候我们学到了一些东西,但是却没办法感受到自己的进步,甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直试图在寻找一条核心的线索,只要我根据这条线索,我就能够一点一点的进步。

前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。所以,我就马不停蹄的去深入的学习了事件循环机制,并总结出了这篇文章跟大家分享。

事件循环机制从整体上的告诉了我们所写的JavaScript代码的执行顺序。但是在我学习的过程中,找到的许多国内博客文章对于它的讲解浅尝辄止,不得其法,很多文章在图中画个圈就表示循环了,看了之后也没感觉明白了多少。但是他又如此重要,以致于当我们想要面试中高级岗位时,事件循环机制总是绕不开的话题。特别是ES6中正式加入了Promise对象之后,对于新标准中事件循环机制的理解就变得更加重要。这就很尴尬了。

最近有两篇比较火的文章也表达了这个问题的重要性。

这个前端面试在搞事
80% 应聘者都不及格的 JS 面试题

但是很遗憾的是,大神们告诉了大家这个知识点很重要,却并没有告诉大家为什么会这样。所以当我们在面试时遇到这样的问题时,就算你知道了结果,面试官再进一步问一下,我们依然懵逼。

在学习事件循环机制之前,我默认你已经懂得了如下概念,如果仍然有疑问,可以回过头去看看我以前的文章。

  • 执行上下文(Execution context)
  • 函数调用栈(call stack)
  • 队列数据结构(queue)
  • Promise(我会在下一篇文章专门总结Promise的详细使用)

因为chrome浏览器中新标准中的事件循环机制与nodejs类似,因此此处就整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate

OK,那我就先抛出结论,然后以例子与图示详细给大家演示事件循环机制。

  • 我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。

当然新标准中的web worker涉及到了多线程,我对它了解也不多,这里就不讨论了。

  • JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
队列数据结构
  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。

  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。

// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
    console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。

// demo01  出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');

首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。

首先script任务开始执行,全局上下文入栈

第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。

setTimeout(function() {
    console.log('timeout1');
})
宏任务timeout1进入setTimeout队列

第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。

因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。

promise1入栈执行,这时promise1被最先输出 resolve在for循环中入栈执行 构造函数执行完毕的过程中,resolve执行完毕出栈,promise2输出,promise1页出栈,then执行时,Promise任务then1进入对应队列

script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。

第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。

执行所有的微任务

第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。

微任务被清空

这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。

timeout1入栈执行

这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。

那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。

这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复杂一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。

// demo02
console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})

这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。

第一步:宏任务script首先执行。全局入栈。glob1输出。

script首先执行

第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})
timeout1进入对应队列

第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})
进入setImmediate队列

第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。

process.nextTick(function() {
    console.log('glob1_nextTick');
})
nextTick

第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。

new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

先是函数调用栈的变化 然后glob1_then任务进入队列

第六步:执行遇到第二个setTimeout。

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})
timeout2进入对应队列

第七步:先后遇到nextTick与Promise

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})
glob2_nextTick与Promise任务分别进入各自的队列

第八步:再次遇到setImmediate。

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
nextTick

这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。

其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。

当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。

这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。

第二轮循环初始状态

setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。

只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。

当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。

大家需要注意这里的循环结束的时间节点。

当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。

OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。

当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。

// 用数组模拟一个队列
var tasks = [];

// 模拟一个事件分发器
var addFn1 = function(task) {
    tasks.push(task);
}

// 执行所有的任务
var flush = function() {
    tasks.map(function(task) {
        task();
    })
}

// 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中
setTimeout(function() {
    flush();
})

// 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法

var dispatch = function(name) {
    tasks.map(function(item) {
        if(item.name == name) {
            item.handler();
        }
    })
}

// 当然,我们把任务丢进去的时候,多保存一个name即可。
// 这时候,task的格式就如下
demoTask =  {
    name: 'demo',
    handler: function() {}
}

// 于是,一个订阅-通知的设计模式就这样轻松的被实现了

这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。

需要注意的是,这里的执行顺序,或者执行的优先级在不同的场景里由于实现的不同会导致不同的结果,包括node的不同版本,不同浏览器等都有不同的结果。

前端基础进阶系列目录

[图片上传失败...(image-26761c-1529051594378)]

相关文章

网友评论

  • da5fe98a930d:想问您找到那个前端的核心线索了吗
  • ce1ce8153ae7:浏览器环境和node环境不一样
  • a2d7b36d630e:这个可以作为面试题。
  • a2d7b36d630e:不错, 运行一些代码就很清楚了。
  • 伊优yiyou:同一个浏览器,刚打开和刷新几次之后得到的结果都是不一样的(>_<)
  • 聚宝大当家:那如果是点击事件呢?比如我点击了一个绑定了某个事件的按钮

    如果是ajax呢?它的回调函数又是怎么样的啊?希望大神能再讲讲
  • f5313e228b00:我想请问一下👇代码的输出为什么是1,3
    console.log(1);
    new Promise(function(resolve,reject){
    window.setTimeout(function(){
    resolve(false);
    },0);
    }).then(function(){
    console.log(2);
    },function(){
    console.log(3);
    })
    不读无女子:结果是1,2 调用了resolve 那么就会执行then方法的第一个参数方法,那么输出的就是2
  • linbin:情况一
    setImmediate(function() {
    console.log('immediate1'); //2
    })

    setTimeout(function() {
    console.log('timeout1'); //1
    })

    输出结果:
    timeout1
    immediate1
    结论:是否可以认为setTimeout比setImmediate优先级高?node 8.x


    情况二
    process.nextTick(function() {
    console.log('glob1_nextTick'); //2
    })
    new Promise(function(resolve) {
    console.log('glob1_promise'); //1
    resolve();
    }).then(function() {
    console.log('glob1_then'); //3
    })
    输出结果:
    glob1_promise
    glob1_nextTick
    glob1_then
    结论:是否可以认为process.nextTick比then优先级高?node 8.x

    @这波能反杀
  • fdf10bc7d4c8:不错不错,收藏了。

    推荐下,源码圈 300 胖友的书单整理:http://t.cn/R0Uflld


    忿
  • Devinnn:重新理了下思路,觉得作者还是要把编译环境写出来,不然很容易导致误解。
    1. 浏览器标准环境中(比如说谷歌webkit内核),是一个宏任务紧接着所有微任务执行。
    2. 在node环境中,则又不一样了,是一个类型宏任务队列执行完,再去执行微任务。

    自己运行一下代码,就可以发现了。
    不知道这样理解是否正确,还有想问一下作者,为什么执行完setImmediate的所有微任务,才算是第二轮结束,这里觉得比较匪夷所思。
  • 4f5197dbbc1f:师者传道授业解惑也,真的解惑啊,终于懂了,其实并不是事件循环有多难理解,是因为错误的文章或不全面的文章太多了,大多数的人都被误导了。感谢作者
    这波能反杀:@宋连盟 加油
  • 艾特老干部:作者怎么看事件循环和JS主线程的关系呢?从文章的描述来看,事件循环不停从队列中取出消息执行,感觉事件循环是在JS主线程上执行的。但实际上,事件循环又不是JS Engine实现的功能,比如node.js中是libuv实现的事件循环。那么这个事件循环和JS Engine是怎样的工作机制,才让事件循环运行在JS主线程上的呢?
  • 诺顿遗迹:怎么理解:事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。想知道script(整体代码)是怎么作为macro-task入栈的?比如说,setTimeout和ajax在浏览器中都有对应的模块去处理,script(整体代码)先编译成指令存在指令区,然后开始执行,这个过程是怎么变成task任务,进而添加到队列里的,为啥不是直接入栈
  • 0201dde59723:感觉这十几章加起来可以写本书了,可以和u don't know js 对抗一下,默默问一句波哥,这个绘图软件是什么? :joy:
  • 897274367f52:大概看懂了。
  • 轨迹枫:已经懵逼
    轨迹枫:@这波能反杀 前面内存空间,原型链那些写得非常全面,容易理解,到了封装对象那里,看得有点枯燥了,到了事件循环机制,更加懵逼。好好学~
    这波能反杀:@struggle_b70e :joy:
  • jia58960://执行第一轮宏任务队列(macro)
    golb1
    glob1_promise
    glob2_promise
    //执行第一轮微任务队列(micro)
    glob1_nextTick
    glob2_nextTick
    glob1_then
    glob2_then

    第一轮事件循环结束

    //执行第二轮宏任务中的setTimeout队列(macro)
    timeout1
    timeout1_promise
    timeout2
    timeout2_promise

    //执行第二轮宏任务setTimeout产生的微任务队列(micro)
    timeout1_nextTick
    timeout2_nextTick
    timeout1_then
    timeout2_then

    第二轮事件循环结束

    //执行第二轮宏任务中setImmediate队列(macro)
    immediate1
    immediate1_promise
    immediate2
    immediate2_promise

    //执行第二轮宏任务setImmediate产生的微任务队列(micro)
    immediate1_nextTick
    immediate2_nextTick
    immediate1_then
    immediate2_then
    不净莲华:@jia58960 和作者说的执行顺序有明显出入,作者说只有两轮循环
  • 09c960c73388:楼主,我想问一下你上面第二个例子中是不是涉及到了三轮事件循环,执行全局代码块算一轮,setTimeout队列算第二轮,然后setImmediate队列是第三轮??
  • 诺顿遗迹: setImmediate(function() {
    console.log('setImmediate1');
    })
    setTimeout(function() {
    console.log('timeout1');
    })
    setImmediate(function() {
    console.log('setImmediate2');
    })
    setTimeout(function() {
    console.log('timeout2');
    })

    在chrome 60.0.3112.90版本下,连续执行20次,结果都是setImmediate1、setImmediate2、timeout1、timeout2。说明在不同执行环境,对于两者优先级的策略是不同的,不能死记硬背。
  • 诺顿遗迹:这里的micro和macro是任务队列的类型吗?还是本身就是任务队列?

    诺顿遗迹:还有一种说法是,每次循环的时候,都会大概率取一个优先级高的macro队列中的一个任务,执行完后执行micro队列。然后在各个macro队列中再选择其中一个队列的其中一个任务,执行后再执行现在的micro队列。不知道以上两种说法哪个是正确的,还是都不正确:joy:
    诺顿遗迹:所以会有多个macro类型的队列,但是只有一个micro类型的队列对么?macro类型的队列又存在优先级,所以每次循环的时候,都会大概率取一个优先级高的macro队列,执行完整个队列再去执行此时的micro队列。
    以上,求大神们解答!
  • fb1fdf20a5c2:波老师,我对setTimeout在有延迟时间的情况下,怎么进行时间循环的有疑惑,我看了MDN,原文是:调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息,虽然和你讲的也不冲突,但是又是怎么实现过一段时间后再队列添加消息的呢,过一段时间再添加这本身就是一个异步操作,所以我无法理解
    这波能反杀:@日进 嗯
    fb1fdf20a5c2:@波同学 我刚刚想到了一个解释。浏览器有三个常驻的核心线程,GUI线程、js引擎线程、浏览器事件触发线程,而setTimeout属于DOM API,和其他的浏览器事件一样,定时器应该也是由浏览器事件触发线程来分发任务到任务队列的,具体流程是:函数调用栈 -->script-->setTimeout-->发给浏览器事件触发线程-->分发任务到js事件循环任务队列。
    这应该可以算是一个合理的解释
    这波能反杀:@日进 建议不用太深究内部的实现细节,因为我也不知道怎么实现的,MDN的编辑也不一定知道是如何实现的。

    例如我们可以直接让setTimeout的事件进入队列,每次循环到该事件时加一个时间判断,如果符合时间了,就执行,不符合就让它过,然后到下一轮循环。

    但是这种方式明显不是最优的解法。我们还可以先将setTiemout的事件丢在事件队列中,先让它处于挂起状态,然后通过事件触发的方式,满足了时间条件再丢入setTimeout的队列中。

    总之能够实现的方式很多,具体浏览器的js引擎采用的那种方式我不知道,有可能不同的浏览器采取的方式都不一样。
  • 2752de3707b4:继续边看你的文章 边写笔记~ 哈哈 再次感谢. 不仅写的棒,例子也很赞
  • 大月山:这个函数调用栈是不是就是主线程?
    这波能反杀:@复活的猫 没必要类比线程哈,js里都是单线程
  • 3b8359d2c535:https://danmartensen.svbtle.com/events-concurrency-and-javascript
    https://vimeo.com/96425312
    讲解了整个浏览器的事件的流程,大家可以看一下.就能清楚很多比如事件监听,异步这些了
  • 3f0d5484d17d:应该是一个宏任务执行完毕后再执行所有的微任务,而不是一个宏任务队列执行完毕再执行所有的微任务。不然确实很矛盾啊。
    诺顿遗迹:我测试了一下,有两个并列顺序执行的setTimeout,在里边分别各写一个promise,结果是先执行第一个setTimeout的回调,紧接着执行了回调中的promise,然后执行了第二个setTimeout的回调,再执行了第二个回调中的promise。这是不是说明macro每次循环只执行一个任务?
    诺顿遗迹:我现在也有些疑惑,不知道执行的是一个宏任务队列,还是一个宏任务队列中的一个宏任务。
    诺顿遗迹:哪有矛盾呀?
  • 88db3f59d838:有个困惑,当代码只有setTimeout和setImmediate的时候,它们对应的任务执行的顺序并不是前者优先,而是不确定的,而当代码中同时包含二者以及process.nextTick,则setTimeout 的任务执行又优先于set Immediate
  • df1b85e0c408:写的很赞,不过事件循环应该是浏览器的实现,所以应该不能说一个JS线程有一个事件循环吧?
  • 8127e8b95614:赞,给前辈打赏了。那事件监听以及它的回调处于什么队列呢,当事件发生时会对本人中的执行顺序产生什么样的影响啊
    3b8359d2c535:https://danmartensen.svbtle.com/events-concurrency-and-javascript
    里面有详细的讲解整个浏览器关于事件循环和事件监听的
  • 一缕殇流化隐半边冰霜:这套题是我们团队博客里面的一篇文章,但是明显没有大神分析的详细,厉害:+1: :+1:
  • 71032c0decad:波同学你好,我想问下,在一个微任务里,又注册了一个微任务和一个宏任务,测试了下,微任务会在当前微任务列表的尾执行,宏任务会在之前注册的宏任务列表尾执行,对于这种在宏任务队列或者微任务队列里面继续注册的宏任务和微任务,怎么理解他们的顺序呢?
    诺顿遗迹:如果注册的是micro,在当前循环继续执行,如果是macro,则需要在下次循环执行了。也可能不是下次,看他所在的队列的优先级吧,最早是下次。
  • zyg:node 和 chrome 的事件循环顺序是不一样的吗?
    文中:
    事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

    注: macro-task 执行一个任务完毕 然后执行完micro-task

    但是在demo2 解释中 写到 :
    只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

    这样有矛盾。

    demo2 在chrome执行(去除了setImmediate 和 process.nextTick)
    code:
    console.log('golb1');
    setTimeout(function() {
    console.log('timeout1');
    new Promise(function(resolve) {
    console.log('timeout1_promise');
    resolve();
    }).then(function() {
    console.log('timeout1_then')
    })
    })

    new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
    }).then(function() {
    console.log('glob1_then')
    })
    setTimeout(function() {
    console.log('timeout2');
    new Promise(function(resolve) {
    console.log('timeout2_promise');
    resolve();
    }).then(function() {
    console.log('timeout2_then')
    })
    })
    new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
    }).then(function() {
    console.log('glob2_then')
    })
    结果:
    golb1
    glob1_promise
    glob2_promise
    glob1_then
    glob2_then
    imeout1
    imeout1_promise
    imeout1_then
    timeout2
    timeout2_promise
    timeout2_then

    node(v7.7.1 原demo2 没修改) 执行情况
    golb1
    glob1_promise
    glob2_promise
    glob1_nextTick
    glob2_nextTick
    glob1_then
    glob2_then
    timeout1
    timeout1_promise
    timeout2
    timeout2_promise
    timeout1_nextTick
    timeout2_nextTick
    timeout1_then
    timeout2_then
    immediate1
    immediate1_promise
    immediate2
    immediate2_promise
    immediate1_nextTick
    immediate2_nextTick
    immediate1_then
    immediate2_then

    请问这怎么解释?
    zyg:@Devinnn 还没有 估计要看源码才知道
    Devinnn:我也是觉得有矛盾的地方,觉得很奇怪,而且循环结束的那个结点也让人匪夷所思,请问你找到正确的答案了吗?
  • c30ea925ea91:Owesome,需要好好消化一下,话说这图是用什么工具做的?
    c30ea925ea91:@波同学 Soga,这篇真是难得技术好文:wink:
    这波能反杀:@riverxs ProcessOn
  • f775952591da:function getY (x) {
    return new Promise(function (resolve, reject) {
    setTimeout(function () {
    resolve((3 * x) - 1);
    }, 0);
    });
    }

    function foo (bar, baz) {
    var x = bar * baz;

    return getY(x)
    .then(function (y) {
    return [ x, y ];
    });
    }
    setTimeout(function () {
    console.log('timeout')
    }, 0)

    foo(10, 20).then(function (msgs) {
    var x = msgs[ 0 ];
    var y = msgs[ 1 ];

    console.log(x, y);
    });

    希望博主可以对这个案例分析一下
  • 与我常在Jerry:质量还是如此之高,很中意。:smile::smiley:
  • a3106a4eac54:排排坐
  • greennn:赞,不过有个疑问……求解

    setImmediate队列在setTimeout队列之后执行是因为setTimeout第一次出现的顺序就在setImmediate之前是吗?

    不知道这里有没有文章,如果代码顺序是
    ......
    setImmediate( function( ) {
    console.log("immediate1");
    ......
    }
    ......
    setTimeout( function( ) {
    console.log("timeout1");
    ......
    }
    ......

    的话,是不是setImmediate队列就会先执行?
    2d8490804cf2:你自己多试试就知道了,在node下这两个函数执行顺序是不固定的,不用纠结哪个先执行
  • 哇哈呀:索然无味,这种纯理论帮助不了任何人。如果真有本事,不要以为看了几篇博客,看了几本书就能写文章,把项目拿出来,一行一行的讲,否则谁能看懂你在说啥?难道就我理解能力差?反正我是一点不看不懂,前面的晦涩难懂!本人中专毕业,正在自学前端,学了1年多了。听说很好挣钱。html很难!是最难的。怪我英文太差!最近看到了css,我感觉javascript不太重要。我语文很不错,如果中文能编程,我早成神了。
    AdreamZ: @哇哈呀 可以,笑出声
    这波能反杀:@哇哈呀 大神你可真逗
  • 93608629b714:能不能在附录中写下参考文献等让我们学习下?
    这波能反杀:@Yestin_233d 你根据几个关键词去google找就行了
  • bc61c7d9f8fa:波同学,不管是微任务队列还是宏任务队列都是遵循FIFO,那队列中的任务被读取到执行栈时也应该是按照这个顺序来执行,那该如何理解队列中任务的优先级?比如dom事件操作,ajax请求的回调、以及setTimeout他们之间是否有优先级?
    console.log('start');
    var xhr = new XMLHttpRequest();
    var url = 'test.json';
    xhr.open('GET', url,true);
    xhr.onreadystatechange = function() {
    if (xhr.status == 200 && xhr.readyState == 4) {
    console.log(xhr.responseText)
    }
    }
    xhr.send();
    setTimeout(function() {
    console.log('timeout')
    }, 0)
    for (var i = 0; i < 3000000000; i++) {}

    var btn = document.querySelector("#btn");
    btn.onclick = function() {
    console.log('click')
    }

    new Promise((reslove, reject) => {
    console.log('promise');
    reslove();
    }).then((res) => {
    console.log('promise finish')
    })
    console.log('end');

    start
    promise
    end
    promise finish
    click
    {
    "test":"aa"
    }
    timeout

    这里和我预期的有点不一样,我以为应该是ajax的回调先被执行到的,以为我故意停留了2-3秒来点击那个按钮,按理说ajax率先返回了应该被先加入到队列中,结果是click先先输出。。。这里是不是有优先级的存在?
    a105fa17ba67:@年少轻狂VS沉稳老练 同步优于异步优于回调,不知道说的对不
    这波能反杀:@年少轻狂VS沉稳老练 这种需要等待的结果的,执行完毕的时间肯定是不确定的
  • 646d09b1e4e6:终于碰到一脸懵的了
  • 9ab719968975:好文,谢谢分享。

    golb1
    glob1_promise
    glob2_promise
    glob1_nextTick
    glob2_nextTick
    glob1_then
    glob2_then
    timeout1
    timeout1_promise
    timeout2
    timeout2_promise
    timeout1_nextTick
    timeout2_nextTick
    timeout1_then
    timeout2_then
    immediate1
    immediate1_promise
    immediate2
    immediate2_promise
    immediate1_nextTick
    immediate2_nextTick
    immediate1_then
    immediate2_then
    49905cddb281:在node环境下已验证 ,@Ludis的返回结果跟node下的结果一致。
    时间de歌:@沐晴的前端世界 是这样的呀,哪里不对?
    全凭一口仙气儿活着: @Ludis 写的不对
  • 1a7691cfd42f:文章不错,学习了,感谢分享。
  • 老人贤:
    console.log('golb1');

    setTimeout(function() {
    console.log('timeout1');
    new Promise(function(resolve) {
    console.log('timeout1_promise');
    resolve();
    }).then(function() {
    console.log('timeout1_then')
    })
    })


    new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
    }).then(function() {
    console.log('glob1_then')
    })

    setTimeout(function() {
    console.log('timeout2');
    new Promise(function(resolve) {
    console.log('timeout2_promise');
    resolve();
    }).then(function() {
    console.log('timeout2_then')
    })
    })

    new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
    }).then(function() {
    console.log('glob2_then')
    })

    对于这段代码,浏览器和node 的执行结果不一致,
    浏览器的结果是:
    timeout1->timeout1_promise->timeout1_then->timeout2->timeout2_promise->timeout2_then
    而node结果是:
    timeout1->timeout1_promise->timeout2->timeout2_promise->timeout1_then->timeout2_then
    请问这是什么原因呢
    与我常在Jerry:@老人贤 看样子是浏览器每执行完一个宏任务(而不是所有宏任务都出栈之后),然后执行微任务
    这波能反杀:应该是node里的v8和浏览器里的v8在实现上有一些小差别,我后边再看看吧
  • 064f00f5e807:好文章,就是错别字有点多,不过还是要感谢作者
  • 淘淘笙悦:波同学,你好,我想请问一下,一执行到 setTimeout 这里的时候把函数放进队列中,那么是从此时就开始算函数的那个出发时间了么?
    例如下面这个例子
    var start = new Date();
    setTimeout(function() {
    console.log(new Date() - start);
    }, 500);
    while (new Date() - start <= 1000) {}
    结果输出是1000大一点,所以我认为是setTimeout这类定时器在执行到的时候就已经开始计算第二个参数的那个时间了。
    表达不好,见谅。
    这波能反杀:@淘淘笙悦 你可能还没完全看明白
    淘淘笙悦:@波同学 这样的话,按照理解,如上代码应该应该输出1500多一点,但是我不太明白为什么输出的是1000多一点。
    这波能反杀:@淘淘笙悦 不是
  • 01823ad369ec:波哥,又是好文,之前只知道任务执行有队列,么鸡这么多任务类型划分和队列划分,收获很到很大。
    我测试了宏任务队列的执行顺序:script(整体代码)->setTimeout(setInterval同源)->setImmediate
    微任务队列的执行顺序:process.nextTick->Promise(then)
    还有一点就是我关于循环结束的时间点的理解:宏任务中最后一个任务(该任务所处的队列可能不是最后一个任务队列)执行完,再执行完微任务中的所有可执行的微任务,循环结束。
    问题:
    1 .宏任务和微任务中的其他的几个任务队列中的任务的执行不知道该如何测试 = =
    2. 对于循环结束时间点的理解是否恰当
    望波哥指点一哈,感谢。
    f775952591da:浏览器运行机制和这个一致么? 因为浏览器是没有 setImmediate 和 process.nextTick的,那么除了他俩之外在浏览器环境是否也是按照这个机制运行呢?
    01823ad369ec:@波同学 嗯嗯,好的,谢谢波哥,我再结合异步编程来加深一下理解。
    这波能反杀:@JJ_Peng 理解是对的,其他的就是事件绑定之类的,在浏览器环境不用去测了吧,就很简单
  • 虾哔哔:有点消化不了~~
  • 128f587d6ed6:这下迷糊了,不了解Promise,先占个沙发,回头慢慢读!
    这波能反杀:不懂Promise,永远成为不了高手
  • c5e2a94f1b07:我发现一个挺奇怪的问题,我用的 OS X Chrome Version 56.0.2924.87 (64-bit), 在当前页面简书的控制台输入 setImmediate 会显示 function setImmediate() { [native code] },但是访问例如 google 的页面,却提示 setImmediate is not defined??
    Crowphy:这个方法浏览器没有,只是node实现了
    这波能反杀:最好在node环境运行这个例子,不要在浏览器环境
  • ChikaraChan:好文章
  • 旭霸:文章很赞!
  • 71f241c96a34:看不懂图……箭头的意思不是很懂……
    这波能反杀:简单表示队列的方向
  • 9f88cb1f6ba0:这篇文章,完全解惑了,以前看的文章看来都是错的
  • 04d5a7e549e0:每次看到你更新了都会兴奋
  • 朵朵鱼:一如既往的厉害👍😎
    这波能反杀:@朵朵鱼 :blush:
  • 3f01f2cb549d:波老师想请教一下,setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
    console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
    console.log('timeout2_promise');
    resolve();
    }).then(function() {
    console.log('timeout2_then')
    })
    })
    我如果把promise放到nextTick前面,也是会先执行nextTick,请问是不是微任务里面的任务类型有先后呢?
    这波能反杀:@脾气永远不要大于本事zZ 是的,机智
    f972d334bfb6:@脾气永远不要大于本事zZ 从demo02的运行结果来看,不止micro task, macro task的运行结果也是有权重的。权重即为文中所述的顺序
    3f01f2cb549d:我刚试了下把setImmediate放到settimeout前面也是结果不变,然后又看了一下您的文章,macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

    micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
    您在这列举的顺序是不是就是他们的权重顺序了?
  • 别过经年:搬上小板凳
  • 饥人谷_xxxxx:谢谢波老师!
  • ce0a0b27f5c6:沙发
    这波能反杀:厉害,这么快

本文标题:前端基础进阶(十二):深入核心,详解事件循环机制

本文链接:https://www.haomeiwen.com/subject/nnzinttx.html