从进程和线程入手,我彻底明白了EventLoop的原理!
前言
最近看到了一个说法: UI 渲染是和宏任务微任务平行的一个操作步骤,不属于宏任务和微任务中的任何一种。
在我过去的认知里,一直把UI渲染当作宏任务中的一份子,看到这个说法的时候,我已经迫不及待地想要进行求证了。这篇文章我会从进程和线程入手分析EventLoop机制,然后编写示例代码进行佐证。
进程和线程
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。
通俗地讲,进程就相当于应用程序运行的一个实例,每个进程之间互不干扰。你可以理解为一个进程就相当于一个独立的空间,里面做的任何事情,都只影响当前进程内部。所以每个应用程序都至少需要一个进程,也可以是多个进程,我们使用的浏览器就是多进程的程序,每个Tab页就是一个独立的进程。
线程
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是属于进程里面的执行流,每一个线程同时只能做一件事,一个进程可以有一条或多条线程。一个进程里只有一条线程就称为单线程,有多条则被称为多线程。
为什么JS被称作单线程?
上面我们说到了,浏览器每个Tab页就是一个独立的进程,进程中是可以有多条线程的。没错,浏览器的Tab页也是多线程的,那为什么又说JS是单线程的呢?因为我们的每一个网页运行环境(Tab页)虽然是多线程的,但是在这些线程中,执行JS代码的线程只有一个,所以我们常说的JS是单线程的。 浏览器进程中的主要线程有以下几个:
- JS引擎线程 JS内核,负责执行JS代码
- GUI渲染线程 负责渲染浏览器界面,与JS引擎线程互斥
- 事件管理线程 控制EventLoop,管理事件队列
- 计时器线程 负责处理定时器,在计时完毕后将定时任务添加到事件管理线程的事件队列中
- 网络线程 处理http网络请求,收到响应后,讲回调事件添加到事件管理线程的事件队列中
宏任务和微任务
JS代码执行过程中,分为同步任务和异步任务。同步任务会顺序执行,而异步任务则会添加到事件队列中,等待所有同步任务执行完后,再按先进先出的顺序推入JS引擎线程执行。异步任务又分为宏任务和微任务,执行宏任务过程中产生的微任务会在当前宏任务执行完后立马执行,不用排在已有的宏任务队列后面执行,所以微任务是为了让优先级更高的任务插队而诞生的。
常见的宏任务包括:setTimeout、setInterval、setImmediate、requestAnimationFrame 、script标签代码、IO操作等。
常见的微任务包括:process.nextTick、Promise.then/catch/finally、Object.observe、MutationObserver等。
EventLoop机制
铺垫了这么多,终于要进入正题了,先上一张流程图,记住这张图有利于后面理解。
浏览器的事件循环一共分为这样几步:
- 打开网页,GUI渲染线程解析到script标签(宏任务)。
- 控制权交给JS引擎线程。同步代码依次执行完后,产生的宏任务和微任务添加到事件管理线程的事件队列中。
- 判断事件队列中有没有微任务,有则到第3步,没有到第4步。
- 事件管理线程把微任务推入JS引擎线程执行栈,依次执行完同步代码后,回到第3步。
- 一轮事件循环完成后,控制器交给GUI渲染线程,对dom变更进行一次渲染。然后回到第2步执行下一个宏任务
代码验证
宏任务和微任务执行顺序?
用setTimeout和Promise.resolve().then分别创建宏任务和微任务。
- 代码:
function showLog(text){
console.log(text);
}
setTimeout(()=>showLog("宏任务1"));
Promise.resolve().then(()=>showLog("微任务1"));
Promise.resolve().then(()=>showLog("微任务2"));
Promise.resolve().then(()=>showLog("微任务3"));
- 运行结果:
- 结论:从截图中可以看到,虽然宏任务创建的时间更早,但是宏任务要到下一次事件循环才开始执行,而微任务会在本次循环全部执行,所以代码中的微任务执行时间在宏任务之前。
UI渲染是穿插在每轮循环之间的吗?
为了让每次调用changeText函数的间隔时间更长,让眼睛可以观察到,在changeText函数中加入了一段for循环打印增加运行所需时间。(运行时请打开开发者工具栏的Console,否则运行速度很快看不清)。
- 代码
const textDom = document.createElement("span");
document.body.appendChild(textDom);
function changeText(text) {
for (let i = 0; i < 10000; i++) {
console.log(i);
}
textDom.innerHTML = text;
}
setTimeout(() => changeText("宏任务1"));
setTimeout(() => changeText("宏任务2"));
setTimeout(() => changeText("宏任务3"));
Promise.resolve().then(() => changeText("微任务1"));
Promise.resolve().then(() => changeText("微任务2"));
Promise.resolve().then(() => changeText("微任务3"));
- 运行结果:(Gif图片,5秒)
- 结论:从gif中可以看到,网页中的文字变化为 空白=>微任务3=>宏任务1=>宏任务2=>宏任务3。证实了当次宏任务+产生的所有微任务为一次循环,UI渲染穿插在两次循环之间,也就是两个宏任务之间,这也是为什么上面的代码中,前面两个微任务调用changeText没有渲染到页面上的原因。
总结
从上文中我们知道了进程和线程的区别;浏览器运行环境由多个线程配合完成EventLoop;事件队列分为宏任务和微任务;一个宏任务+产生的一组微任务为一次循环;UI渲染穿插在两次循环之间。
那么,你认为UI渲染属于宏任务吗?
转载自:https://juejin.cn/post/7269420553042919487