Vue初学习之响应式原理

60 阅读3分钟

前言

Vue的响应式系统是它的一个很重要的特性,数据模型的修改,视图要接收到通知进行变更。在刚接触到Vue框架时,也是对它的这个特性比较好奇,所以带着这个疑问,去了解了下它的整体运行机制。

数据变化如何追踪

当我们把一个对象加入到Vue的data中后,Vue 会遍历该对象的所有Property ,使用 Object.defineProperty 把这些 property 全部转发 getter/setter 。(相信熟悉服务端语言(Java/C#)的同学,看到这里,会很亲切,因为对这些在这两种语言中早已驾轻就熟了)

Object.defineProperty的属性描述符有两种形式: 数据描述符和存取描述符.

Object.defineProperty是实现数据变化的源头,简单看下是怎么创建一个对象的:

var value = 38;
Object.defineProperty(o, "b", {
  // 使用了方法名称缩写(ES2015 特性)
  // 下面两个缩写等价于:
  // get : function() { return bValue; },
  // set : function(newValue) { bValue = newValue; },
  get() { return value; },
  set(newValue) { value = newValue; },
  // 可枚举
  enumerable : true,
  // 该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除
  configurable : true
});
console.log(o.b); // 控制台输出 38

从上面代码形式可以知道,在Vue中声明一个属性时,会通过Object.defineProperty来定义这个属性的 getter/setter 方法,在真正访问属性时是调用get方法,更新属性时是调用 set 方法,那是不是可以直接在set 方法中,直接增加一个方法来监听属性的变化,当调用set 时,直接发一个消息,通知已经有监听这个通知的订阅者呢?

在Vue 内部其实也是通过这种方式,整个是通过 Observer 这个模块来实现,内部有 Observer -> Dep -> Watcher 。

首先看下 Observer的实现

  • 构造函数
constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}

value 是要被观察的对象,同时会给这个对象添加 __ob__属性,作为已经被观察的标识。

以普通对象来分析这个构造过程,在walk 方法中, 遍历对象的每个key,对对象上每个key的数据调用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: 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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知
      dep.notify()
    }
  })
}

在 Observer 中会通过构建属性值的变化,通过 Dep 通知各个已经订阅的 Watcher ,在Dep 中实现如下:

subs: Array<Watcher>;
notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 更新
      subs[i].update()
    }
  }

在 Watcher update时,会通知到已经绑定的组件上。

简单勾画下三者关系

结束语

框架只是对基础的封装,就像Java/C# 这种高级语言只是对底层的封装,让使用者可以更加便捷的使用底层的一些特性来为项目服务,所以底层的基础知识还是需要更深入的学习。

了解了Vue的响应式原理,其实底层只是对于 Object.defineProperty的深度定制和扩展,只有深入到源码后,才可以比较好的了解其实质,当然这篇文章仅是作为入口,它的底层实现还是有很多细节,后面的篇幅会继续展开。

最后引用官方的响应式图看下数据流转