在我们讲解事件循环机制之前,我们先来了解了解JavaScript这门语言。
众所周知,JavaScript是一门单线程的语言,什么是单线程?可以简单理解为只能一件事一件事的干,只有一个执行流。那为什么不将JavaScript设置为多线程的呢,这效率不是巨高?
- 因为JS可以修改dom结构,如果在JS执行的时候,UI线程还在工作,就可能导致不安全的渲染UI,得益于JS是单线程运行的,可以达到节省运行内存,节约上下文切换的时间(变快)。 所以为了避免复杂性,一开始JavaScript就设置成了单线程的。
同步任务和异步任务
我们都知道JavaScript中的事件分为同步任务和异步任务。对于同步而言,是从上到下依次执行的,程序的执行顺序和任务的排列顺序是一致的。
这就是所谓的程序的执行顺序和任务的排列顺序是一致的。
而异步呢?简单举个例子:比如说你在10点必须去街上买菜,但是现在才九点半。你会一直在那等着啥事不干,干等到10点?那必不可能,你会先着手去做其他事,等到了10点再去街上买菜。这个时候程序的执行顺序和任务的排列顺序不一致了。这便可理解为异步。
setTimeOut排在console.log(2)前面,但是他是延迟两秒执行(现在我们可以这么理解),执行时当读取到setTimeOut,不会等它执行然后再往下执行。如果等他执行完再去执行下面的代码,把延迟时间调到巨大,那完犊子了,想执行下面的代码得等到猴年马月。而是将它放到备忘录里去(这里指的是WebAPIs中的timer模块),先去执行下面的console.log(2),而console.log只是一个普通的方法,所以会立即执行输出 2 ,到这里,能立即执行的代码全执行完了,这时setTimeOut的延时时间也到了,又将它里面的东西放到代办事件里去(这里指的是任务队列task queue)。然后执行者会去代办事件里看看还有谁没执行,发现还有东西未执行,这时会去执行里面的console.log(1),输出 1 。
这里上个流程图瞅瞅,更直观些
到这我们简单了解了一下同步任务和异步任务的区别所在。
到这我们就能知道,JavaScript代码的执行过程中除了正常从上到下执行(函数调用栈)外,还会用到另一个东西(任务队列)去执行另一些代码,这整个执行过程可以简单的称为事件循环过程。
上面讲述执行过程时,谈到了一个名为任务队列的东西。
- 我们都知道JavaScript是单线程的,拥有唯一的一个事件循环,但是呢,任务队列可以有好多个;
- 任务队列分为宏任务(macro-task)和微任务(micro-task);
- 宏任务包括:script(整体代码), setTimeout, setInterval, I/O, UI rendering;
- 微任务包括: Promise, Promise .then, Object.observe(已废弃), MutationObserver(html5新特性);
- 这里我们要注意一下setTimeOut和setInterval,进入任务队列的是他们里面的具体任务,而不是他们本身。他们被称为任务源
因为script也属于宏任务,而script表示一个整体的代码,这说明事件循环是从宏任务开始的
现在我们来说说这整个的执行流程:
简单来讲就是先执行宏任务,再去执行宏任务里面的微任务,当执行完这个宏任务里面的所有代码后,再去执行另一个宏任务,依次反复。
这里我们先来拿上面那个例子讲讲:
`
setTimeOut(() => {
consloe.log(1);
})
console.log(2);
`
首先从上往下读代码:
- 首先script(整体代码)开始执行,全局上下文进入到调用栈中
- setTimeOut是一个宏任务,他的回调函数进入任务队列(setTimeOut是一个任务源)
- console.log(2)是同步任务,则直接在主线程中执行输出2
4. 这时主线程中的同步任务执行完毕
5. 去读取任务队列
6. 执行setTimeOut的的回调函数,输出 1
7. 执行完毕
上面只是前菜,让大家心里对事件循环机制有个底。
这里我们再来上个难点的例子:
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
},0)
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end');
我们来理理上面代码的执行顺序:
1:首先script(整体代码)进入宏任务,全局上下文进入到调用栈中
2:script任务从上往下执行时,先遇到console.log('script start'),入栈执行
3: 然后继续向下执行,遇到setTimeout,他是一个宏任务源,将他里面的回调函数分配到宏任务队列中(这里我们为了便于直观的查看结果,只移动语句的输出结果)
4:继续往下执行遇到了Promise实例,这里我们要注意,这里是表示Promise构造函数,Promise构造函数是同步执行的,而.then是异步执行的(微任务),所以console.log('promise1')进入调用栈中执行并直接输出,而.then进入到微任务队列
这里我们可以参考一下这两篇
使用 Promise(时序部分)
5:继续往下执行,读取到console.log('script end');没得想直接输出
6:最后,栈中的代码就执行完了,微任务中只有一个.then了,直接执行
7:所有的微任务就执行完了,第一轮循环就结束了,现在开启第二轮循环,从宏任务开始,宏任务中还有一个setTimeOut未执行,所以直接执行即可。
8:这是宏任务和微任务队列中没有了任务,所以全局上下文出栈,程序运行结束
到这这个例子就差不多解释完了,如果上面文字加图片还是不太能理解的话,来看看流程图吧!!
结语
到这,我们的浏览器中的事件循环机制就讲解完了,这是我对于这个机制的理解。
最后再贴上一些例题吧,供大家思考 例一:
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
Promise.resolve().then(function(){
console.log('promise')
}).then(function(){
console.log('promise2')
})
console.log('script end')
例二:
console.log(1);
setTimeout(()=>{
console.log(2);
Promise.resolve().then(()=>{
console.log(3);
})
});
new Promise((resolve,reject)=>{
console.log(4);
resolve(5);
}).then((data)=>{
console.log(data)
})
setTimeout(()=>{
console.log(6)
})
console.log(7)