Event Loop

JavaScript单线程

JavaScript被设计成单线程,主要的原因还是在于操作DOM ,包括在异步的事件处理器中操作DOM。

如果JS是多线程,那么操作DOM必然会涉及资源的竞争,这款语言就必然被实现的非常臃肿,在客户端中跑这样的程序,资源消耗和性能都将是不乐观的,同时在客户端也没有实现多线程的刚需。

如果设计成单线程,并辅以完善的异步队列来实现,那么运行成本就会比多线程小很多了。

随着HTML5的到来,JavaScript也开始支持多线程webWorker,但它不能操作DOM。

JavaScript异步

单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行。

如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直排队。

这从用户体验角度上是不可接受的,所以JavaScript就有了异步。

同步任务

代码从上到下按顺序执行。

异步任务

宏任务

script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax

微任务

Promise.then.catch.finally、MutationObserver、process.nextTice(Node.js环境)

运行机制

所有的同步任务都是在主进程执行的,形成了一个执行栈。

主线程之外,还存在一个“任务队列”。异步任务执行队列中,先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick,如此形成循环。

运行机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
async function Prom() {
console.log("Y")
await Promise.resolve()
console.log("X")
}
setTimeout(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
}, 0)
setTimeout(() => {
console.log(3)
Promise.resolve().then(() => {
console.log(4)
})
}, 0)
Promise.resolve().then(() => {
console.log(5)
})
Promise.resolve().then(() => {
console.log(6)
})
Promise.resolve().then(() => {
console.log(7)
})
Promise.resolve().then(() => {
console.log(8)
})
Prom()
console.log(0)
// 执行顺序:Y 0 5 6 7 8 X 1 2 3 4

nextTick

创建一个异步任务,它要等到同步任务执行完成后才执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<template>
<div ref="box" class="wraps">
<div>
<div class="item" v-for="(item,index) in chatList" :key="index">
<div>{{ item.name }}</div>
<div>{{ item.message }}</div>
</div>
</div>
</div>
<div class="ipt">
<div>
<textarea v-model="ipt" type="text"></textarea>
</div>
<div>
<button @click="send">send</button>
</div>
</div>
</template>

<script setup lang="ts">
const chatList = reactive([
{name:"张三",message:"xxxxxxxxxx"},
])
const box = ref<HTMLDivElement>()
const ipt = ref("")
const send = async ()=> {
if(ipt.value === "") return
chatList.push({name:"李四",message:ipt.value})
// 直接操作DOM的滚动高度,有时好使有时不好使
// box.value!.scrollTop = 1145141919
// Vue更新DOM是异步的,更新数据是同步的
// 此处执行的都是同步代码,走完才去更新DOM
// 造成了先开始滚动,还没滚完就被插入DOM,于是滚动高度混乱的情况

// 1.回调函数模式
// nextTick(()=> {
// box.value!.scrollTop = 1145141919
// })

// 2.async await写法(从此之后的代码都是异步的啦)
await nextTick()
box.value!.scrollTop = 1145141919
ipt.value = ""

// 当操作DOM时发现数据读取的还是上次的,就需要使用nextTick
}
</script>

<style scoped lang="scss">
.wraps {
margin: 10px auto;
width: 500px;
height: 400px;
overflow: auto;
overflow-x: hidden;
background-color: #fff;
border: 1px solid #ccc;

.item {
width: 100%;
height: 50px;
background-color: #ccc;
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #fff;
}
}
.ipt {
margin: 10px auto;
width: 500px;
height: 40px;
background: #fff;
border: 1px solid #ccc;

textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
}
button {
width: 100px;
margin: 10px 0;
float: right;
}
}
</style>

浏览器在一个tick里做了如下工作:

  1. 处理用户的事件(event),例如click、input、change等
  2. 执行定时器任务
  3. 执行requestAnimationFrame
  4. 执行dom的回流与重绘
  5. 计算更新图层的绘制指令
  6. 绘制指令合并主线程 如果有空余时间会执行requestidlecallback

如果DOM改变的操作是同步的,如果短时间内某响应式数据持续变化,DOM就会接连改变,消耗性能。如果是异步,下一个tick读取数据并改变DOM,就不会接连改变DOM。

源码

在Vue源码(/package/runtime-core/src/scheduler.ts)中可以看到nextTick和异步DOM更新的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
const queue: SchedulerJob[] = []

const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null

// 通过Promise.then()实现异步执行nextTick的回调函数
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
): Promise<void> {
// p是一个promise
const p = currentFlushPromise || resolvedPromise
// 把传入的函数放到.then()里执行,走了个微任务
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
// nextTick的原理就是把函数放进Promise里执行,把代码变异步

// job就是组件实例上的update方法(effect函数)
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
// 去重判断
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
// 如果组件实例没有id,添加到队列尾部
queue.push(job)
} else {
// 按照id自增的顺序排列job
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}

function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// 创建了一个promise微任务 把flushJobs放进去执行
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}

function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
// 给队列进行排序
queue.sort(comparator)
// 更新前确保队列是正确的
// 先创建父组件再创建子组件
// 如果父组件更新时子组件被卸载了,那么子组件的更新可以被跳过
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP

// 遍历queue队列批量执行
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
// 执行job函数
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 执行完成 重置状态
flushIndex = 0
queue.length = 0

// 执行不同的调度策略
flushPostFlushCbs(seen)

isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}