CPU、进程、线程
CPU、进程、线程之间的关系
cpu:计算机的核心,承担了所有的计算任务;
进程: 进程就好比工厂的车间,它代表CPU所能处理的单个任务,CPU使用时间片轮转进度算法来实现同时运行多个进程;
线程: 线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源;
浏览器是多进程的,其中包含了主进程、第三方插件进程、GPU进程、渲染进程,其中有渲染进程,也就是我们说的浏览器内核;
浏览器内核又包含多条线程
GUI渲染线程:
- 负责渲染页面,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与js引擎线程互斥,防止渲染结果不可预期
JS引擎线程:
- 负责处理解析和执行javascript脚本程序
- 只有一个JS引擎线程(单线程)
- 与GUI渲染线程互斥,防止渲染结果不可预期
事件触发线程:
- 用来控制事件循环(鼠标点击、setTimeout、ajax等)
- 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
定时触发器线程:
- setInterval与setTimeout所在的线程
- 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
- 计时完毕后,通知事件触发线程
异步http请求线程:
- 浏览器有一个单独的线程用于处理AJAX请求
- 当请求完成时,若有回调函数,通知事件触发线程
同步与异步
javascript是单线程的,但是浏览器并不是单线程的;例如我们使用了setTimeout,在浏览器中就会有一个定时触发器线程去为我们执行代码;也就是说这些内部的API会用单独的线程去执行;
同步还是异步指的是 运行环境提供的API是以同步或异步模式的方式去工作;
js中的任务分为同步任务和异步任务
同步模式:
同步任务都在JS引擎线程上执行,形成一个执行栈;
代码当中的任务依次执行,执行顺序与代码的编写顺序完全一致,在单线程的情况下,大多数任务都会以同步模式的方式去执行;但是如果其中有个任务执行的时间过长,就会产生阻塞,意味着页面会卡顿或者无法操作,所以需要有异步模式,来解决程序中无法避免的耗时操作;
console.log(1)
function b() {
console.log(3)
}
function a() {
console.log(2)
b()
}
a()
console.log(4)
异步模式:
事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中;
不会去等待这个任务的结束才开始下一个任务,开启过后就立即往后执行下一个任务,后续逻辑一般会通过回调函数的方式来定义;
console.log(1)
setTimeout(function time1() {
console.log(4)
}, 1800)
setTimeout(function time2() {
console.log(3)
setTimeout(function inner () {
console.log(5)
}, 1000)
}, 1000)
console.log(2)
Event Loop
执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行;
- JS引擎线程只执行执行栈中的事件
- 执行栈中的代码执行完毕,就会读取事件队列中的事件
- 事件队列中的回调事件,是由各自线程插入到事件队列中的
- 如此循环

在js中,任务又可以被细分为宏任务和微任务;
宏任务
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。
前面我们提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。
常见的宏任务:
- script(可以理解为外层同步代码)
- setTImeout
- setInterval
一个例子:
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
我们在控制台输入上面的代码会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。
第二个例子:
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)
我们继续在控制台输入上面的代码会看到,页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。
微任务
我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
常见的微任务:
- Promise.then
- Async/Await
- process.nextTick
第一个例子:
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);
控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行,所以 2 最后输出
页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了黑色,然后才执行的渲染
第二个例子:
setTimeout(() => {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)
setTimeout(() => {
console.log(2)
}, 0)
// 1 3 2
上面代码共包含两个 setTimeout ,也就是说除主代码块外,共有两个宏任务, 其中第一个宏任务执行中,输出 1 ,并且创建了微任务队列,所以在下一个宏任务队列执行前, 先执行微任务,在微任务执行中,输出 3 ,微任务执行后,执行下一次宏任务,执行中输出 2。
总结
-
执行一个
宏任务(栈中没有就从事件队列中获取) -
执行过程中如果产生了新的
微任务,就将它添加到微任务的任务队列中 -
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行) -
当前
宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染 -
渲染完毕后,
JS线程继续接管,开始下一个宏任务(从事件队列中获取)

一些问题:
Q:定时器的时间一定准确嘛?
A:不一定
setTimeout(() => {
console.log(111111)
}, 0)
new Promise((resolve, reject) => {
resolve()
}).then(() => {
for (var i = 0; i < 3000000000; i++) { }
console.log(2222222)
})
循环分析:
第一轮循环:
执行setTimeout,定时器任务线程接手;
执行promise中的resolve(),在当前微任务队列中放入fn2;
当前宏任务队列没有任务,执行微任务队列fn2;
执行完成,GUI渲染引擎渲染,进入下一轮循环;
第二轮循环:
定时器任务线程触发条件达成,将fn1放入宏任务队列;
此时执行栈为空,事件触发线程发现宏任务队列有任务fn1,放入执行栈执行;
fn1执行完成,事件触发线程再次循环发现执行栈和任务队列为空;
GUI渲染引擎接手,渲染,进入下一轮循环;
Q:既然渲染阶段是在这个宏任务的执行栈清空,微任务队列清空之后由GUI渲染线程接管进行。那为什么我在循环appendChild之后,立马用document.getElements能获取插入的节点呢?这个时候还没有渲染是怎么获取到的?
A:appendchild只是修改了dom树,但是浏览器并没有渲染,要等到下一次gui渲染进程渲染之后界面上才会出现,但是在js中你已经可以访问这个dom了;
一些细节:
async/await执行顺序
我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 旧版输出如下,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为:
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。 知乎上也有相关讨论,可以看看 www.zhihu.com/question/26…
如果await后面跟的是一个异步函数的调用:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
输出为:
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
参考资料:1. 「前端进阶」从多线程到Event Loop全面梳理
2. 面试题:说说事件循环机制(满分答案来了)
本文详细介绍了CPU、进程、线程以及JavaScript中的Event Loop机制,包括宏任务和微任务的执行顺序。JavaScript是单线程的,但通过Event Loop协调同步和异步任务的执行。宏任务包括script、setTimeout等,它们按顺序执行。微任务如Promise.then、Async/Await则在当前宏任务执行完后立即执行。文章还探讨了定时器的不准确性、渲染过程以及async/await的执行细节,并给出了相关问题的解答和参考资料。
2827

被折叠的 条评论
为什么被折叠?



