vue数据绑定源码

思路分析

数据的双向绑定,就是数据变化了自动更新视图,视图变化了自动更新数据,实际上视图变化更新数据只要通过事件监听就可以实现了,并不是数据双向绑定的关键点。关键还是数据变化了驱动视图自动更新。

所有接下来,我们详细了解下数据如何驱动视图更新的。
数据驱动视图更新的重点就是,如何知道数据更新了,或者说数据更新了要如何主动的告诉我们。可能大家都听过,vue的数据双向绑定原理是Object.defineProperty( )对属性设置一个set/get,是这样的没错,其实get/set只是可以做到对数据的读取进行劫持,就可以让我们知道数据更新了。但是你详细的了解整个过程吗?
先来看张大家都不陌生的图:

vue数据绑定源码

  • Observe 类劫持监听所有属性,主要给响应式对象的属性添加 getter/setter 用于依赖收集与派发更新
  • Dep 类用于收集当前响应式对象的依赖关系
  • Watcher 类是观察者,实例分为渲染 watcher、计算属性 watcher、侦听器 watcher三种

介绍数据驱动更新之前,先介绍下面4个类和方法,然后从数据的入口initState开始按顺序介绍,以下类和方法是如何协作,达到数据驱动更新的。

defineReactive

这个方法,用处可就大了。

我们看到他是给对象的键值添加get/set方法,也就是对属性的取值和赋值都加了拦截,同时用闭包给每个属性都保存了一个Dep对象。

当读取该值的时候,就把当前这个watcherDep.target)添加进他的dep里的观察者列表,这个watcher也会把这个Dep添加进他的依赖列表。
当给设置值的时候,就让这个闭包保存的Dep去通知他的观察者列表的每一个watcher

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

Observer

什么是可观察者对象呢?

简单来说:就是数据变更时可以通知所有观察他的观察者。

1、取值的时候,能把要取值的watcher(观察者对象)加入它的dep(依赖,也可叫观察者管理器)管理的subs列表里(即观察者列表);

2、设置值的时候,有了变化,所有依赖于它的对象(即它的dep里收集到的观察者watcher)都得到通知。

这个类功能就是把数据转化成可观察对象。针对Object类型就调用defineReactive方法循环把每一个键值都转化。针对Array,首先是对Array经过特殊处理,使它可以监控到数组发生了变化,然后对数组的每一项递归调用Observer进行转化。

对于Array是如何处理的呢?这个放在下面单独说。

export class Observer {

