Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue源码详细解析(五)--batcher:数据变动后的批处理更新dom #5

Open
Ma63d opened this issue Feb 27, 2017 · 3 comments
Open

Comments

@Ma63d
Copy link
Owner

Ma63d commented Feb 27, 2017

依赖变动后的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,而是搞不懂作者这么写的目的,我自己模拟多种情况,调试、分析了很多次,消耗较多精力,希望能帮到同样在阅读源码的你。

@Ma63d Ma63d changed the title Vue源码详细解析(四)--batcher:数据变动后的批处理更新dom Vue源码详细解析(五)--batcher:数据变动后的批处理更新dom Feb 27, 2017
@monkingxue
Copy link

感谢分享,基本通读了一遍,写的非常赞,感谢,准备自己也来整理一下 Vue 的源码。
最后一段写的很有道理,读源码只能一步步断点跟踪,情景重现,有时候还得自己做 unit test,博主辛苦了。

@Ma63d
Copy link
Owner Author

Ma63d commented Mar 26, 2017

@monkingxue 其实废话略多哈,但是希望把自己理解的全都说出来,因为中间调试、猜测、模拟作者考虑的具体场景花费了挺多时间,不想这些努力都白费了。
另外,文章只是讲述了原理,想要有提高还是要多到具体的代码当中去看看作者的写法。

@Pines-Cheng
Copy link

坐等更新啊。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants