本文为我的看法,若是发现文章有错误的地方,欢迎你们指正,感谢感谢~~前端
转载请标明出处node
众所周知,JavaScript的一大特色就是单线程,也就是会按顺序执行代码,同一时间只能作一件事。web
JavaScript的诞生,一开始是为了解决浏览器用户交互的问题,以及用来操做DOM,基于这个缘由,JavaScript被设计成单线程,不然会带来复杂的同步问题。ajax
单线程意味着全部任务都要排队进行,若是存在一个任务执行时间过长,后面的任务都会被阻塞,对于用户而言就意味着“卡死”。vim
单线程的JavaScript是怎么执行异步代码的呢?api
这就涉及到JavaScript的事件循环机制(event loop)了。promise
这里先推荐去看看Philip Roberts的演讲《Help, I’m stuck in an event-loop》,虽然内容没有涉及到任务队列的细分,可是对函数调用栈(call stack)的分析仍是挺不错的浏览器
列举几个概念:执行上下文, 函数调用栈(call stack), 任务队列(task queue)bash
当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
)进栈执行,执行完毕后出栈,至此所有代码执行完毕
呼~终于写完了
前端萌新一个~~打算常常写写文章总结一下知识点,欢迎关注,一块儿加油啦