本文为个人见解,如果发现文章有错误的地方,欢迎大家指正,感谢感谢~~
转载请标明出处
本文针对于Chrome浏览器环境下的事件循环机制,node环境下还没有进行试验,以后再试验下~~
前言
众所周知,JavaScript的一大特点就是单线程,也就是会按顺序执行代码,同一时间只能做一件事。
为什么JavaScript会被设计成单线程?
JavaScript的诞生,一开始是为了解决浏览器用户交互的问题,以及用来操作DOM,基于这个原因,JavaScript被设计成单线程,否则会带来复杂的同步问题。
为什么JavaScript需要异步?
单线程意味着所有任务都要排队进行,如果存在一个任务执行时间过长,后面的任务都会被阻塞,对于用户而言就意味着“卡死”。
单线程的JavaScript是怎么执行异步代码的呢?
这就涉及到JavaScript的事件循环机制(event loop)了。
事件循环机制(event loop)
这里先推荐去看看Philip Roberts的演讲《Help, I’m stuck in an event-loop》,虽然内容没有涉及到任务队列的细分,但是对函数调用栈(call stack)的分析还是挺不错的
列举几个概念:执行上下文, 函数调用栈(call stack), 任务队列(task queue)
- 执行上下文(以后有空应该会再写一篇文章分析一下哈哈):
- 全局环境:JavaScript代码运行起来会首先进入该环境
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码
- eval(不建议使用,可忽略)
- 函数调用栈(call stack)是决定了js代码的运行机制,遇到函数时,会生成一个新的函数上下文,并且入栈,执行完毕后出栈
- JavaScript中的任务分为macro-task(宏任务)与micro-task(微任务)
- macro-task包括:script(一段代码),setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering
- micro-task包括:process.nextTick, Promise, Object.observe, MutationObserver
-
当JavaScript代码开始执行时,首先将全局环境压入函数调用栈(栈底永远都是全局上下文,除非线程结束,在浏览器上表现为窗口关闭),之后,每遇到一个函数,创建一个新的函数上下文,并且入栈。
-
执行过程中,遇到了macro-task或者micro-task,都会将其交给对应的web api去处理,比如setTimeout交给timer模块,ajax请求交给network模块,DOM操作交给DOM对应模块处理,处理完成后,会将对应的回调函数放入对应的队列中(macro-task队列以及micro-task队列)
-
每当函数调用栈中的上下文都执行完毕时(全局环境仍然存在),主进程会去查询micro-task队列,如果micro-task队列为空,会取macro-task队列第一个task放入调用栈执行,否则,取micro-task队列的第一个task放入调用栈执行,如果在处理task期间,如果有新添加的microtasks或者macro-task,也会被添加到相应队列的末尾
-
一直循环第3步,直至所有任务执行完毕,这就是事件循环
按照我的思路大概画了个流程图
来实践一下,想象以下代码片段的控制台输出
console.log('start')
setTimeout(function setTimeout1() {
console.log('setTimeout1')
setTimeout(function setTimeout3() {
console.log('setTimeout3')
new Promise(function promise4(resolve, reject) {
console.log('promise4')
resolve('then')
}).then(function then4() {
console.log('promise4 then')
})
}, 0)
new Promise(function promise3(resolve, reject) {
console.log('promise3')
setTimeout(function setTimeout4() {
resolve('then')
}, 0)
console.log('after resolve')
}).then(function then3() {
console.log('promise3 then')
})
}, 0)
new Promise(function promise1(resolve, reject) {
console.log('promise1')
resolve('then')
}).then(function then1() {
console.log('promise1 then')
new Promise(function promise2(resolve, reject) {
console.log('promise2')
resolve('then')
}).then(function then2() {
console.log('promise2 then')
})
})
setTimeout(function setTimeout2() {
console.log('setTimeout2')
}, 0)
console.log('end')
/*
控制台输出
start
promise1
end
promise1 then
promise2
promise2 then
setTimeout1
promise3
after resolve
setTimeout2
setTimeout3
promise3 then
*/
细化步骤还挺多的,所以做了个gif~~
第一步
全局上下文global进栈
第二步
console.log('start')
遇到console.log,函数进栈,调用web api的console接口,运行完成后出栈
第三步
setTimeout(function setTimeout1() {
//....
}, 0)
遇到setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout1放入macro-task队尾。划重点!!这是一个很容易产生误解的地方,很多同学下意识都觉得定时器就是到了设定时间后立即执行,其实是到了时间后,将回调函数放入macro-task队列,等待执行
第四步
new Promise(function promise1(resolve, reject) {
console.log('promise1')
resolve('then')
}).then(function then1() {
//...
})
遇到promise,构造函数里的promise1会立刻进栈并且执行,执行中遇到了resolve函数,进栈,将回调函数then1放入micro-task队列,此时promise1和resolve都已执行完毕,出栈
第五步
setTimeout(function setTimeout2() {
//...
}, 0)
遇到setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout2放入macro-task队尾。
第六步
console.log('end')
第七步
到了很关键的一步,这个时候call stack已经执行完了(只剩下global),主进程会去查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then1)进栈执行
function then1() {
console.log('promise1 then')
new Promise(function promise2(resolve, reject) {
console.log('promise2')
resolve('then')
}).then(function then2() {
//...
})
}
在执行过程中,又遇到了promise,先执行构造函数里的promise2,执行中遇到了resolve函数,进栈,将回调函数then2放入micro-task队列,此时then1、promise2和resolve都已执行完毕,出栈
第八步
call stack执行完毕,查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then2)进栈执行
function then2() {
console.log('promise2 then')
}
第九步
call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout1)进栈执行
function setTimeout1() {
console.log('setTimeout1')
setTimeout(function setTimeout3() {
//...
}, 0)
new Promise(function promise3(resolve, reject) {
console.log('promise3')
setTimeout(function setTimeout4() {
//...
}, 0)
console.log('after resolve')
}).then(function then3() {
//...
})
}
执行中遇到setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout3放入macro-task队尾。
继续执行,遇到了promise,先执行构造函数里的promise3,又遇到了setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout4放入macro-task队尾,此时setTimeout1和promise3
都已执行完毕,出栈
第十步
call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout2)进栈执行
function setTimeout2() {
console.log('setTimeout2')
}
第十步
call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout3)进栈执行
function setTimeout3() {
console.log('setTimeout3')
new Promise(function promise4(resolve, reject) {
console.log('promise4')
resolve('then')
}).then(function then4() {
console.log('promise4 then')
})
}
执行中遇到了promise,先执行构造函数里的promise4,遇到了resolve函数,进栈,将回调函数then2放入micro-task队列,此时setTimeout3和promise4都已执行完毕,出栈
第十一步
call stack执行完毕,查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then4)进栈执行
function then4() {
console.log('promise4 then')
}
第十二步
call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout4)进栈执行
function setTimeout4() {
resolve('then')
}
执行遇到resolve,将promise的回调函数then3放入micro-task队列,此时setTimeout4和resolve已执行完毕,出栈
第十三步
call stack执行完毕,查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then3)进栈执行,执行完毕后出栈,至此全部代码执行完毕
呼~终于写完了
总结
- 主进程开始执行代码时,先将全局环境入栈,以后每遇到一个函数,创建一个新的上下文,进栈并且执行,遇到主进程执行不了的函数,交给web api执行,同时出栈
- 执行过程中,遇到了macro-task或者micro-task,都会将其交给对应的web api去处理,比如setTimeout交给timer模块,ajax请求交给network模块,DOM操作交给DOM对应模块处理,处理完成后,会将对应的回调函数放入对应的队列中(macro-task队列以及micro-task队列)
- 每当函数调用栈中的上下文都执行完毕时(全局环境仍然存在),主进程会去查询micro-task队列,如果micro-task队列为空,会取macro-task队列第一个task放入调用栈执行,否则,取micro-task队列的第一个task放入调用栈执行,如果在处理task期间,如果有新添加的microtasks或者macro-task,也会被添加到相应队列的末尾
- 以上内容只针对于Chrome浏览器环境,node环境还没有具体测试,好像是不太一样的
关于
前端萌新一个~~打算经常写写文章总结一下知识点,欢迎关注,一起加油啦