avatar
原创2019/03/18

Vue2 响应式原理

AI 总结

深度解析 Vue 2.x 基于 Object.defineProperty 实现的响应式系统,从 Observer、Dep、Watcher 三者关系入手,逐步拆解数据劫持、依赖收集与派发更新的完整流程,同时分析数组响应式的特殊处理方式,是理解 Vue 响应式原理的完整指南。

响应式原理

vue 2.0中,是基于 Object.defineProperty实现的响应式系统。

Object.defineProperty(obj, prop, descriptor)
descriptor: -configurable - enumerable - value - writable - get - set

主要涉及属性:

  • enumerable,属性是否可枚举,默认 false。
  • configurable,属性是否可以被修改或者删除,默认 false。
  • get,获取属性的方法。
  • set,设置属性的方法。

响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化 vue 实例的时候,对 data、props等对象的每一个属性都通过 Object.defineProperty 定义一次,在数据被 set 的时候,做一些操作,改变相应的视图。

class Vue {
  /* Vue构造类 */
  constructor(options) {
    this._data = options.data
    observer(this._data)
  }
}
function observer(value) {
  if (!value || typeof value !== 'object') {
    return
  }
  Object.keys(value).forEach((key) => {
    defineReactive(value, key, value[key])
  })
}
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true /* 属性可枚举 */,
    configurable: true /* 属性可被修改或删除 */,
    get: function reactiveGetter() {
      return val
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return
      cb(newVal)
    }
  })
}

实际应用中,各种系统复杂无比。假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。又或者写在 data 中的数据并没有应用到视图中呢,这个时候去更新视图就是多余的了。这就需要依赖收集的过程。

依赖收集

所谓依赖收集,就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作。“发布者”在 Vue中基本模式如下:

初始化时 Vue 去遍历 data的key,在defineReactive函数中对每个key进行get和set的劫持,Dep是一个新的概念,它主要用来做上面所说的 dep.depend()去收集当前正在运行的渲染函数和dep.notify() 触发渲染函数重新执行。

export default class Dep {
  static target: ?Watcher
  id: number
  subs: Array<Watcher>
 
  constructor() {
    this.id = uid++
    this.subs = []
  }
 
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }
 
  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  } //依赖收集,有需要才添加订阅
  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的实现。源码 Watcher逻辑比较多,简化后的模型如下

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把获取过的数据记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    //传进来的对象 例如Vue
    this.vm = vm
    //在Vue中cb是更新视图的核心,调用diff并更新视图的过程
    this.cb = cb
    //收集Deps,用于移除监听
    this.newDeps = []
    this.getter = expOrFn
    //设置Dep.target的值,依赖收集时的watcher对象
    this.value = this.get()
  }
 
  get() {
    //设置Dep.target值,用以依赖收集
    pushTarget(this)
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    return value
  }
 
  //添加依赖
  addDep(dep) {
    // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
    this.newDeps.push(dep)
    dep.addSub(this)
  }
 
  //更新
  update() {
    this.run()
  }
 
  //更新视图
  run() {
    //这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
    console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
  }
}

defineReactive 详细逻辑

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
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
 
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
 
    // 初始渲染时页面显示会调用get方法
    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
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

所以响应式原理就是,我们通过递归遍历,把 vue实例中data里面定义的数据,用defineReactive重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被读取的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被写入的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

Watcher 的产生

在 vue 中,共有 4 种情况会产生Watcher:

  1. Vue实例对象上的 watcher , 观测根数据,发生变化时重新渲染组件

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    vm._watcher = new Watcher(vm, updateComponent, noop)
  2. 在 vue 对象内用 watch 属性创建的 watcher

  3. 在 vue 对象内创建的计算属性,本质上也是watcher

  4. 使用 vm.$watch创建的watcher

Wathcer会增减,也可能在 render 的时候新增。所以,必须有一个Schedule来进行 Watcher 的调度。部分主要代码如下:

