Vue源码学习之双向绑定

(注:此篇博客主要讨论Watcher,Dep,Observer的实现)

原理

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter。Object.defineProperty 是 ES5 中一个没法 shim 的特性,这也就是为何 Vue 不支持 IE8 以及更低版本浏览器。

上面那段话是Vue官方文档中截取的,能够看到是使用Object.defineProperty实现对数据改变的监听。Vue主要使用了观察者模式来实现数据与视图的双向绑定。
javascript

function initData(vm) { //将data上数据复制到_data并遍历全部属性添加代理
  vm._data = vm.$options.data;
  const keys = Object.keys(vm._data); 
  let i = keys.length;
  while(i--) {  
    const key = keys[i];
    proxy(vm, `_data`, key);
  }
  observe(data, true /* asRootData */) //对data进行监听
}
复制代码

在第一篇数据初始化中,执行new Vue()操做后会执行initData()去初始化用户传入的data,最后一步操做就是为data添加响应式。
html

实现

在Vue内部存在三个对象:Observer、Dep、Watcher,这也是实现响应式的核心。
vue

Observer

Observer对象将data中全部的属性转为getter/setter形式,如下是简化版代码,详细代码请看这里
java

export function observe (value) {
  //递归子属性时的判断
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  ...
  ob = new Observer(value)
}
export class Observer {
  constructor (value) {
    ... //此处省略对数组的处理
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) //为每一个属性建立setter/getter
    }
  }
  ...
}

//设置set/get
export function defineReactive ( obj: Object, key: string, val: any ) {
  //利用闭包存储每一个属性关联的watcher队列,当setter触发时依然能访问到
  const dep = new Dep()
  ...
  //若是属性为对象也建立相应observer
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      if (Dep.target) {
        dep.depend() //将当前dep传到对应watcher中再执行watcher.addDep将watcher添加到当前dep.subs中
        if (childOb) {  //若是属性是对象则继续收集依赖
          childOb.dep.depend()
          ...
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      childOb = observe(newVal) //若是设置的新值是对象,则为其建立observe
      dep.notify() //通知队列中的watcher进行更新
    }
  })
}
复制代码

建立Observer对象时,为data的每一个属性都执行了一遍defineReactive方法,若是当前属性为对象,则经过递归进行深度遍历。该方法中建立了一个Dep实例,每个属性都有一个与之对应的dep,存储全部的依赖。而后为属性设置setter/getter,在getter时收集依赖,setter时派发更新。这里收集依赖不直接使用addSub是为了能让Watcher建立时自动将本身添加到dep.subs中,这样只有当数据被访问时才会进行依赖收集,能够避免一些没必要要的依赖收集。
react

Dep

Dep就是一个发布者,负责收集依赖,当数据更新是去通知订阅者(watcher)。源码地址
git

export default class Dep {
  static target: ?Watcher; //指向当前watcher
  constructor () {
    this.subs = []
  }
  //添加watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //移除watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //经过watcher将自身添加到dep中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  //派发更新信息
  notify () {
    ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

Watcher

源码地址
github

//解析表达式(a.b),返回一个函数
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]    //遍历获得表达式所表明的属性
    }
    return obj
  }
}
export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    } 
    //对建立的watcher进行收集,destroy时对这些watcher进行销毁
    vm._watchers.push(this)
    // options
    if (options) {
      ...
      this.before = options.before
    }
    ...
    //上一轮收集的依赖集合Dep以及对应的id
    this.deps = []
    this.depIds = new Set()
    //新收集的依赖集合Dep以及对应的id
    this.newDeps = []
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      ...
    }
    ...
    this.value = this.get()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps() //清空上一轮的依赖
    }
    return value
  }

  /** * Add a dependency to this directive. */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) { //同一个数据只收集一次
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  //每轮收集结束后去除掉上轮收集中不须要跟踪的依赖
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  },
  update () {
    ...
    //通过一些优化处理后,最终执行this.get
    this.get();
  }
  // ...
}
复制代码

依赖收集的触发是在执行render以前,会建立一个渲染Watcher:
express

updateComponent = () => {
  vm._update(vm._render(), hydrating) //执行render生成VNode并更新dom
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
复制代码

渲染Watcher建立时会将Dep.target指向自身并触发updateComponent也就是执行_render生成VNode并执行_updateVNode渲染成真实DOM,在render过程当中会对模板进行编译,此时就会对data进行访问从而触发getter,因为此时Dep.target已经指向了渲染Watcher,接着渲染Watcher会执行自身的addDep,作一些去重判断而后执行dep.addSub(this)将自身push到属性对应的dep.subs中,同一个属性只会被添加一次,表示数据在当前Watcher中被引用。数组

当_render结束后,会执行popTarget(),将当前Dep.target回退到上一轮的指,最终又回到了null,也就是全部收集已完毕。以后执行cleanupDeps()将上一轮不须要的依赖清除。当数据变化是,触发setter,执行对应Watcher的update属性,去执行get方法又从新将Dep.target指向当前执行的Watcher触发该Watcher的更新。浏览器

这里能够看到有deps,newDeps两个依赖表,也就是上一轮的依赖和最新的依赖,这两个依赖表主要是用来作依赖清除的。但在addDep中能够看到if (!this.newDepIds.has(id))已经对收集的依赖进行了惟一性判断,不收集重复的数据依赖。为什么又要在cleanupDeps中再做一次判断呢?

while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
复制代码

cleanupDeps中主要清除上一轮中的依赖在新一轮中没有从新收集的,也就是数据刷新后某些数据再也不被渲染出来了,例如:

<body> <div id="app"> <div v-if='flag'> </div> <div v-else> </div> <button @click="msg1 += '1'">change</button> <button @click="flag = !flag">toggle</button> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { flag: true, msg1: 'msg1', msg2: 'msg2' } }) </script> </body> 复制代码

每次点击change,msg1都会拼接一个1,此时就会触发从新渲染。当咱们点击toggle时,因为flag改变,msg1再也不被渲染,但当咱们点击change时,msg1发生了变化,但却没有触发从新渲染,这就是cleanupDeps起的做用。若是去除掉cleanupDeps这个步骤,只是能防止添加相同的依赖,可是数据每次更新都会触发从新渲染,又去从新收集依赖。这个例子中,toggle后,从新收集的依赖中并无msg1,由于它不须要被显示,可是因为设置了setter,此时去改变msg1依然会触发setter,若是没有执行cleanupDeps,那么msg1的依赖依然存在依赖表里,又会去触发从新渲染,这是不合理的,因此须要每次依赖收集完毕后清除掉一些不须要的依赖。

总结

依赖收集其实就是收集每一个数据被哪些Watcher(渲染Watcher、computedWatcher等)所引用,当这些数据更新时,就去通知依赖它的Watcher去更新。

相关文章
相关标签/搜索