执行栈
先来看一下《JavaScript高级程序设计第三版》中有一段关于执行函数与执行栈的描述。
执行环境(execution context,为简单起见,有时也称为“环境”)是 JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个 与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。 而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个方便的机制控制着。
接下来看一段代码,以及这段代码在chrome中的运行过程。
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
我们可以看到以上js代码执行时,首先调用了bar
函数,bar
函数先进入栈,在bar
函数中又调用了foo
函数,foo
函数进栈,然后根据栈先进后出的原则,foo
函数执行完毕后先出栈,然后bar
函数出栈。
任务队列(Task Queue)与事件循环(Event Loop)
但是执行栈并不是遇到什么代码都立即执行的,比如ajax
。
我们在平时开发中经常会用到ajax
,ajax
回调函数里的代码是异步执行的。以下代码会先打印1,然后打印2。
$.ajax({
type:'post',
url:'xxx.com',
data:{xx:'xx'},
success(result) {
console.log(2)
},
});
console.log(1)
这是因为js是单线程的,执行栈不可能一直等待ajax
的回调函数执行完毕之后再执行下面的代码,这样的话如果网络不通畅或者很耗时那么下面的代码会被阻塞很久。所以js在遇到ajax
这样的异步代码时会将其加入到一个任务队列中,执行栈处理完任务后处于空闲状态时就会取出任务队列中最前面的任务进入执行栈。
检查执行栈是否空闲,取出任务队列中的任务并执行的过程是不断循环的,这个过程就是事件循环。
[图片上传失败...(image-d64c57-1584078574091)]
上图是MDN上关于并发模型与事件循环的可视化描述,stack就是执行栈,queue就是任务队列。
微任务(microtask)和宏任务(macrotask)
在js中除了ajax
外,其实还有很多我们熟知的异步代码,比如setTimeout
,setInterval
,Promise
等,同为异步代码,但是还是有一些区别的。
这些异步代码加入到任务队列之后会分为 微任务 和 宏任务 。
- 微任务:
process.nextTick
,promise
,MutationObserver
。 - 宏任务: 主代码块,
MessageChannel
,setTimeout
,setInterval
,setImmediate
,network IO
,UI render
。
事件循环步骤
从上图中可以了解,事件循环中,宏任务和微任务是按照一定步骤执行的。
首先检查队列中是否有等待执行的宏任务,有则执行一个宏任务。
然后检查队列中是否有等待执行的微任务,有则执行队列中所有微任务。
判断是否需要渲染UI,如果需要则渲染UI并 回到第1步 ,不需要则直接回到第1步,检查队列中是否有等待执行的宏任务。
console.log('start')
setTimeout(() => {
console.log('setTimeout callback');
});
Promise.resolve().then(function() {
console.log('promise then callback')
})
按照上面事件循环的步骤,应该先进行第一个宏任务主代码块
的执行,打印start
,遇到setTimeout
将其加入到 宏任务,遇到promise.then
将其加入到 微任务,此时第一个宏任务主代码块
已经执行完毕,接下来检查队列中的微任务并执行,所以打印 'promise then callback' ,执行完所有微任务后,重新检查任务队列发现宏任务setTimeout
然后执行,打印 'setTimeout callback'。
在chrome调试代码运行步骤结果符合预期。
我们再来看一个稍微复杂点的例子
console.log('start')
setTimeout(() => {
console.log('setTimeout callback');
Promise.resolve().then(function() {
console.log('promise2 in setTimeout')
setTimeout(()=>{
console.log('setTimeout2 in Promise2');
})
})
});
Promise.resolve().then(function() {
console.log('promise then callback')
})
上面的代码其实一共进行了 3次 事件循环
- 第1次 循环打印 'start' 与 'promise then callback'。
- 第2次 循环打印了 'setTimeout callback' 与 'promise2 in setTimeout'。
- 第3次 循环打印了 'setTimeout2 in Promise2'。