Skip to main content

4. 浏览器事件环

1.什么是进程

进程是系统进行资源分配和调度的基本的单位。多个进程之间是独立的,相互隔离的,因此一个进程的挂掉不会影响其他进程。

浏览器采用的就是多进程模型

  • 每个网⻚运行在自己的进程中,如果一个网⻚发生故障,只会影响该网⻚,而不会对整个浏览器造成影响。
  • 每个网⻚都在它自己的隔离环境中运行,因此不同网⻚之间的代码不能相互访问。

2.浏览器中进程的组成

  • 浏览器主进程 (管理浏览器的整体界面)

  • 渲染进程 (每个⻚面一个,负责呈现⻚面内容及响应用户交互)

  • 网络进程 (加载资源的进程)

  • 插件进程(独立的进程)

  • GPU 绘图进程 (通过 GPU 来处理图形渲染和图形处理的过程)

  • ...

3.渲染进程

  • js 引擎线程 (执行 js 代码)--单线程

  • 渲染线程 (渲染⻚面、布局、画⻚面)

  • 网络线程(处理⻚面的网络请求)

  • GPU 线程 (使用 GPU 进行图形渲染)

  • 合成线程(将多个图层合并为单个图像)

  • 事件触发线程 (调度任务)

    在 js 执行的过程中还会创建一些其他的线程 (定时器、http 请求、事件)

4.同步和异步

同步:在执行一个操作时,程序必须等待该操作完成后才能继续执行下一步操作。 异步:在执行一个操作时,程序不需要等待该操作完成,而是可以继续执行其他任务。

思考:阻塞和非阻塞、同步和异步的关系? 异步一定是非阻塞的吗(一定),同步一定是阻塞的吗(一定), 针对是调用方和被调用来说的。

  • 当我们调用一个方法之后,收否需要等待这个操作返回的结果 (不需要等待就是异步操作, 同步操作就是需要等待这个操作的返回值)
  • Promise.then(原生的 promise) setTimeout mutationObserver(h5 提供的 api) ajax 请求 用户的事件 setImmediate(ie 特有的) 脚本, ui 渲染
  • process.nextTick i/o setImmediate
  • requestAnimationFrame requestIDleCallback (16.6ms) 到达渲染时机后 帧执行之前 requestFrameAnimation 方法 一针执行完毕后剩余的时间 requestIDleCallback (回调)

5.宏任务 微任务

  • 宏任务: setTimeout ajax 请求 setImmediate 脚本<script> ui 渲染 i/o MessageChannel(消息通道)

  • 微任务: Promise.then(原生的 promise) mutationObserver(h5 提供的 api) process.nextTick

  • requestAnimationFrame(动画) requestIDleCallback (react 中 fiber 的实现就是) 可以认为是宏任务?

mutationObserver 案例

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="abc"></div>
<script>
let observer = new MutationObserver(() => {
// 回调异步的
console.log(abc.children.length)
})

observer.observe(abc, {
childList: true,
})

for (let i = 0; i < 20; i++) {
abc.appendChild(document.createElement("p"))
}

for (let i = 0; i < 20; i++) {
abc.appendChild(document.createElement("p"))
}
</script>
</body>
</html>

MessageChannel 案例

const channel = new MessageChannel()
channel.port1.onmessage = function (e) {
// nextTick
console.log(e.data)
}
console.log(1)
channel.port2.postMessage("abc")
console.log(2)

给你代码让你判断,代码如何执行的?

  • 浏览器内部有很多的宏任务队列 (默认在执行渲染的过程中,会被单独的一个宏任务队列来管理) 所以在剖析代码运行的时候我们就当只有一个宏任务队列
  • 宏任务是最新执行的(我要先执行一个脚本>) - -> 可能会调用一些 webApi (定时器, ajax ) -> 成功后会将这些回调放入到宏任务队列中 (不是立刻放入) - -> 可能调用了一些宿主环境提供的方法 promise.then / mutationObserver -> 立刻放入微任务队列中
  • 宏任务执行的时候 会对应产生一个微任务队列
  • 当当前的宏任务执行完毕后,会清空微任务队列(清空的过程可能会产生 宏任务、微任务,产生的宏任务逻辑和上面一致,产生的微任务会被放到当前队列中)
  • 微任务清空后,会在宏任务队列中取出来一个继续执行

