VueJS 响应式原理及简单实现

1,245 阅读2分钟

概述

Vue 的响应式模型指的是:

  1. 视图绑定的数据在更新后也会渲染到视图上
  2. 使用vm.$watch()监听数据的变化,并调用回调
  3. 使用Vue实例的属性watch注册需要监听的数据和回调

上面的三种方式追根揭底,都是通过回调的方式去更新视图或者通知观察者更新数据

Vue的响应式原理是基于观察者模式和JS的API:Object.defineProperty()Proxy对象

主要对象

每一个被观察的对象对应一个Observer实例,一个Observer实例对应一个Dep实例,Dep和Watcher是多对多的关系,附上官方的图,有助于理解:

Vue 视图更新

1. Observer

一个被观察的对象会对应一个Observer实例,包括options.data

一个Observer实例会包含被观察的对象和一个Dep实例。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;
}

2. Dep

Dep实例的作用是收集被观察对象(值)的订阅者。

一个Observer实例对应一个Dep实例,该Dep实例的作用会在Vue.prototype.$setVue.prototype.$del中体现——通知观察者。

一个Observer实例的每一个属性也会对应一个Dep实例,它们的getter都会用这个Dep实例收集依赖,然后在被观察的对象的属性发生变化的时候,通过Dep实例通知观察者。

options.data就是一个被观察的对象,Vue会遍历options.data里的每一个属性,如果属性也是对象的话,它也会被设计成被观察的对象。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
}

3. Watcher

一个Watcher对应一个观察者,监听被观察对象(值)的变化。

Watcher会维护一个被观察者的旧值,并在被通知更新的时候,会调用自身的this.getter()去获取最新的值并作为要不要执行回调的依据。

Watcher分为两类:

  1. 视图更新回调,在数据更新(setter)的时候,watcher会执行this.getter()——这里Vue把this.getter()作为视图更新回调(也就是重新计算得到新的vnode)。

  2. 普通回调,在数据更新(setter)的时候,会通知Watcher再次调用this.getter()获取新值,如果新旧值对比后需要更新的话,会把新值和旧值传递给回调。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;
}

使options.data成为响应式对象的过程

Vue使用initData()初始化options.data,并在其中调用了observe方法,接着:

  1. 源码中的observe方法是过滤掉不是对象或数组的其它数据类型,言外之意Vue仅支持对象或数组的响应式设计,当然了这也是语言的限制,因为Vue使用API:Object.defineProperty()来设计响应式的。
  2. 通过observe方法过滤后,把传入的value再次传入new Observer(value)
  3. 在Observer构造函数中,把Observer实例连接到value的属性__ob__;如果value是数组的话,需要修改原型上的一些变异方法,比如push、pop,然后调用observeArray遍历每个元素并对它们再次使用observe方法;如果value是普通对象的话,对它使用walk方法,在walk方法里对每个可遍历属性使用defineReactive方法
  4. defineReactive方法里,需要创建Dep的实例,作用是为了收集Watcher实例(观察者),然后判断该属性的property.configurable是不是false(该属性是不是不可以设置的),如果是的话返回,不是的话继续,对该属性再次使用observe方法,作用是深度遍历,最后调用Object.defineProperty重新设计该属性的descriptor
  5. 在descriptor里,属性的getter会使用之前创建的Dep实例收集Watcher实例(观察者)——也是它的静态属性Dep.target,如果该属性也是一个对象或数组的话,它的Dep实例也会收集同样的Watcher实例;属性的setter会在属性更新值的时候,新旧值对比判断需不需要更新,如果需要更新的话,更新新值并对新值使用observe方法,最后通知Dep实例收集的Watcher实例——dep.notify()。至此响应设计完毕
  6. 看一下观察者的构造函数——constructor (vm, expOrFn, cb, options, isRenderWatcher),vm表示的是关联的Vue实例,expOrFn用于转化为Watcher实例的方法getter并且会在初始化Watcher的时候被调用,cb会在新旧值对比后需要更新的时候被调用,options是一些配置,isRenderWatcher表示这个Watcher实例是不是用于通知视图更新的
  7. Watcher构造函数中的expOrFn会在被调用之前执行Watcher实例的get()方法,该方法会把该Watcher实例设置为Dep.target,所以expOrFn里的依赖收集的目标将会是该Watcher实例
  8. Watcher实例的value属性是响应式设计的关键,它就是被观察对象的getter的调用者——value = this.getter.call(vm, vm),它的作用是保留旧值,用以对比新值,然后确定是否需要调用回调

总结:

  1. 响应式设计里的每个对象都会有一个属性连接到Observer实例,一般是__ob__,一个Observer实例的value属性也会连接到这个对象,它们是双向绑定的
  2. 一个Observer实例会对应一个Dep实例,这个Dep实例会在响应式对象里的所有属性的getter里收集Watcher实例,也就是说,响应式对象的属性更新了,会通知观察这个响应式对象的Watcher实例
  3. 在Vue里Watcher实例,可以是视图更新回调,也可以是普通回调,本质上都是一个函数,体现了JS高阶函数的特性
  4. Vue的响应式设计很多地方都使用了遍历、递归

Vue提供的其它响应式API

Vue除了用于更新视图的观察者API,还有一些其它的API

1. Vue实例的computed属性

构造Vue实例时,传入的options.computed会被设计成既是观察者又是被观察对象,主要有下面的三个方法:initComputed、defineComputed、createComputedGetter

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

2. Vue实例的watch属性

在实例化Vue的时候,会把options.watch里的属性都遍历了,然后对每一个属性调用vm.$watch()

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

vm.$watch被作为一个独立的API导出。

3. Vue.prototype.$watch

Vue.prototype.$watch是Vue的公开API,可以用来观察options.data里的属性。

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    cb.call(vm, watcher.value)
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

4. Vue.prototype.$set

Vue.prototype.$set用于在操作响应式对象和数组的时候通知观察者,也包括给对象新增属性、给数组新增元素。

Vue.prototype.$set = set

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

ob.dep.notify()之所以可以通知观察者,是因为在defineReactive里有如下代码:

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
    }
    /* 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()
  }
})

上面的childOb.dep.depend()也为响应式对象的__ob__.dep添加了同样的Watcher实例。所以Vue.prototype.$setVue.prototype.$del都可以在内部通知观察者。

5. Vue.prototype.$del

Vue.prototype.$del用于删除响应式对象的属性或数组的元素时通知观察者。

Vue.prototype.$del = del

/**
 * Delete a property and trigger change if necessary.
 */
export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

简单实现响应式设计

  1. 实现Watcher类和Dep类,Watcher作用是执行回调,Dep作用是收集Watcher
class Watcher {
  constructor(cb) {
    this.callback = cb
  }

  update(newValue) {
    this.callback && this.callback(newValue)
  }
}

class Dep {
  // static Target

  constructor() {
    this.subs = []
  }

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

  notify(newValue) {
    this.subs.forEach(sub => sub.update(newValue))
  }
}
  1. 处理观察者和被观察者
// 对被观察者使用
function observe(obj) {
  let keys = Object.keys(obj)
  let observer = {}
  keys.forEach(key => {
    let dep = new Dep()
    Object.defineProperty(observer, key, {
      configurable: true,
      enumerable: true,
      get: function () {
        if (Dep.Target) dep.addSub(Dep.Target)
        return obj[key]
      },
      set: function (newValue) {
        dep.notify(newValue)
        obj[key] = newValue
      }
    })
  })
  return observer
}

// 对观察者使用
function watching(obj, key) {
  let cb = newValue => {
    obj[key] = newValue
  }
  Dep.Target = new Watcher(cb)
  return obj
}
  1. 检验代码
let subscriber = watching({}, 'a')
let observed = observe({ a: '1' })
subscriber.a = observed.a
console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
observed.a = 2
console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
  1. 结果:
subscriber.a: 1, observed.a: 1
subscriber.a: 2, observed.a: 2

CodePen演示

参考

深入理解Vue响应式原理 vue.js源码 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定 50行代码的MVVM,感受闭包的艺术 Vue.js 技术揭秘