queue.sort((a, b) => a.id - b.id)
 
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
  watcher = queue[index]
  if (watcher.before) {
    watcher.before()
  }
  id = watcher.id
  has[id] = null
  watcher.run()
  // in dev build, check and stop circular updates.
  if (process.env.NODE_ENV !== 'production' && has[id] != null) {
    circular[id] = (circular[id] || 0) + 1
    if (circular[id] > MAX_UPDATE_COUNT) {
      warn(
        'You may have an infinite update loop ' +
          (watcher.user
            ? `in watcher with expression "${watcher.expression}"`
            : `in a component render function.`),
        watcher.vm
      )
      break
    }
  }
}

Schedule 调度的作用:

  • 去重,每个Watcher有一个唯一的 id。首先,如果 id 已经在队列里了,跳过,没必要重复执行,如果 id 不在队列里,要看队列是否正在执行中。如果不在执行中,则在下一个时间片执行队列,因此队列永远是异步执行的。

  • 排序,按解析渲染的先后顺序执行,即 Watcher小的先执行。Watcher里面的 id 是自增的,先创建的id比后创建的id小。所以会有如下规律:

    • 组件是允许嵌套的,而且解析必然是先解析了父组件再到子组件。所以父组件的id比子组件小。

    • 用户创建的Watcher会比render时候创建的先解析。所以用户创建的Watcher的id比render时候创建的小。

删除Watcher,如果一个组件的 Watcher 在队列中,而他的父组件被删除了,这个时候也要删掉这个Watcher。

队列执行过程中,会保存一个对象circular,里面有每个 watcher 的执行次数,如果哪个watcher执行超过MAX_UPDATE_COUNT定义的次数就认为是死循环,不再执行,默认是 100 次。

总之,调度的作用就是管理 Watcher。

总结

  • 初始化 Vue实例并将data通过Object.defineProprty后, 执行new Watcher(vm, update) 传入组件的实例和更新方法,并调用组件的渲染函数,同时设置Dep的静态属性target为当前的 Watcher 实例

  • 调用渲染函数时若有从data中取值,则调用属性的get方法。该方法会判断Dep的 target有没有值,有值则将该 target的值加入到 Dep的 subs 中

  • 当 data 中有属性值更新时。调用属性的 set 方法。由于在将属性变为 reactive 时,给每个属性都加了一个 dep 的实例,而且还在读取属性值时即调用属性的 get 方法时,给 dep 的属性 subs 加入了订阅者Watcher的实例。所以调用 set 方法时可以拿到 subs 中的值并逐个调用其run方法。run方法会执行组件的更新函数

补充

数组的响应式

为什么我们直接修改数据中某项的时候,视图并没有响应式地变化呢。因为没有对数组的每一项进行数据劫持。为了解决这个问题 vue 重写了数组相关的方法。

const arrayProto = Array.prototype
exportconst arrayMethods = Object.create(arrayProto)
 
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
 
/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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
  })
})

Computed 属性

//  模拟
const data = reactive({
  number: 1
})
 
const numberPlusOne = computed(() => data.number + 1)
 
// 渲染函数watcher
new Watcher(() => {
  document.getElementById('app2').innerHTML = `
    computed: 1 + number 是 ${numberPlusOne.value}
  `
})

这段渲染函数执行过程中,读取到numberPlusOne的值的时候,首先会把Dep.target设置为numberPlusOne所对应的computedWatcher

computedWatcher的特殊之处在于

  1. 渲染watcher只能作为依赖被收集到其他的 dep里,而computedWatcher实例上有属于自己的dep,它可以收集别的watcher作为自己的依赖。
  2. 惰性求值,初始化的时候先不去运行getter。
export default class Watcher {
  constructor(getter, options = {}) {
    const { computed } = options
    this.getter = getter
    this.computed = computed
 
    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }
}
点击播放