# NextTick
# 介绍
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
使用 nextTick 方法经常用来解决以下问题
- 获取更新后的 DOM 元素
- 更新后的样式计算
- 触发一些特定事件
# Vue2 NextTick 实现原理
- 源码位置:/src/core/util/next-tick.js
- callbacks也就是异步操作队列
- callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// cb 回调函数会经统一处理压入 callbacks 数组
callbacks.push(() => {
if (cb) {
// 给 cb 回调函数执行加上了 try-catch 错误处理
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// 执行异步延迟函数 timerFunc
if (!pending) {
pending = true;
timerFunc();
}
// 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
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
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
timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:
Promise.then、MutationObserver、setImmediate、setTimeout
通过上面任意一种方法,进行降级操作
export let isUsingMicroTask = false;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 判断1:是否原生支持Promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver)
|| MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 判断2:是否原生支持MutationObserver
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
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
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
- 把回调函数放入callbacks等待执行
- 将执行函数放到微任务或者宏任务中
- 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
- 如果nextTick是微任务,它会在当前同步任务执行完立即执行所有的微任务,也就是修改DOM的操作也会在当前tick内执行,等本轮tick任务全部执行完成,才是开始执行UI rendering
- 如果nextTick是宏任务,它会被推进宏任务队列,并且在本轮tick执行完之后的某一轮执行,注意,它并不一定是下一轮,因为你不确定宏任务队列中它之前还有所少个宏任务在等待着
- 为了能够尽快更新DOM,Vue中优先采用的是微任务,并且在Vue3中,它没有了兼容判断,直接使用的是promise.then微任务,不再考虑宏任务
# vue3 nextTick 和 Promise
# nextTick 和 数据变更
<template>
<p id="test">{{ message }}</p>
<button @click="changeData">变更数据</button>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const message = ref('hello');
const changeData = () => {
// message.value = 'world0';
nextTick(() => {
message.value = 'world1';
const text = window.document.getElementById('test').innerText;
console.log(1, text);
});
message.value = 'world2';
nextTick(() => {
message.value = 'world3';
const text = window.document.getElementById('test').innerText;
console.log(2, text);
});
};
// 不注释 message.value = 'world0';
// 1 'world2'
// 2 'world2'
// 注释 message.value = 'world0';
// 1 'hello'
// 2 'world1'
</script>
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
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
注释 message.value = 'world0'
- 这里第一个 nextTick 由于之前没有数据变更,所以直接当成微任务添加到事件队列中,然后执行,由于没有数据变更,所以直接打印出 'hello'
- 第二个 nextTick 由于之前有数据变更,所以直接当成微任务添加到事件队列中,然后执行,由于有数据变更,取微任务执行之前最后一次变更的数据,所以打印出 'world1'
不注释 message.value = 'world0'
- 这里第一个 nextTick 由于之前有数据变更,所以添加到nextTick的事件队列中,在数据更新后执行,第二个nextTick也加载队列后面
- 最终之前钱赋值world2,所以都打印 'world2'
# nextTick 和 Promise
<template>
<p id="test">{{ message }}</p>
<button @click="changeData">变更数据</button>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const message = ref('hello');
const changeData = () => {
// message.value = 'world0';
Promise.resolve().then(() => {
message.value = 'world1';
const text = window.document.getElementById('test').innerText;
console.log(1, text);
Promise.resolve().then(() => {
const text = window.document.getElementById('test').innerText;
console.log(2, text);
});
});
nextTick(() => {
message.value = 'world2';
const text = window.document.getElementById('test').innerText;
console.log(3, text);
Promise.resolve().then(() => {
const text = window.document.getElementById('test').innerText;
console.log(4, text);
});
});
// message.value = 'world';
Promise.resolve().then(() => {
message.value = 'world3';
const text = window.document.getElementById('test').innerText;
console.log(5, text);
Promise.resolve().then(() => {
const text = window.document.getElementById('test').innerText;
console.log(6, text);
});
});
};
// 注释 message.value = 'world0';
// 1 'hello' 没有赋值 顺序执行promise
// 3 'hello'
// 5 'hello'
// 2 'world3' 存在赋值,取最新的值 world3 执行promise
// 4 'world3'
// 6 'world3'
// 不注释 message.value = 'world0';
// 1 'world0' 有赋值,取最新的值 world0 执行promise
// 5 'world0'
// 3 'world0'
// 2 'world2' 第一层promise执行完,nextTick中存在赋值,取最新的值 world2 执行promise
// 6 'world2'
// 4 'world2'
// 不注释 message.value = 'world'; 放中间
// 1 'hello' 没有赋值 顺序执行promise
// 3 'hello' 没有赋值 顺序执行nextTick
// 5 'world2' 存在赋值,取最新的值 world2 执行promise
// 2 'world2'
// 4 'world2'
// 6 'world3' 多个promise嵌套,存在赋值,会触发更新
</script>
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
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
# nextTick 和 DOM
<template>
<p id="test">{{ message }}</p>
<button @click="changeData">变更数据</button>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const message = ref('hello');
const changeData = () => {
Promise.resolve().then(() => {
message.value = 'world1';
const text = window.document.getElementById('test').innerText;
console.log(1, text);
});
nextTick(() => {
message.value = 'world2';
const text = window.document.getElementById('test').innerText;
console.log(2, text);
Promise.resolve().then(() => {
const text = window.document.getElementById('test').innerText;
console.log(3, text);
});
});
message.value = 'world';
nextTick(() => {
message.value = 'world3';
const text = window.document.getElementById('test').innerText;
console.log(4, text);
Promise.resolve().then(() => {
const text = window.document.getElementById('test').innerText;
console.log(5, text);
});
});
Promise.resolve().then(() => {
message.value = 'world4';
const text = window.document.getElementById('test').innerText;
console.log(6, text);
Promise.resolve().then(() => {
const text = window.document.getElementById('test').innerText;
console.log(7, text);
});
});
};
// 不注释 message.value = 'world'; 放中间
// 1 'hello' 没有赋值 顺序执行promise
// 2 'hello'
// 6 'world2' 存在赋值,取最新的值 world2 执行promise
// 3 'world2' 注意这里 先执行nextTick中的promise
// 4 'world2'
// 7 'world3'
// 5 'world3'
</script>
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
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
- 总结下来,如果有值变更,多个nextTick会在当前所有微任务执行完后顺序执行
- 如果没有值变更,nextTick等同于Promise,不会触发Dom更新,Dom更新算一次宏任务
- 如果存在嵌套的Promise,会在一层执行完后更新Dom再执行二层任务
← 组件通信方式