We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
1. 【前端体系】从一道面试题谈谈对EventLoop的理解 2. JavaScript 运行机制详解:再谈Event Loop 3. 详解JavaScript中的Event Loop(事件循环)机制 4. 再话js的事件循环机制 5. JavaScript中的Event Loop(事件循环)机制
JavaScript的单线程与它的用途紧密相关。作为浏览器的脚本语言,JavaScript 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。如果说JavaScript同时又2个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这个时候浏览器该如何处理呢? 所以,为了避免复杂性,从一开始,JavaScript就是单线程,这已经成为了这门语言的核心特点,未来也不会改变。 为了利用多核CPU的计算能力,HTML5提出了WEB Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,并且不允许操作DOM。所以这个标准并没有改变JavaScript单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是很多时候CPU是处于空闲状态的,因为IO设备很慢,比如说Ajax获取数据,这个时候就不得不等到结果出来之后,再往下继续执行。 这个时候JavaScript的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到设备返回了结果,再回过头,把挂起的任务继续执行下去。 因此,所有的任务分成了2种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务是指,不进入主线程、而进入“任务队列”(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 具体的运行机制如下:
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。如下图所示: 上图,在主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码会调用各种外部的API,它们在“任务队列”中加入各种事件(click, done, load)。一旦栈中的代码执行完成,主线程就会去读取“任务队列”,一次执行其中的事件对应的回调函数。执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。
总结起来:Event Loop会循环不断的去拿宏任务队列中最老的一个任务,推入队列中执行,并在当次循环里一次执行并清空microtask队列里的任务,执行完microtask队列里的任务之后,更新渲染。也就是:
这两种任务都是在任务队列的异步事件中注册的回调函数。那为什么要引入宏任务与微任务,只有一种类型的任务可不可以呢?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。不同的异步任务被分为:宏任务和微任务。
宏任务:
微任务:
异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务与微任务队列中去。在当前执行栈为空时,主线程会查看微任务队列是否有事件存在:
为了方便理解,我们可以认为在任务队列里面有宏任务和微任务;宏任务有多个,微任务只有一个。执行顺序应该是:
所以总体是:宏任务优先,在宏任务执行完毕之后才会来一次性清空任务队列中的所有微任务。
// 代码一执行就开始执行了一个宏任务-宏0 console.log('script start'); setTimeout(() => { // 宏1 console.log('setTimeout'); }, 1* 2000) Promise.resolve().then(function() { console.log(promise1); // 微1-1 }) .then(function() { console.log(promise2); // 微1-4,这个then中的会等待上一个then执行完毕之后得到其他状态才会Queue注册状态对应的回调,假设上一个then中主动抛出错误且没有被捕获,那就注册的是这个then中的第二个回调了。 }) async function foo() { await bar(); // await是promise的语法糖,会异步等待获取其返回值 // 后面的代码可以理解为放到异步队列微任务中。 console.log('async1 end'); } foo(); function bar() { console.log('async2 end'); } async function errorFunc() { try { await Promise.reject('error!'); }.catch(e) { // 从这后面开始,所有的代码可以理解为放到微任务队列中去 console.log(e); // 微1-3 } console.log('async1'); return Promise.resolve('async1 success'); } errorFunc().then(res => console.log(res)); // 微1-5 console.log('script end');
PS:注意一点就是Promise.then().then(),在注册异步任务的时候,第二个then中的回调是依赖第一个then中回调的结果的,如果执行没有异常才会在该异步任务执行完成之后注册状态对应的回调。
全局一个宏任务执行,输出同步代码。挂载宏1,微1-1, 微1-2,微1-3, 微1-4。1-表示属于第一次轮询。输出的结果是:
script start->async2 end->script end
同步代码执行完毕,开始执行异步任务中的微任务队列中的代码。 微任务队列:只有一个队列且会在当前轮询一次性清空。
执行微1-1:promise1 执行微1-2:async1 end 执行微1-3:error!, async1。当前异步回调执行完毕才Promise.resolve('async1 success'),然后注册.then()中的成功的回调:微1-5 执行微1-4:promise2 执行刚注册的微1-5:async1 success
到这里,第一波轮询结束。
开启第二波轮询:执行宏1
run: setTimeout
到此为止,整个轮询结束。其实相对难以理解的也就是微任务,对于微任务也就是上面说的只有一个队列会在此次轮询中一次清空(包括此次执行过程中所产生的微任务)。 整个打印的顺序为:script start -> async2 end ->script end -> promise1 -> async1 end -> error! -> async1 -> promise2 -> async1 success -> setTimeout
通过上面的分析,再来看一些类似的题目。
console.log(1) setTimeout(function() { //settimeout1 console.log(2) }, 0); const intervalId = setInterval(function() { //setinterval1 console.log(3) }, 0) setTimeout(function() { //settimeout2 console.log(10) new Promise(function(resolve) { //promise1 console.log(11) resolve() }) .then(function() { console.log(12) }) .then(function() { console.log(13) clearInterval(intervalId) }) }, 0); //promise2 Promise.resolve() .then(function() { console.log(7) }) .then(function() { console.log(8) }) console.log(9)
打印顺序是:1 -> 9 -> 7 -> 8 -> 2 -> 3 ->10 -> 11 -> 12 -> 13 解析:
注意:由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。
为什么在第一次Event Loop时,不先执行macrotask,因为按照流程的话,不应该是先检查macrotask队列是否为空,再检查microtask队列吗? 因为一开始Js主线程中跑的任务就是macrotask任务,而根据事件循环的流程,一次事件循环只会执行一个macrotask任务,所以执行完主线程的代码之后,它就去从microtask队列里面取队首的任务来执行。
定时器是否是真实可靠的呢?比如我执行一个命令:setTimeout(task, 1000),他是否就能准确的在1000毫秒后执行呢? 实根据以上的讨论,我们就可以得知,这是不可能的。 因为你执行setTimeout(task,100)后,其实只是确保这个任务,会在100毫秒后进入macrotask队列,但并不意味着他能立刻运行,可能当前主线程正在进行一个耗时的操作,也可能目前microtask队列有很多个任务,所以这也可能是大家一直诟病setTimeout的原因吧。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
参考文章
1. 【前端体系】从一道面试题谈谈对EventLoop的理解
2. JavaScript 运行机制详解:再谈Event Loop
3. 详解JavaScript中的Event Loop(事件循环)机制
4. 再话js的事件循环机制
5. JavaScript中的Event Loop(事件循环)机制
前言:为什么 JavaScript 是单线程语言?(单线程+非阻塞)
JavaScript的单线程与它的用途紧密相关。作为浏览器的脚本语言,JavaScript 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。如果说JavaScript同时又2个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这个时候浏览器该如何处理呢?
所以,为了避免复杂性,从一开始,JavaScript就是单线程,这已经成为了这门语言的核心特点,未来也不会改变。
为了利用多核CPU的计算能力,HTML5提出了WEB Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,并且不允许操作DOM。所以这个标准并没有改变JavaScript单线程的本质。
任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是很多时候CPU是处于空闲状态的,因为IO设备很慢,比如说Ajax获取数据,这个时候就不得不等到结果出来之后,再往下继续执行。
这个时候JavaScript的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到设备返回了结果,再回过头,把挂起的任务继续执行下去。
因此,所有的任务分成了2种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务是指,不进入主线程、而进入“任务队列”(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体的运行机制如下:
只要主线程空了,就会去读取“任务队列”,这就是JavaScript的运行机制,整个过程都会不断重复。
事件与回调函数
Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。如下图所示:
上图,在主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码会调用各种外部的API,它们在“任务队列”中加入各种事件(click, done, load)。一旦栈中的代码执行完成,主线程就会去读取“任务队列”,一次执行其中的事件对应的回调函数。执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。
总结起来:Event Loop会循环不断的去拿宏任务队列中最老的一个任务,推入队列中执行,并在当次循环里一次执行并清空microtask队列里的任务,执行完microtask队列里的任务之后,更新渲染。也就是:
其中,在执行代码过程中新增的microtask任务会在当前事件循环周期内执行,而新增的macrotask任务只能等到下一个事件循环才能执行了。
宏任务与微任务
这两种任务都是在任务队列的异步事件中注册的回调函数。那为什么要引入宏任务与微任务,只有一种类型的任务可不可以呢?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。不同的异步任务被分为:宏任务和微任务。
宏任务:
微任务:
运行机制
异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务与微任务队列中去。在当前执行栈为空时,主线程会查看微任务队列是否有事件存在:
当前执行栈执行完毕后,会立刻处理所有的微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
在事件循环中,每进行一次循环操作称为tick,每一次tick的任务处理模型都是比较复杂的,但关键的步骤主要是:
宏任务和微任务优先问题
为了方便理解,我们可以认为在任务队列里面有宏任务和微任务;宏任务有多个,微任务只有一个。执行顺序应该是:
3.1 先查看任务队列中的微任务队列是否存在宏任务执行过程中所产生的的微任务:①如果有,就将微任务队列中的所有微任务清空;②微任务执行过程中所产生的微任务放到微任务队列中去,在此次执行中一并清空。
3.2 如果没有再看看宏任务队列中有没有宏任务,有的话执行,没有的话事件轮训第一波结束。执行过程中所产生的微任务放到微任务队列中,完成宏任务之后执行清空微任务队列的代码。
所以总体是:宏任务优先,在宏任务执行完毕之后才会来一次性清空任务队列中的所有微任务。
再来看一道经典的题目
PS:注意一点就是Promise.then().then(),在注册异步任务的时候,第二个then中的回调是依赖第一个then中回调的结果的,如果执行没有异常才会在该异步任务执行完成之后注册状态对应的回调。
第一次执行
全局一个宏任务执行,输出同步代码。挂载宏1,微1-1, 微1-2,微1-3, 微1-4。1-表示属于第一次轮询。输出的结果是:
第二次执行
同步代码执行完毕,开始执行异步任务中的微任务队列中的代码。
微任务队列:只有一个队列且会在当前轮询一次性清空。
到这里,第一波轮询结束。
第三次执行
开启第二波轮询:执行宏1
到此为止,整个轮询结束。其实相对难以理解的也就是微任务,对于微任务也就是上面说的只有一个队列会在此次轮询中一次清空(包括此次执行过程中所产生的微任务)。
整个打印的顺序为:script start -> async2 end ->script end -> promise1 -> async1 end -> error! -> async1 -> promise2 -> async1 success -> setTimeout
深入
通过上面的分析,再来看一些类似的题目。
打印顺序是:1 -> 9 -> 7 -> 8 -> 2 -> 3 ->10 -> 11 -> 12 -> 13
解析:
注意:由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。
问题思考
为什么在第一次Event Loop时,不先执行macrotask,因为按照流程的话,不应该是先检查macrotask队列是否为空,再检查microtask队列吗?
因为一开始Js主线程中跑的任务就是macrotask任务,而根据事件循环的流程,一次事件循环只会执行一个macrotask任务,所以执行完主线程的代码之后,它就去从microtask队列里面取队首的任务来执行。
定时器是否是真实可靠的呢?比如我执行一个命令:setTimeout(task, 1000),他是否就能准确的在1000毫秒后执行呢?
实根据以上的讨论,我们就可以得知,这是不可能的。
因为你执行setTimeout(task,100)后,其实只是确保这个任务,会在100毫秒后进入macrotask队列,但并不意味着他能立刻运行,可能当前主线程正在进行一个耗时的操作,也可能目前microtask队列有很多个任务,所以这也可能是大家一直诟病setTimeout的原因吧。
The text was updated successfully, but these errors were encountered: