Vue 源码剖析 —— 对象变化侦测

当应用在运行时,内部状态是会不断变化的。而对于 web 应用而言这会直接致使页面不停的从新渲染。那么如何经过状态变化肯定具体要从新渲染哪一个部分呢?在 MVVM 框架出现以前,大多数时候都须要手动去建立并维护数据与显示层的联系,随着应用的复杂度提升,内部状态和 UI 的联系变得错综复杂,难以维护。前端 MVVM 的框架正是经过编写一个通用的 ViewModel 层,负责让 Model 层的变化自动同步到 View 层,还负责让 View 层的修改同步回 Model。今天咱们一块儿来剖析一下,当应用的内部状态改变时,Vue.js 是怎么作到侦测到变化的。前端

Vue.js 的变化侦测与 React 不一样,对 React而言,当状态发生变化时,它并不知道具体哪一个状态变化了,只知道状态有可能变了,而后发送信号给框架,框架内部收到信号后,会进行暴力比对找出来那些DOM节点须要从新渲染。对 Vue.js 而言,当状态发生改变时,它马上就知道了,并且必定程度上知道具体哪些状态改变了,且若是一个状态上绑定了多个依赖,当状态改变时,会向全部绑定的依赖发送通知。可是,这种粒度更细也是要付出必定代价的,每一个状态绑定的依赖越多,依赖跟踪在内存上的消耗就更大,这种状况在 Vue.js 2.0 引入虚拟 DOM 以后改善了不少。web

问题一:如何侦听一个对象的变化

在 JS 中,咱们侦听对象变化的手段无非两种:Object.defineProperty 和 ES6 的 Proxy。因为 ES6 的支持状况不理想,Vue.js 2.0 中采用的是第一种方法,但在新版本中应该会放弃 Object.defineProperty 选择 Proxy。由于 Object.defineProperty 是存在明显缺陷的,后文会提到。首先咱们能够采用下面的函数来封装 Object.definePropertybash

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      return val
    },
    set: function(newVal) {
      if (val === newVal) return
      val = newVal
    }
  })
}
复制代码

此时,思考一下,要观察数据的真正目的是什么?框架

目的就是当数据变化时,能够通知那些曾经使用过该数据的地方。因此咱们须要先收集依赖,这样当数据变化时在去通知这些依赖。显而易见,能够在 getter 中收集依赖,在 setter 中触发依赖。函数

问题二:依赖收集在哪里

首先咱们能够封装一个通用的依赖类,在 Vue.js 中是 Dep 类:学习

class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (somethingToWatch) {
      this.addSub(somethingToWatch)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let sub of subs) {
      sub.update()
    }
  }
}

function remove (arr, items) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
复制代码

接着改造一下 defineReactiveui

function defineReactive(data, key, val) {
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend()
      return val
    },
    set: function(newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify()
    }
  })
}
复制代码

问题三:依赖是谁

在上面的 Dep 类中出现了 somethingToWatch,显然它正是咱们在数据变化以后须要通知的对象。在 Vue.js 中,咱们通知用到数据的地方有不少,好比模板中,或是自定义的一个 watch 。因此此时须要一个抽象的类来覆盖这些状况,Vue.js 中这个类为 Watcherthis

class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  get () {
    somethingToWatch = this
    let value = this.getter.call(this.vm, this.vm)
    somethingToWatch = undefined
    return value
  }

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

在这段代码中,当 Watcher 初始化时,会调用 get 方法,而在 get 方法中,咱们将 somethingToWatch 指向了当前的 Watcher 实例,当咱们在获取 value 值的时候又会触发数据的 getter,从而自动将 Watcher 实例添加到 Dep 中。当数据变化时,Dep 会触发依赖列表中全部依赖的 update 方法,也就是 Watcher 中的 update 方法,Watcher 中的 update 方法。spa

能够看一个 vm.$watch('a.b.c', (oldVal, newVal) => {}) 的例子,当 a.b.c 变化时,要调用后面的回调函数。首先,要解析 a.b.c,在 Vue.js 中用 parsePath 来完成:.net

const bailRE = /^\w+.$/
function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let segment of segments) {
      if (!obj) return
      obj = obj[segment]
    }
  }
  return obj
}
复制代码

至此,咱们就拿到了 a.b.c 这个属性,且在 Watcherget 方法中访问了它,触发了它的 getter,从而将当前 Watcher 实例添加到 a.b.c 的依赖列表里。且当 a.b.c 发生变化时,回调函数将会在 Watcher 中的 update 方法里被调用。

问题四:怎么侦测全部 key

能够看到使用 Object.defineProperty 能够侦测到对象的某个属性值变化,可是咱们须要侦听全部属性值(包括子属性)的变化。如今开始封装 Observer 类来实现这一目的:

/**
 * Observer 类会被附加到每个被侦测的 object上。
 * 一旦加上,会将 object 全部的属性都转化为 getter/setter 的形式
 * 来收集属性依赖,而且在属性变化时通知这些依赖
 */
class Observer {
  constructor(value) {
    this.value = value

    if (!Array.isArray(value)) {
      this.walk(value)
    }
  }

  /**
   * walk 将每个属性都转为 getter/setter
   */
  walk (obj) {
    const keys = Object.keys(obj)
    for (let key of keys) {
      defineReactive(obj, key, obj[key])
    }
  }
}

function defineReactive(data, key, val) {
  // 新增,用于递归子属性
  if (typeof val == 'object') {
    new Observer(val)
  }
  ...
}
复制代码

经过定义了 Observer 类,咱们将一个正常的 object 转换为了被侦测的 object。而后判断数据类型,只有 Object 类型的数据才会调用 walk 方法将每个属性都变为 getter/setter 模式。而改造后的 defineReactive 加上了一段新代码用于判断当子属性为 Object 时,对子属性调用 new Observer(val),从而造成递归。这样咱们就把全部的属性都变为 getter/setter 的形式了。

问题五:Object.defineProperty 带来的隐藏问题

思考一个场景,当咱们在一个 Vue 实例中,定义 data: { a: {} },又定义了一个方法 action () { this.a.name = 'jay' },若是调用了 action 方法,能不能侦听到对象 a 的改变呢? 答案是否认的,因为在初始化过程当中, a 并无 name 这个属性,也就是说在 walk 方法中,咱们没有将 name 属性变为 getter/setter 模式,因此没法侦测到这个变化,也不会向依赖发送通知。

再好比,咱们在 action 中删除某个已经存在的属性值,Object.defineProperty 只能判断一个数据是否被修改,故一样也是没法侦测到变化的。要解决这两个问题,咱们能够调用 vm.set* 和 *vm.delete 这两个API。

Object 的变化侦测过程梳理

变化侦测

Data 经过 Observe 转换为 getter/setter 形式来追踪变化。当外界经过 Watcher 读取数据是,会触发 getter 从而将 Watcher 添加到依赖中。当数据发生变化时,会触发 setter,从而向 Dep 中的依赖发送通知。Watcher 收到通知后,会向外界发送通知,外界收到通知后,可能会触发视图更新,也可能触发用户的回调函数。

本系列文章均是深刻浅出 Vue.js的学习笔记,有兴趣的小伙伴能够去看书哈。

相关文章
相关标签/搜索