通过厨子炒菜来说,浏览器事件循环Event Loop

364 阅读8分钟

1.关于javascript

javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!(不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的)

2.javascript事件循环

事件循环是js实现异步的一种方法,也是js的执行机制。

首先浏览器会把主任务队列中的同步任务挨个全部执行完,然后再去等待任务队列中看哪个任务可以执行了, 然后把该执行的任务放到主任务队列中去执行,等这个任务执行完, 再去等待任务中看谁可以执行了,再把这个任务放到主任务队列中执行... 如此循环。 这种循环叫做事件循环(Event Loop) js是单线程,js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

  1. 同步任务
  2. 异步任务

一张图表示事件循环

  1. 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  2. 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  3. 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  4. 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

主线程执行栈何时为空?

js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

所以可以看做是这样的:

  1. 浏览器线程先执行同步任务,途中遇到异步任务就将其加入到等待任务队列中去,然后继续向下执行,
  2. 等同步任务全部执行完毕后,再去等待任务队列中去将所有可执行的微任务逐个执行,
  3. 执行完微任务后在拿取第一个先到达执行条件的宏任务来执行,
  4. 执行完后再去等待任务队列中清理执行完所有已到达执行条件的微任务,
  5. 然后再拿取下一个宏任务来执行,如果宏任务执行产生微任务或者微任务执行产生宏任务就一样加入到等待任务队列中,然后还是按照主线程每次到等待队列中先执行完所以的微任务再逐个执行宏任务的顺序来走

异步任务都是谁先到达条件谁先执行,但是谁先到达执行条件也有优先级的问题,这个优先级要看这个任务是宏任务还是微任务;微任务的优先级比宏任务的要高;

3.运行机制

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  1. 执行一个宏任务(栈中没有就从事件队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  5. 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

宏任务:

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。 浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

微任务:

microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

总结

javascript是单线程的在浏览器运行的,但是浏览器是多线程和多进程的,javascript运行时,会创建一个执行栈和一个消息队列,两个数据结构;单线程相当于只有一个厨房,而消息队列是一个点菜订单,在第一次执行第一个宏任务时,相当在炒第一单的菜,在这一道菜中会产生一些调料,这些相当于微任务,在一次宏任务执行中,会把遇到的微任务放到执行栈的微任务队列中,遇到另一个宏任务就又添加到消息队列中,炒菜(宏任务)执行完成,就拿出这道菜需要的全部调料(微任务)添加(执行),当前主线程执行完(浏览器的多进程服务判断),就从消息队列上面去取下一个任务,这样循序执行下去...

-宏任务(macrotask)微任务(microtask)
谁发起的宿主(Node、浏览器)JS引擎
具体事件1.script(同步代码), 2.setTimeout/setInterval, 3.UI rendering/UI事件, 4.postMessage,MessageChannel, 5.setImmediate,I/O(Node.js)1. Promise, 2.MutaionObserver, 3.Object.observe(已废弃;Proxy 对象替代), 4.process.nextTick(Node.js)
谁先运行后运行先运行
会触发新一轮tick吗不会

案例

setTimeout(function(){
console.log('1')
});
new Promise(function(resolve){
  console.log('2');
  resolve();
}).then(function(){
console.log('3')
});
console.log('4');//2  4  3  1

分析:

  1. settimeout是宏任务,虽然先执行的他,但是他被放到了宏任务的eventqueue里面
  2. 往下检查看有没有微任务,发现Promise回调函数内的代码是同步的(微任务)输出2
  3. then函数把他放入了微任务序列。
  4. 主代码块(宏任务)输出4
  5. 主线进程所有代码执行结束。先从微任务queue里拿回掉函数,输出3微任务全部完成
  6. 再从宏任务的queue拿函数。输出1
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')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
  1. 主代码块输出1
  2. 宏任务1,setTimeout(2,3,4,5)
  3. 微任务1process(6)
  4. promise立即执行,回调是同步输出7,then微任务2(8)
  5. 宏任务2setTimeout(9,10,11,12),主代码全部执行完(余微任务1,2,宏任务1,2)
  6. 执行微任务1,输出6
  7. 执行微任务2,输出8 ,微任务全部执行完(余宏任务1, 2)
  8. 执行宏任务1,输出2,增微任务process(3),promise立即执行回调输出4,微任务(5),
  9. 执行微任务输出3,输出5
  10. 执行宏任务2,输出9,增微任务process(10),promise立即执行回调输出11,微任务(12),
  11. 执行微任务输出10,输出12
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');
  1. 输出“script start”
  2. setTimeout宏任务
  3. 执行async1()输出async1 start,执行 async2(),输出 “async2”。
  4. async2执行完毕,将await async2 后面的代码加入到 微任务队列async1 end');
  5. 继续执行,new Promise, 同步输出“promise1”。promise.then,加入到微任务队列,
  6. 输出script end
  7. 当前宏任务执行完毕,查看微任务队列输出async1 end “promise2”
  8. 微任务全部执行完,检查宏任务,输出setTimeout

改上面代码:

async function async1() {
    console.log('async1 start');
    let p = await async2();
    console.log(p);
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
    return new Promise((resolve, reject) => {
        resolve(10);
    })
}
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');
//第一次宏任务执行
script start
async1 start
async2
promise1
script end
//微任务执行
promise2
10
async1 end
//下一次宏任务执行
setTimeout
  1. new promise的操作就跟你 new 一个普通函数没区别,所以这一句其实是宏任务,但后面的then是微任务
  2. resolved后的promise对象会在这该级别事件队列结束之后才开始执行,及执行与该轮微任务队列中,始于下一级别宏任务之前
  3. resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
  4. 如果出现两个这种微任务,则先出现的会先执行
  5. async 函数中,遇到 await 会跳出当前函数,并让出线程,再将await后面的代码放到 微任务(microtask)队列中