宏任务和微任务的调度操作靠的就是 eventLoop 来实现的

6.异步任务划分

  • 宏任务: 脚本的执行、ui 渲染、定时器、http 请求、事件处理(用户操作)、MessageChannel、setImmediate
  • 微任务: 原生的 promise.then、mutationObserver、node 中的(process.nextTick)、queueMicrotask
  • requestFrameAnimation 、requestIDleCallback (这两个方法是根渲染相关的,不应该算事件环的一部分)

通常浏览器会实现一个单独的宏任务队列,包含所有需要在主线程上完成的任务。宏任务队列中的任务在执行完成后,会检查是否存在微任务队列,如果存在,则按顺序执行该队列中的任务

  • 主线程代码执行完毕后,会查找所有的微任务将其执行并且清空, 如果微任务嵌套会将生成的新的微任务放到本次队列的后面。

  • 微任务执行完毕后,要检测"是否需要"渲染,浏览器有自己的刷新频率。 渲染一定是在宏任务之前做的。

  • 在去宏任务队列中取出一个宏任务继续执行此流程。

案例分析

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>

<body>
<script>
document.body.style.background = "red"
console.log(1)
Promise.resolve().then(() => {
console.log(2)
document.body.style.background = "yellow"
})
console.log(3)

// 结果 1 3 2 页面直接黄色 因为先执行微任务队列才进行GUI渲染
// 如果是setTimeout 会由红变黄 闪烁 也可能不闪 // 页面渲染只有达到16.6ms 才会渲染,浏览器有合并机制
</script>
</body>
</html>

我们的逻辑希望是异步的,但是不希望多次渲染,此时可以采用 Promise.then

想让触发浏览的渲染 setTimeout(()=>{}) transiton, 触发浏览器的回流、重绘

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>

<body>
<button id="button"></button>
<script>
button.addEventListener("click", () => {
console.log("listener1")
Promise.resolve().then(() => console.log("micro task1"))
})
button.addEventListener("click", () => {
console.log("listener2")
Promise.resolve().then(() => console.log("micro task2"))
})
button.click() // click1() click2() 不会产生对应的宏任务,是在主线程中执行的

// 点击的话: 默认会产生两个宏任务
// 默认的输出 listener1 listener2 micro task1 micro task2
// 点击后的输出 listener1 micro task1 listener2 micro task2
</script>
</body>
</html>
Promise.resolve().then(() => {
console.log("Promise1")
setTimeout(() => {
console.log("setTimeout2")
}, 0)
})
setTimeout(() => {
console.log("setTimeout1")
Promise.resolve().then(() => {
console.log("Promise2")
})
}, 0)
// Promise1 setTimeout1 Promise2 setTimeout2
console.log(1)
async function async() {
console.log(2)
await console.log(3) // ==> yield console.log(3)
// Promise.resolve(console.log(3)).then(()=>{console.log(4)})
console.log(4)
}
setTimeout(() => {
console.log(5)
}, 0)
const promise = new Promise((resolve, reject) => {
console.log(6)
resolve(7)
})
promise.then((res) => {
console.log(res)
})
async()
console.log(8)
// 微任务队列 [7,4 ]

// 1 6 2 8 3 7 4 5
//// ----原生的promise中 会判断如果返回的是一个promise,那么会给这个promise在产生一个微任务------
Promise.resolve()
.then(() => {
console.log(0)
return new Promise((resolve) => {
resolve("a")
}) // x resolvePromise会调用x.then()
//return Promise.resolve('a')
})
.then((res) => {
console.log(res)
})
Promise.resolve()
.then(() => {
console.log(1)
})
.then(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
.then(() => {
console.log(4)
})
.then(() => {
console.log(5)
})
// 微任务队列 [ then0 then1, Promise.resolve 产生的then,then2,then('空的'),then('a'),then3]

// 0 1 2 a 3 4 5