Vue源码详细解析(五)--batcher:数据变动后的批处理更新dom
Ma63d opened this issue · 3 comments
依赖变动后的dom更新
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
数据变动时触发的notify遍历了所有的watcher,执行器update方法。(删节了shallow update的内容,想了解请看注释)
Watcher.prototype.update = function (shallow) {
if (this.lazy) {
// lazy模式下,标记下当前是脏的就可以了,这是计算属性相关的东西,大家先跳过
this.dirty = true
} else if (this.sync || !config.async) {
// 如果你关闭async模式,即关闭批处理机制,那么所有的数据变动会立即更新到dom上
this.run()
} else {
// 标记这个watcher已经加入批处理队列
this.queued = true
pushWatcher(this)
}
}
我们先忽略lazy和同步模式,真正执行的就是将这个被notify的watcher加入到队列里:
export function pushWatcher (watcher) {
const id = watcher.id
// 如果已经有这个watcher了,就不用加入队列了,这样不管一个数据更新多少次,Vue都只更新一次dom
if (has[id] == null) {
// push watcher into appropriate queue
// 选择合适的队列,对于用户使用$watch方法或者watch选项观察数据的watcher,则要放到userQueue中
// 因为他们的回调在执行过程中可能又触发了其他watcher的更新,所以要分两个队列存放
const q = watcher.user
? userQueue
: queue
// has[id]记录这个watcher在队列中的下标
// 主要是判断是否出现了循环更新:你更新我后我更新你,没完没了了
has[id] = q.length
q.push(watcher)
// queue the flush
if (!waiting) {
//waiting这个flag用于标记是否已经把flushBatcherQueue加入到nextTick任务队列当中了
waiting = true
nextTick(flushBatcherQueue)
}
}
}
pushWatcher把watcher放入队列里之后,又把负责清空队列的flushBatcherQueue放到本轮事件循环结束后执行,nextTick就是vm.$nextTick,利用了MutationObserver,注释里讲述了原理,这里跳过:
function flushBatcherQueue () {
runBatcherQueue(queue)
runBatcherQueue(userQueue)
// user watchers triggered more watchers,
// keep flushing until it depletes
// userQueue在执行时可能又会往指令queue里加入新任务(用户可能又更改了数据使得dom需要更新)
if (queue.length) {
return flushBatcherQueue()
}
// 重设batcher状态,手动重置has,队列等等
resetBatcherState()
}
runBatcherQueue就是对传入的watcher队列进行遍历,对每个watcher执行其run方法。
Watcher.prototype.run = function () {
if (this.active) {
var value = this.get()
// 如果两次数据不相同,则不仅要执行上面的 求值、订阅依赖 ,还要执行下面的 指令update、更新dom
// 如果是相同的,那么则要考虑是否为Deep watchers and watchers on Object/Arrays
// 因为虽然对象引用相同,但是可能内层属性有变动,
// 但是又存在一种特殊情况,如果是对象引用相同,但为浅层更新(this.shallow为true),
// 则一定不可能是内层属性变动的这种情况(因为他们只是_digest引起的watcher"无辜"update),所以不用执行后续操作
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated; but only do so if this is a
// non-shallow update (caused by a vm digest).
((isObject(value) || this.deep) && !this.shallow)
) {
// set new value
var oldValue = this.value
this.value = value
} else {
// this.cb就是watcher构造过程中传入的那个参数,其基本就是指令的update方法
this.cb.call(this.vm, value, oldValue)
}
}
this.queued = this.shallow = false
}
}
可以看到run其实就是先执行了一次this.get(),求出了表达式的最新值,并订阅了可能出现的新依赖,然后执行了this.cb。this.cb是watcher构造函数中传入的第三个形参。
我们回忆一下指令的_bind函数中在用watcher构造函数创造新的watcher的时候传入的参数:
//指令的_bind方法
// 处理一下原本的update函数,加入lock判断
this._update = function (val, oldVal) {
if (!dir._locked) {
dir.update(val, oldVal)
}
}
var watcher = this._watcher = new Watcher(
this.vm,
this.expression,
this._update, // callback
{
filters: this.filters,
twoWay: this.twoWay,
deep: this.deep,
preProcess: preProcess,
postProcess: postProcess,
scope: this._scope
}
)
很简单了,其实就是加入了_locked判断后的指令的update方法(一般指令都是未锁住的)。而我们之前就已经举例讲述过指令的update方法。他完成的就是dom更新的具体操作。
好了,其实批处理就是个很好理解的东西,我把收到notify的watcher存放到一个数组里,在本轮事件循环结束后遍历数组,取出来一个个执行run方法,也即求出新值,订阅新依赖,然后执行对应指令的update的方法,将新值更新作用到dom里。
最后
我已经介绍完了Vue的大体流程,Vue为所有需要绑定到数据的指令都建立了一个watcher,watcher跟指令一一对应,watcher最终又精确的依赖到数据上,即使是数组内嵌对象这样的复杂情况。所以在小量数据更新时,可以做到极其精确、微量的dom更新。
但是这种方式也有其弊端,在大量数组渲染时,一方面需要遍历数据defineReactive,一方面需要将数组元素转为scope(一个既装载了数组元素的内容,又继承了其父级vm实例的对象),另一方面所有需要响应式订阅的dom也肯定是O(n)规模,因此必须要建立O(n)个watcher,执行每个watcher的依赖订阅和求值过程。
上述3个O(n)步骤决定了Vue在启动阶段的性能开销不小,同时,在大数据量的数组替换情况下,新数组的defineReactive,依赖的退订、重订过程,和watcher的对应dom更新也都是O(n)级别。虽然最重的肯定是dom更新部分,但其实前两者也依然会有一定的性能开销。而基于脏检查的Angular而言,其不会有那么多的watcher产生变动,也不会有上述前两个过程,因此会有一定的性能优势。
为了满足大量数组变动的性能需求,track-by的提出就显得很有必要,最大可能的重用原来的数据和依赖,只执行O(data change)级别的defineReactive、依赖的退订、重订、dom更新,所以合理优化和复用情况,Vue就具有了很高的性能。我们熟悉了源码之后可以从内部层面进行分析,而不是对于各个框架的性能了解停留在他们的宣传层面。
后续应该还有3篇左右的文章用来介绍网上资料较少的内容:
- 计算属性部分,即lazy watcher相关内容
- Vue.set和delele中用到的vm._digest(), 即shallow update相关东西
- v-for指令的实现,涉及diff算法
这篇文章非常长(比我本科的毕业论文都长😂),非常感谢你能看完。Vue源码较长,因为作者提供的功能非常多,所以要处理的edge case就很多,而要想深入了解Vue,源码阅读是绕不开的一座大山。源码阅读过程中很多时候不是看不懂js,而是搞不懂作者这么写的目的,我自己模拟多种情况,调试、分析了很多次,消耗较多精力,希望能帮到同样在阅读源码的你。
感谢分享,基本通读了一遍,写的非常赞,感谢,准备自己也来整理一下 Vue 的源码。
最后一段写的很有道理,读源码只能一步步断点跟踪,情景重现,有时候还得自己做 unit test,博主辛苦了。
@monkingxue 其实废话略多哈,但是希望把自己理解的全都说出来,因为中间调试、猜测、模拟作者考虑的具体场景花费了挺多时间,不想这些努力都白费了。
另外,文章只是讲述了原理,想要有提高还是要多到具体的代码当中去看看作者的写法。
坐等更新啊。