嗯,还记得上一节讲的js为什么是单线程吗,这一节我们参考网上资料,来整理一下自己对js运行机制的理解 。
想要理解JavaScript的运行机制,需要分别深刻理解以下几个点:
- JavaScript的单线程机制
- 任务队列(同步任务和异步任务)
- 事件和回调函数
- 定时器
- Event Loop(事件循环)
1、因为JavaScript作为浏览器脚本语言,必定会涉及到与用户进行互动,操作DOM。假定这时两个线程分别对某个DOM节点做删除,修改,那么此时浏览器应该以那个线程为准呢?所以为了避免复杂性,JavaScript的语言核心特征就是单线程,一个时间点只能做一件事。
2、JavaScript是单线程的语言,在执行时必须等前面的任务处理完以后才会处理后面的(排队); 是但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务: 主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
用阮大大的图解分析一波:
首先我们的同步任务都在主线程上执行,形成一个执行栈,异步任务只要有了任务结果就存放置在 "任务队列" 里。我们执行过程中先执行执行栈中的所有同步任务,待全部已经执行完了,我们就会去检查任务队列头部的事件,于是任务队列里的那些异步任务避免结束等待,进入执行栈,开始执行。
3、"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。我理解为回调函数=>事件处理函数
4、除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。定时器功能主要由setTimeout()和setInterval()这两个函数来完成。需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
5、主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
JavaScript的事件分两种,宏任务(macro-task)和微任务(micro-task)
宏任务:包括整体代码script,setTimeout,setInterval 可以理解是每次执行栈执行的代码就是一个宏任务
微任务:Promise.then(非new Promise),process.nextTick(node中) 可以理解是在当前task执行结束后立即执行的任务
图是来自掘金的文章里的。他叫:ssssyoki
事件的执行顺序,是先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
输出1,2,4,3,5
解析:
宏任务同步代码console.log('1'),不多说
setTimeout,加入宏任务Event Queue,没有发现微任务,第一轮事件循环走完
第二轮事件循环开始,先执行宏任务,从宏任务Event Queue中独取出setTimeout的回调函数
同步代码console.log('2'),发现process.nextTick,加入微任务Event Queue
new Promise,同步执行console.log('4'),发现then,加入微任务Event Queue
宏任务执行完毕,接下来执行微任务,先执行process.nextTick,然后执行Promise.then
微任务执行完毕,第二轮事件循环走完,没有发现宏任务,事件循环结束
现在知道了这些概念,我们来一个面试题实战一下吧(当初字节跳动一面被问到的题。。。),加油!
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1();
new Promise( function( resolve ) {
console.log('promise1')
resolve();
} ).then( function() {
console.log('promise2')
} )
console.log('script end')
解析一下:
首先我们课外先了解一下async和await,async将你的函数返回值转换为promise对象,await关键字只能在带有async关键字的函数内部使用,await等待的是右侧的[表达式结果],如果右侧是一个函数,等待的是右侧函数的返回值,如果右侧的表达式不是函数则直接是右侧的表达式。await在等待时会让出线程阻塞后面的执行。
我的思路
第一个宏任务中 console.log('script start')直接打印
settimeout 分发到宏任务Event Queue中
第一个宏任务中的第一个微任务 async1 打印async 1 start
因为await,接着打印async2 然后await关键字阻塞了后面async1 end
第一个宏任务中 遇见new promise 打印 promise1 ,then部分回调会先执行本轮宏任务的“同步代码”再执行微任务
接着主线程代码 script end
再挨个执行所有的微任务,依次输出:promise2 ,async1 end
第一个宏任务执行完成,执行第二个宏任务:settimeout