0.前言
又是新的一年啊!
1.简介
JavaScript 是脚本语言,JS也是单线程的,因此同一时间只能做一件事,也就是说它一次仅能处理一个任务。
思考题
- JavaScript为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢?
- JavaScript中的
event loop()
- 看一段代码,想下正确的输出顺序
console.log(1)
Promise.resolve().then(function () {
console.log(2)
})
new Promise(function(resolve, reject){
console.log(3)
resolve()
}).then(function () {
console.log(4)
setTimeout(function () {
console.log(5)
})
})
console.log(6)
setTimeout(function () {
Promise.resolve().then(function () {
console.log(7)
setTimeout(function () {
console.log(8)
})
})
})
2.概念
执行上下文(Execution Context)
执行上下文简单来说就是一个执行环境。它有全局环境、函数环境和eval
函数环境之分。它会在javascript引擎执行你的脚本的时候去创建。
执行栈(Execution Stack)
执行栈也就是常说的调用栈,它是一种拥有LIFO(后进先出)的数据结构。它会存储代码运行时创建的执行上下文
微任务(micro task)与宏任务(macro task)
javasript中的任务分为微任务和宏任务两种,这两种任务的执行时机是不同的,因此区分js中哪些是宏任务,哪些是微任务则十分重要。我们常见的宏任务有:script
任务、setTimeout
、ajax
等,常见的微任务比较典型的是:Promise.resolve().then()
、process.nextTick
、MutationObserver
等。
事件循环(event loop)
javasript是单线程的,一次仅能处理一个任务。但js所在的宿主环境,也就是我们所说的浏览器并不是单线程的(这里宿主环境仅讨论浏览器)。它在遇到一些任务时,比如说setTimeout
、event listener
等。它会告诉浏览器:老兄帮个忙,事成后通知我一声,小弟我先干别的事去了。浏览器会回应说:交给我吧,小老弟,事成后我放到任务队列,自己去取啊。于是,javasript开始执行script
任务,执行完了就开始检查有没有微任务啊,没有的话就从任务队列开始取宏任务执行,每执行完一次宏任务,就去看看有没有微任务,有的话就执行完成,再执行宏任务,如此往复。如下图:
而准确的划分方式是:
-
macro-task
(宏任务):包括整体代码script
,setTimeout
,setInterval
-
micro-task
(微任务):Promise
,process.nextTick
3.回答思考题
JavaScript为什么是单线程的?
因为现在如果有两个任务一个是删除DOM节点,一个是增加DOM节点,浏览器该如何执行?所以JavaScript是单线程
为什么需要异步?
如果JavaScript中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞,不向下执行。
页面出来,用户看到觉得是“卡死了”,所以需要异步。
JavaScript单线程又是如何实现异步的呢?
是通过的事件循环(event loop)实现异步的。
JavaScript中的event loop()
JavaScript的执行机制是
- 首先判断JavaScript是同步还是异步,同步就进入主线程,异步就进入
event table
- 异步任务在
event table
中注册函数,当满足触发条件后,被推入event queue
- 同步任务进入主线程后一直执行,直到主线程空闲时,才会去
event queue
中查看是否有可执行的异步任务,如果有就推入主线程中
了解了这几个概念,再来看看javascript是怎么执行代码的就比较轻松愉快了。开始吧
console.log(1)
Promise.resolve().then(function () {
console.log(2)
})
new Promise(function(resolve, reject){
console.log(3)
resolve()
}).then(function () {
console.log(4)
setTimeout(function () {
console.log(5)
})
})
console.log(6)
setTimeout(function () {
Promise.resolve().then(function () {
console.log(7)
setTimeout(function () {
console.log(8)
})
})
})
第一波先执行宏任务
- JavaScript引擎在执行这段代码的时候,首先将全局执行上下文压入栈中,然后呢,在执行的时候会碰到
console.log
函数,将它压入栈中,这个时候,直接执行console
函数,并输出1
。然后console
函数出栈 - 继续往下执行,碰到了
Promise.resolve().then()
,先将Promise.resolve().then()
压入栈中,然后执行Promise.resolve().then()
,前面说过,这个then()
函数是个微任务,它会将传入给它的回调函数加入到微任务队列中。然后Promise.resolve().then()
就出栈了。 - 接着执行,遇到
promise
的构造函数,这个构造函数是一个宏任务,会直接将传递给它的函数压入栈中。执行console
函数并输出3
,执行完,console
函数出栈,接着执行resolve()
函数,并出栈。然后继续执行then函数,将传递给then函数的参数函数放到微任务队列中: - 继续来,继续往下执行。碰到
console.log(6)
,二话不说,直接压入栈中,执行,输出6
,出栈,一气呵成。 - 接着,引擎碰到了
setTimeout
函数,这家伙是个宏任务,但同时它会将传递给它的函数,加入到任务队列中:
好了,到此第一波宏任务就全部执行完毕。接着,引擎就会去看一下微任务队列中有没有任务,如果有的话,执行它们。
第二波先执行微任务
- 现在看到的是,微任务队列中有两个任务。按照队列的先入先出规则,先从
function () {console.log(2)}
开始执行。先是函数入栈,然后执行函数,输出2
,然后函数出栈。
接着执行下面这段代码:
console.log(4)
setTimeout(function () {
console.log(5)
})
先从 console.log(4)
开始,先将它入栈,然后执行它,输出4
,然后函数出栈。
接着执行:
setTimeout(function () {
console.log(5)
})
上面说过setTimeout
是宏任务,加入到任务队列中去。
继续向下执行
function(){
Promise.resolve().then(function () {
console.log(7)
setTimeout(function () {
console.log(8)
})
})
}
这里执行这个函数的时候遇到一个微任务,将这个微任务添加到微任务队列,这一波又执行完了,接着就回去检查微任务队列中有没有待执行的任务,一看还真有两个小可爱等待执行,于是没什么好说的,直接拧出去就执行。
第三波
- 先是执行
console.log(7)
,然后输出7
。接着执行setTimeout
,将传递给他的任务添加到任务队列中去 - 最后就剩这两个函数了,按照队列的先入后出一次执行吧,输出5和8。
最后看一下打印最后的结果就是1,3,6,2,4,7,5,8
。你写对了吗?
4.总结
最后牢记两点
- JavaScript是单线程语言。
- JavaScript的
Event Loop
是JS的执行机制。