  /**
   *如果是对象就循环把对象的每一个键值都转化成可观察者对象
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 如果是数组就对数组的每一项做转化
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Dep

这个类功能简单来说就是管理数据的观察者的。当有观察者读取数据时,保存观察者到subs,以便当数据变化了的时候,可以通知所有的观察者去update,也可以删除subs里的某个观察者。

export default class Dep {
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
    
  // 这个方法非常绕,Dep.target就是一个Watcher对象,Watcher把这个依赖加进他的依赖列表里,然后调用dep.addSub再把这个Watcher加入到他的观察者列表里。
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Watcher

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // 省去了初始化各种属性和option
    this.dirty = this.lazy // for lazy watchers
    // 解析expOrFn,赋值给this.getter
    // expOrFn也要明白他是什么?
    // 当是渲染watcher时,expOrFn是updateComponent,即重新渲染执行render
    // 当是计算watcher时,expOrFn是计算属性的计算方法
    // 当是侦听器watcher时,expOrFn是watch属性的取值表达式,可以去读取要watch的数据,this.cb就是watch的handler属性
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * 执行this.getter,同时重新进行依赖收集
   */
  get () {
    pushTarget(this)
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    if (this.deep) {
      // 对于deep的watch属性,处理的很巧妙,traverse就是去递归读取value的值,
      // 就会调用他们的get方法,进行了依赖收集
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

  /**
   * 不重复的把当前watcher添加进依赖的观察者列表里
   */
  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)
      }
    }
  }

  /**
   * 清理依赖列表:当前的依赖列表和新的依赖列表比对,存在于this.deps里面,
   * 却不存在于this.newDeps里面,说明这个watcher已经不再观察这个依赖了,所以
   * 要让个依赖从他的观察者列表里删除自己,以免造成不必要的watcher更新。然后
   * 把this.newDeps的值赋给this.deps,再把this.newDeps清空
   */
  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
   */
  update () {
    if (this.lazy) {
      // 对于计算watcher时,不需要立即执行计算方法,只要设置dirty,意味着
      // 数据不是最新的了,使用时需要重新计算
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 调度watcher执行计算。
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
          this.cb.call(this.vm, value, oldValue)
      }
    }
  }

  /**
   * 对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
   * 说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * 把这个watcher所观察的所有依赖都传给Dep.target,即给Dep.target收集
   * 这些依赖。
   * 举个例子:具体可以看state.js里的createComputedGetter这个方法
   * 当render里依赖了计算属性a,当渲染watcher在执行render时就会去
   * 读取a,而a会去重新计算,计算完了渲染watcher出栈,赋值给Dep.target
   * 然后执行watcher.depend,就是把这个计算watcher的所有依赖也加入给渲染watcher
   * 这样,即使data.b没有被直接用在render上,也通过计算属性a被间接的是用了
   * 当data.b发生改变时,也就可以触发渲染更新了
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

综上所述,就是vue数据驱动更新的方法了,下面是对整个过程的简单概述:
每个vue实例组件都有相应的watcher对象,这个watcher是负责更新渲染的。他会在组件渲染过程中,把属性记录为依赖,也就是说,她在渲染的时候就把所有渲染用到的prop和data都添加进watcher的依赖列表里,只有用到的才加入。同时把这个watcher加入进data的依赖的订阅者列表里。也就是watcher保存了它都依赖了谁,data的依赖里保存了都谁订阅了它。这样data在改变时,就可以通知他的所有观察者进行更新了。渲染的watcher触发的更新就是重新渲染,后续的事情就是render生成虚拟DOM树,进行diff比对,将不同反应到真实的DOM中。

queueWatcher

下面是Watcher的update方法,可以看的除了是计算属性和标记了是同步的情况以外,全部都是推入观察者队列中,下一个tick时调用。也就是数据变化不是立即就去更新的,而是异步批量去更新的。

update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

下面来看看queueWatcher方法

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

这里使用了一个 has 的哈希map用来检查是否当前watcher的id是否存在,若已存在则跳过,不存在则就push到queue,队列中并标记哈希表has,用于下次检验,防止重复添加。因为执行更新队列时,是每个watcher都被执行run,如果是相同的watcher没必要重复执行,这样就算同步修改了一百次视图中用到的data,异步更新计算的时候也只会更新最后一次修改。

nextTick(flushSchedulerQueue)把回调方法flushSchedulerQueue传递给nextTick,一次异步更新,只要传递一次异步回调函数就可以了,在这个异步回调里统一批量的处理queue中的watcher,进行更新。

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  resetSchedulerState()

}

每次执行异步回调更新,就是循环执行队列里的watcher.run方法。

在循环队列之前对队列进行了一次排序:

  • 组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
  • 一个组件的user watchers(侦听器watcher)比render watcher先运行,因为user watchers往往比render watcher更早创建
  • 如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过

nextTick

export function nextTick (cb?: Function, ctx?: Object) {
// 这个方法里,我把关于不写回调,使用promise的情况处理去掉了,把trycatch都去掉了。
  callbacks.push(() => {
     cb.call(ctx)
  })
  if (!pending) {
    pending = true
    setTimeout(flushCallbacks, 0) // 异步任务进行了简化
  }
}

下面是异步的回调方法flushCallbacks,遍历执行callbacks里的方法,也就是遍历执行调用nextTick时传入的回调方法。

你可能就要问了,queueWatcher的时候不是控制了只会调用一次nextTick吗,为啥要用callbacks数组来存储呢。举个例子:

你写了一堆同步语句,改变了data等,然后又调用了一个this.$nextTick来做个异步回调,这个时候不就又会向callbacks数组里push了一个回调方法吗。

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

如何把数组处理成可观察对象

不考虑兼容处理

本质就是改写数组的原型方法。当数组调用methodsToPatch这些方法时,就意味者数组发生了变化,需要通知所有观察者update。

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // 保存数组的原始原型方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

后记
关于从数据入口initState开始解析的部分,写在一篇里篇幅太大,我放在下一篇文章了,记得去读哦,可以加深理解。

参考文章

剖析Vue实现原理 - 如何实现双向绑定mvvm

vue.js源码解读系列 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定

Vue源码学习笔记之Dep和Watcher

watcher调度原理

Vue源码阅读 - 依赖收集原理

相关推荐