Vue数据绑定原理之依赖收集触发

在上一篇咱们讲到了数据劫持,和数据观测。那么怎么将数据和相关的DOM关联起来呢?本篇咱们将解开这个过程。html

从实例化Watcher开始

上一篇讲解中咱们知道Watcher是实际执行数据变动以后操做的主要对象,咱们先找到它的实例化路径,发现它是在mount的时候进行的操做。vue

Vue -> this._init -> initLifecycle -> mountComponent
复制代码

咱们在这个方法中找到了关于Watcher的实例化代码node

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
复制代码

结合以前的Watcher构造函数:bash

class Watcher {
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
  }
}
复制代码

咱们先解释下参数:首先传入了组件实例vm,而后是expOrFn传入的是updateComponentcb是一个空函数noopoptions中定义了一个钩子before,最后传入了isRenderWatchertrue,代表这是一个RenderWatcher,就会将该Wathcer挂载到组件上。dom

而这里关键的地方就是updateComponent。咱们在上一篇分析中提到,当数据变动,依赖会通知全部订阅者Watcher作出相应更新,也就是watcher.update,而watcher.update不论是同步仍是异步,其核心是调用wathcer.run去执行相关操做。异步

run () {
  if (this.active) {
    const value = this.get()
    
    // ...
    this.cb.call(this.vm, value, oldValue)
  }
}
复制代码

这个函数有两个关键的地方,一个是获取值,调用了get方法,而另外一个就是执行回调函数cb,在上一篇咱们一样提到,咱们自定义的watch就是经过传入expcb来实现观测具体某个属性的。ide

好比:函数

new Vue({
  data: {
    msg: ''
  },
  watch: {
    msg: function() {}
  }
})
复制代码

这里的watch就是经过new Watcher(vm, 'msg', fn)相似这样的方式定义的,这和咱们如今看见的彻底不同。oop

这也是困惑的一点,咱们如今看到的RenderWatcher传入了一个空函数做为cb,也就是说执行cb是没有任何做用的,那么在数据更新时是怎么通知到视图层的呢?咱们发如今run方法中,除了执行cb外,还执行了get方法。这就是关键!性能

在上一篇中咱们提到get方法其实调用的就是getter,在传入的第二个参数expOrFn类型为function时,getter = expOrFn。那还记得传了什么进去吗?updateComponent

咱们理一下思路,并暂时移除掉无关代码:

// function mountComponent
new Watcher(vm, updateComponent, noop)

// class Watcher
class Watcher {
  constructor(vm, fn, cb) {
    this.vm = vm
    this.getter = fn

    this.value = this.get()
  }
  get() {
    this.getter.call(this.vm, this.vm) 
  }
  update() {
    this.run()
  }
  run() {
    const value = this.get()
    
    // ...
    this.cb.call(this.vm, value, this.value)
  }
}
复制代码

这里有几个要点。第一,在初始化Watcher的时候就调用过一次get;第二,在数据更改触发更新时又会调用get。再根据实际执行的函数名updateComponent,我想你也猜到了,这个函数就是用来渲染DOM的,而且在每次观测到数据变动时都会从新渲染DOM。

再来看这张图,至此,WatcherRender的路径咱们也清晰了。

data.png

updateComponent

咱们猜想该函数是用来更新DOM的,但咱们仍是得实际看一下它是如何实现的,由于这里面其实涉及到了更多技术,十分值得学习。

那咱们仍是一步一步的来,看完相关代码,能够总结出:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码

它最主要就是调用了两个函数,_render_update

_render

咱们先来看看_render,它是经过renderMixin加在原型上的,因此相关定义会在不一样的地方。

Vue.prototype._render = function (): VNode {}
复制代码

咱们先看下这个函数声明,其返回值是一个VNode类型,若是你有仔细读过官方文档,你就会对这个词有点印象。

在建立一个Vue组件的时候咱们能够不使用template选项来写DOM模板,而使用render选项。而render函数返回值的类型就是VNode。很显然,从函数名上来看,内部的_render是对传入render的二次包装。

看一看源码归纳:

Vue.prototype._render = function (): VNode {
  const { render, _parentVnode } = vm.$options
  // ...
  vnode = render.call(vm._renderProxy, vm.$createElement)
  // ...
  return vnode
}
复制代码

该函数调用了render并返回了VNode。 这里发什么什么?仅仅是调用render函数这么简单吗? 咱们来看看好比下面这个render函数:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}
复制代码

他用到了this.blogTitle,很明显这里是访问属性,也就是会调用到该属性的get方法,上一篇咱们再讲Observer时讲过,属性的get里面会进行依赖收集。此时,blogTitle有了新的订阅者subs.push(Watcher),而该Watcher的依赖deps也增长了blogTitle,在blogTitle更新时,就会调用该Watcherupdate方法。

因此上面那张图中的renderdata这条线也清晰了吧,这也就是官方文档上说的接触(touched)!

_update

OK,其实到这里,整个数据劫持,依赖收集过程都已经很明了了,咱们已经能够实现一个简单而且优雅的数据单向绑定了。接下来就是Vue怎么优化DOM渲染,提高性能的操做了。

咱们如今知道_render是建立虚拟DOM的,那么建立完虚拟DOM以后干吗?固然是渲染成真实DOM啊!这也就是_update的做用,那为何它叫作update而不是create或者transform呢?这也是有知识点在里面的。

// Vue.prototype._update 
const prevVnode = vm._vnode

if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}
复制代码

能看见清晰的注释,initial render/updates,也就是该方法处理了新建和更新两种操做。新建的时候会将VNode挂载在vm上表示已经建立过了,以后只须要更新就好了,减小消耗。

而这里又用到了另外一个方法__patch__

// runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop

// runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

// vdom/patch.js
export function createPatchFunction (backend) {
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //...
  }
}
复制代码

介于这里内容比较复杂,暂时就不讲了,咱们留着下一篇再见。

相关文章
相关标签/搜索