this.$nextTick(cb)

一、简化图

vueasync

二、异步更新

Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。

当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。如果同一个watcher被多次触发,只会被推入到队列中一次。

然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。

如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。

flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。

flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。

nexttick就是在dom更新后执行延迟回调,可以获取更新后的dom

1.先判断对promise等的支持度,能用微任务就用微任务。宏任务比微任务实效性差。一般把一个宏任务开始到微任务队列清空视为一次循环,两个宏任务之间时长肯定比两个微任务长;settimeout最短延迟为4ms。

2.flushcallback函数,遍历执行回调函数

3.nexttick,里面就是我们写的回调函数。供flushcallback用。将flushcallback放入异步任务里

三、源码

1.Vue,Observer,Compiler,Dep,Watcher
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// 1: 通过属性  保存选项的数据
// 2: 把data中的成员 转换为getter和setter 注入到vue实例中 方便使用
// 3:调用observer对象 监听数据变化
// 4:调用compiler 解析指令和插值表达式
class Vue {
constructor(options) {
// 通过属性 保存选项的数据
this.$options = options || {};//如果我们在调用vue构造函数的时候 没有传入参数 我们初始化一个空对象
this.$data = options.data || {};
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
// 把data中的成员 转换为getter和setter 注入到vue实例中 方便使用
this._proxyData(this.$data)
// 调用observer对象 监听数据变化
new Observer(this.$data)
// 调用compiler 解析指令和插值表达式
new Compiler(this)
}
_proxyData(data) {//vue传过来的参数 转换为getter和setter
// 遍历data的所有属性
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
// 可遍历 可枚举
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return
} else {
data[key] = newValue;
}
}
})
})
// 把data中的属性 注入到vue实例中
}
}
class Observer {
constructor(data) {
this.walk(data)
}
// 1. 判断数据是否是对象,如果不是对象返回
// 2. 如果是对象,遍历对象的所有属性,设置为 getter/setter
walk(data) {
if (!data || typeof data != 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 定义响应式成员 即对data总的数据实现setter和getter
defineReactive(data, key, val) {
//负责收集依赖 并发布通知
let dep = new Dep()
const that = this
// 如果 val 是对象,继续设置它下面的成员为响应式数据
this.walk(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return val;
},
set(newValue) {
if (val === newValue) {
return
}
// 如果 newValue 是对象,设置 newValue 的成员为响应式
that.walk(newValue)//这里不用this 因为在set方法中 在function的内部 会开启新的作用域 此时的this执行data对象
val = newValue;
// 发布通知
dep.notify()
}
})
}
}
class Compiler {
}
// 要实现数据的响应机制 即数据变化 视图变化
// 在vue的响应机制中 我们要使用观察模式来监听数据的变化
// 因此 在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知 调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知

class Dep {
// 存储所有观察者
constructor() {
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
//数据变化 watcher去更新视图
// 当我们去创建一个watcher对象时 需要把自己添加到自己的主题对象中去
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数 负责更新视图
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target中
Dep.target = this;
// 触发get方法 在get方法中调用addSub
this.oldValue = vm[key]
Dep.target = null;
}
// 当数据发生变化的时候 更新视图
update() {
/* istanbul ignore else */
if (this.lazy) {
// 懒执行时走这里,比如 computed
// 将 dirty 置为 true,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
this.dirty = true
} else if (this.sync) {
// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run
// 方法进行更新
// 这个属性在官方文档中没有出现
this.run()
} else {
// 更新时一般都这里,将 watcher 放入 watcher 队列
queueWatcher(this)
}
}
}
/**
* 将 watcher 放入 watcher 队列
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果 watcher 已经存在,则跳过,不会重复入队
if (has[id] == null) {
// 缓存 watcher.id,用于判断 watcher 是否已经入队
has[id] = true
if (!flushing) {
// 当前没有处于刷新队列状态,watcher 直接入队
queue.push(watcher)
} else {
// 已经在刷新队列了
// 从队列末尾开始倒序遍历,根据当前 watcher.id 找到它大于的 watcher.id 的位置,然后将自己插入到该位置之后的下一个位置
// 即将当前 watcher 放入已排序的队列中,且队列仍是有序的
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
// 直接刷新调度队列
// 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣
flushSchedulerQueue()
return
}
/**
* 熟悉的 nextTick => vm.$nextTick、Vue.nextTick
* 1、将 回调函数(flushSchedulerQueue) 放入 callbacks 数组
* 2、通过 pending 控制向浏览器任务队列中添加 flushCallbacks 函数
*/
nextTick(flushSchedulerQueue)
}
}
}
2.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
const callbacks = []
let pending = false

/**
* 完成两件事:
* 1、用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
* 2、如果 pending 为 false,表示现在浏览器的任务队列中没有 flushCallbacks 函数
* 如果 pending 为 true,则表示浏览器的任务队列中已经被放入了 flushCallbacks 函数,
* 待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
* 浏览器的任务队列了
* pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
* @param {*} cb 接收一个回调函数 => flushSchedulerQueue
* @param {*} ctx 上下文
* @returns
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 用 callbacks 数组存储经过包装的 cb 函数
callbacks.push(() => {
if (cb) {
// 用 try catch 包装回调函数,便于错误捕获
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

/**
* Flush both queues and run the watchers.
* 刷新队列,由 flushCallbacks 函数负责调用,主要做了如下两件事:
* 1、更新 flushing 为 ture,表示正在刷新队列,在此期间往队列中 push 新的 watcher 时需要特殊处理(将其放在队列的合适位置)
* 2、按照队列中的 watcher.id 从小到大排序,保证先创建的 watcher 先执行,也配合 第一步
* 3、遍历 watcher 队列,依次执行 watcher.before、watcher.run,并清除缓存的 watcher
*/
function flushSchedulerQueue () {
/**
* 刷新队列之前先给队列排序(升序),可以保证:
* 1、组件的更新顺序为从父级到子级,因为父组件总是在子组件之前被创建
* 2、一个组件的用户 watcher 在其渲染 watcher 之前被执行,因为用户 watcher 先于 渲染 watcher 创建
* 3、如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 可以被跳过
* 排序以后在刷新队列期间新进来的 watcher 也会按顺序放入队列的合适位置
*/
}
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
// 可以看到 timerFunc 的作用很简单,就是将 flushCallbacks 函数放入浏览器的异步任务队列中
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
// 首选 Promise.resolve().then()
timerFunc = () => {
// 在 微任务队列 中放入 flushCallbacks 函数
p.then(flushCallbacks)
/**
* 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态,
* 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。
* 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
*/
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// MutationObserver 次之
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)) {
// 再就是 setImmediate,它其实已经是一个宏任务了,但仍然比 setTimeout 要好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后没办法,则使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

参考

https://blog.csdn.net/Guolicheng_/article/details/119463976

https://blog.csdn.net/weixin_43097944/article/details/116134903?spm=1001.2101.3001.6650.9&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-9-116134903-blog-107348545.pc_relevant_aa&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-9-116134903-blog-107348545.pc_relevant_aa&utm_relevant_index=10