Skip to content

VueJS 响应式原理及简单实现 #75

@coconilu

Description

@coconilu

概述

Vue 的响应式模型指的是:

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

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

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

主要对象

每一个被观察的对象对应一个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.$set Vue.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

参考

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions