那么Vue中data数据改变后会触发几次render呢

那么Vue中data数据改变后会触发几次render呢

2021年01月05日

粗略看过vue2的部分源码之后,尝试自己写个简单版出来。结果发现自己实现的版本里边一次data修改触发不止一次render

这里说的render指的是生成新的虚拟dom, 并与先前的虚拟dom进行diff操作,将得到的差异部分做增量的dom更新。

这是之前写的部分代码,嗯完整的点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 实例化一个Watcher对象,收集data和computedData相关的所有数据依赖
// 一旦有变动,则触发调用vm.render
vm.renderWatcher = new Watcher(
vm,
(() => {
// 简单粗暴但是有问题
// 比如data变动触发diff重绘和computedData的重新计算
// computedData可能还有变动,然后又一次触发diff重绘
walk(data); // 递归遍历data的所有属性
walk(computedData);
}),
vm.render,
);

显然如果是在vue中应该只触发一次,不然就是一个待优化项,来看看vue是怎么做的。

vue2中lifecycle.js的190到200行点我去Github上看Vue2相应源码
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

这里称这个new Watcher产生的实例为renderWatcher

updateComponent第一次执行完成了组件的渲染和挂载, 然后也顺便完成了对组件的renderWatcher的依赖收集,此后如果数据发生变化,则触发renderWatcher再次通过自己的getter方法(即updateComponent)来获取新的结果值。 于是达到了更新视图的目的。

Watcher实例化时接收的第二个参数为getter方法, 一般情况下会在实例化的同时执行一次getter来获取目标值,并且收集之前由 observe方法“种”下的依赖。当之后依赖的值发生变化,则触发Watcher实例重新获取目标值。

于是改完之后代码变成了这样

1
2
// 重绘触发
vm.renderWatcher = new Watcher(vm, vm.render, () => 1);

新的问题

实际上如果只是这样的话还是差点意思
举个例子, 给一个按钮绑定一个点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
export default {
data(){
return {
btnClickCount: 0,
otherData: '',
}
},
methods:{
onBtnClick(){
this.btnClickCount = this.btnClickCount + 1;
this.otherData = this.otherData + 'jkhas';
},
}
};

新的问题是:
点击按钮之后会触发几次render呢?
在vue中的话,那自然是触发一次render了。

需要说明的是,vue2是通过设置对象属性的getter来触发数据的依赖收集,通过设置对象属性的setter来触发数据的变化通知。
这里说setter被调用时触发的变化通知,它通知的是依赖当前被修改数据的其他数据(比如computed属性),或者是通知组件重新生成虚拟dom然后diff然后重新render。
在方法onBtnClick中有两项数据修改,那么就会触发两次变化,但是实际上只会触发一次render。

为了达成这个结果, vue2中是这么做的

根据上文分析的部分源码,已经知道方法onBtnClick执行后, 属性btnClickCountotherData都发生了变化, 于是触发了两次renderWatcher实例的更新。从代码执行层面,则是会调用两次renderWatcher.update

这里来看Watcher.update的源码

vue2中watcher.js的160到173行点我去Github上看Vue2相应源码
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

结合上文中renderWatcher实例化过程中的传参以及Watcher的构造函数,可以确定renderWatcherlazysync属性均为false;
于是可知queueWatcher(renderWatcher)被执行了两次。

然后接着去找queueWatcher的实现

vue2中scheduler.js的130到156行点我去Github上看Vue2相应源码
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
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
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) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

明显可以这里做了去重处理(watcherId全局唯一),看出如果连续两次执行queueWatcher(renderWatcher),起作用的只有第一次。
这里只是将renderWatcher排到了将来要处理的队列里边。
具体处理的时机则是nextTick之后。
而vue2的nextTick中安排的flushSchedulerQueue则会在onBtnClick方法执行,所有相关的watcher实例被放进队列之后再开始执行。

顺便一提,flushSchedulerQueue中处理每个watcher之前会将当前队列中的所有watcher按照id排个序,id最大的排在最后。
而这个id最大的Watcher实例, 就是renderWatcher, 因为它是最后一个实例化的Watcher,于是在computed等其他属性更新完毕之后,最后调用一次updateComponent, 即对应文章开头所说的render。