vue源码 - 利用 Object.defineProperty 进行数据监测

688 阅读4分钟

vue 是数据驱动的,意思是数据改变会驱动视图更新,要实现这个首先要对数据进行监测,下面看看 Vue 初始化时是如何监测数据的

初始化

vue 在创建实例时,会调用 _init 进行初始化,在 _init 中,会调用 initState 对数据进行初始化

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ... 略
    initState(vm)
  }
}

initState

initState 定义在 src/core/instance/state.js

export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);

  if (opts.data) {
    initData(vm); // 初始化 data
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

可以看到该方法中对 props methods data computed watch 进行了初始化,由于数据驱动主要是针对 data 中的数据,所以下面看一下 initData

initData

initData 定义在 src/core/instance/state.js

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== "production" &&
      warn(
        "data functions should return an object:\n" +
          "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
        vm
      );
  }

  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    if (process.env.NODE_ENV !== "production") {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== "production" &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        );
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

平时使用 Vue 时,data 一般会定义为方法(为了 data 的指针不同),所以 initData 开头有一个判断,如果 data 是函数,则调用一下获取到 data。

如果 data 不是一个对象,则初始化为对象。

然后进行了重名判断,如果与 methods props 中的属性重名了,则给出警告

最后 initData 主要做了如下两件事

1. proxy - 代理属性

什么是代理属性,举例来说,假设在 data 中定义了一个 msg 属性,在引用时,为什么可以直接使用 this.msg 而不需 this.data.msg 呢?原因就是 initData 时,Vue 将 data 的属性代理到了 this 上,其他 props computed methods 等等也是这样处理过的,所以也可以直接通过 this 访问

{
  data () {
    return {
      msg: 'hello'
    }
  },
  methods: {
    say() {
      console.log(this.msg)
    }
  }
}

这个实现很简单,可以看到 initData 中一行关键代码

proxy(vm, `_data`, key);

proxy 其实是通过 Object.defineProperty 拦截 this 的 getter读取 和 setter设置 操作,在读取时,读取 this._data 上的属性,设置时,设置 this._data 上的属性

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

2. observe - 监测数据

可以看到 initData 最后执行了

observe(data, true /* asRootData */);

observe 定义在 src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

如果数据是被监测过的,那么会有一个 __ob__ 属性,这时直接赋值给 ob 即可,如果未监测过则实例化一个 Observer 类,并且把 data 通过参数传进去

Observer 类定义在 src/core/observer/index.js

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

可以看到 Observer 给数据加了一个 __ob__ 属性,指向自身,这里就和上面 observe 中的判断对应上了

后面对于数组则调用 observeArray监测数组的方法,其实是遍历数组,对每个 item 进行监测

对于对象,则直接调用 walk 方法,可以看到 walk 方法中遍历对象调用了 defineReactive

defineReactive 定义在 src/core/observer/index.js

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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

可以看到 defineReactive 开头实例化了 Dep 观察者系统,由此可知 data 中每个属性都会实例化一个 Dep,这个是用于给当前属性收集当前 watcher 的,watcher 的作用是初始化视图,并且它包含一个更新视图的 update 方法,这里把 watcher 收集起来,等到数据更新时,调用这个 update 方法即可实现驱动视图更新

后面又对属性调用了 observe 方法,这样相当于递归把 data 中的对象属性也进行了监测

最后关键代码来了,defineReactive 中利用 Object.defineProperty 拦截了属性的 读取getter 和 设置setter 操作,读取时订阅 watcher,设置时通过 watcher 更新视图

在 getter 中收集当前 watcher 这里的关键代码是 dep.depend(),这一行代码就是使用该属性的观察者系统订阅当前 watcher ,这里判断了 Dep.target ,Dep.target 是当前 watcher ,关于这个值是什么时候赋上的,请看下面数据驱动流程分析,这里只需先了解 getter 订阅当前 watcher,setter 更新视图

在 setter 中更新视图,这里的关键代码是 dep.notify() 这一行代码用于通知观察者队列中的 watcher 进行视图更新

数据驱动流程分析

上面说到,defineReactive 拦截了属性的 getter 和 setter,读取属性时订阅 watcher,设置属性时通过 watcher 更新视图,我们设置属性时就会触发 setter ,从而更新视图,那 getter 是什么时候触发的呢? getter 中判断的 Dep.target 又是什么时候设置的呢? 了解了 vue 初始化的整体流程就能明白这个问题。

可以先看一下 vue源码之初始化 - new Vue() 之后发生了什么

<div id="app">
  {{msg}}
  <button @click="changeMsg">改变message</button>
</div>

var app = new Vue({
  el: '#app',
  data: {
    msg: 'Hello Vue!'
  },
  methods: {
    changeMsg () {
      this.msg = 'Hello word'
    }
  }
})

以上面代码为例,new Vue 时,Vue 首先会进行一系列初始化,然后通过 mountComponent 函数进行挂载,在挂载时,会组装 updateComponent 函数, 这个函数会传入 watcher 中,提供给 watcher 进行视图更新,它通过 _render 生成 vnode 然后通过 _update 渲染 dom

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

Watcher 类的关键代码如下

// core/observer/watcher.js
export default class Watcher {
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } 
  }
}

// dep.js
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

可以看到在实例化 watcher 时,会调用 get 方法给 value 赋值,在 get 方法中会调用 pushTarget, 在 pushTarget 中就把当前 watcher 赋值到了 Dep.target 中

接着又调用了 value = this.getter.call(vm, vm) ,这里的 getter 实际上是 updateComponent 方法,所以这里实际上是调用 updateComponent 初始化视图

在初始化视图生成 Vnode 时,必定会读取到 msg 这个值,于是就触发了 msg 这个值的 getter, 在 msg 的 getter 中初始化观察者系统 Dep,然后将刚刚设置的 Dep.target 推进观察者系统中。

接着点击按钮,调用 changeMsg 方法,这个方法会设置 msg ,这时就进入到了 msg 的 setter 中,setter 通过 dep.notify() 通知 watcher 调用 updateComponent 方法进行视图更新

data.png

如图所示:render 触发 getter,收集 watcher, 设置时触发 setter 通知 watcher 更新视图