1.JavaScript是单线程
javascript的一个大特点就是单线程,也就是说同一个时间只能做一件事,这样能够避免一个线程在更新DOM节点,而另外一个却在删除该DOM节点。
2.JavaScript的异步
由于javascript是单线程,全部代码只能自上而下执行,但这样子又会有很多的麻烦。如果上一行代码解析时间长,会对下面的代码造成阻塞,这将导致很差的用户体验。所以聪明的程序员想到了将任务分为同步任务与异步任务。
- 任务进入执行栈时先判断是同步任务还是异步任务,同步任务直接进入主线程,而异步任务放到
Event Table。 - 等到主线程任务完成,会去
Event Queue读取对于的函数,进入主线程执行。 - 这些过程不断重复,就是所谓的
Event Loop(事件循环)。
3.什么算是异步任务
- 定时器都是异步操作
- 事件绑定都是异步操作
- AJAX中一般我们都采取异步操作
- 回调函数可以理解为异步(不是严谨的异步操作)
console.log('开始')
setTimeout(()=>{
console.log('执行中...')
},2000)
console.log('结束')
运行结果:开始 结束 执行中...
console.log('开始')作为同步任务,直接放入主线程;- setTimeout()属于异步任务,放入
Event Table,并注册匿名函数。当两秒钟后才被放入Event Queue; console.log('结束')属于同步任务,也被放入主线程。- 如果主线程的任务在两秒钟内执行完成,也不会立即执行setTimeout()的任务,因为只有两秒钟后注册的匿名函数才会被推到
Event Queue中,才能进入主线程执行。还有就是主线程的任务在两秒钟内没有执行完成,setTimeout注册的函数被推到到Event Queue也不会被主线程立即调用。
setTimeout(() => {
console.log('fine')
},2000)
sleep(10000)
console.log('ok');
运行结果:ok fine
console.log('fine')进入Event Table并注册,计时开始。- 执行sleep函数;
- 3秒到了,
setTimeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。 sleep终于执行完了 -console.log('ok')再执行; task()终于从Event Queue进入了主线程执行。
4.宏任务与微任务
我们先来看一个例子:
setTimeout(()=>{
console.log('开始执行')
});
new Promise((resolve)=>{
resolve(console.log('我在哪'));
}).then(function(){
console.log('我是then函数')
});
console.log('终于结束了');
按照上面的规则,我们想的执行结果应该为:我在哪 终于结束了 开始执行 我是then函数
但是实际上运行的结果却是:
我在哪 终于结束了 我是then函数 开始执行
WTH,难道是异步任务的执行顺序,不是前后顺序,而是另有规定?事实上,按照异步和同步的划分方式,并不准确。 除了广义上的同步任务与异步任务,我们对任务还有更加精细的定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
按照上面对任务更加精细的分配,JS的执行机制是:
- 执行一个宏任务,过程中如果遇到微任务,则将微任务放到微任务的"事件队列"里;
- 当前宏任务执行完毕,查看微任务的"事件队列",并将事件队列里的全部微任务执行完毕。
- 进入宏任务"事件队列",继续以上的步骤。
console.log("begin");
setTimeout(()=>{
console.log('计时器1');
process.nextTick(function() {
console.log('nextTick1');
})
})
new Promise(function(resolve) {
resolve(console.log('outterP'));
}).then(function() {
console.log('outterT');
})
setTimeout(()=>{
console.log('计时器2');
process.nextTick(function() {
console.log('nextTick2');
})
new Promise(function(resolve) {
resolve(console.log('innerP'));
}).then(function() {
console.log('innerT');
})
})
console.log('end');
第一轮事件循环:
- 整个
script作为第一个宏任务进入主线程,遇到console.log,打印出begin; - 遇到第一个
setTimeout,则里面的回调函数放到宏任务的Event Queue中; - 遇到
new promise,直接执行,输出outterP,then函数则分配到微任务·Event Queue·中; - 遇到第二个·setTimeout·,则里面的回调函数放到宏任务的
Event Queue中; - 再次遇到
console.log,则打印出end; - 接着执行微任务
Event Queue,执行console.log打印出outterT
第一轮事件循环输出:begin outterP end outterT;
第二轮事件循环:
- 先到宏任务队列里面找到第一个宏任务;
- 遇到
console.log打印输出计时器1 - 遇到
process.nextTick,则放到微任务Event Queue中; - 第二轮宏任务执行完毕,接着执行微任务
Event Queue,执行console.log打印出nextTick1
第二轮事件循环输出:计时器1 nextTick1;
第三轮事件循环:
- 进入宏任务队列,找到第一个宏任务(前面的宏任务执行的时候就出队了,所以又是找第一个)
- 遇到
console.log,打印出计时器2; - 遇到
process.nextTick,将其回调函数放到微任务Event Queue; - 遇到
new promise输出innerP,将then的回调函数放到微任务Event Queue; - 第三轮宏任务执行完毕,接着执行微任务队列,输出
nextTick2和innerT;
第三轮事件循环输出:计时器2 innerP nextTick2 innerT
所以完整的输出为:begin outterP end outterT 计时器1 nextTick1 计时器2 innerP nextTick2 innerT