Vue2源码学习笔记 - 8.响应式原理一def、proxy及defineReactive函数

上一节我们学习了响应式的底层基础方法 Object.defineProperty,这一节我们来学习下 Vue 响应式相关中的几个重要函数:def、proxy 与 defineReactive。它们三个名虽不同,但是实际底层实现都是调用的 Object.defineProperty,对于不同目的对它进行了包装以实现各自的功能。

def

我们先来看看 def 函数,这个函数非常简单,它的作用就是在数据对象上定义一个属性值。我们来看看它的源码,它在文件 /src/core/util/lang.js 中定义

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

var data = {}
data.id = 999 === def(data, 'id', 999); // 类似于

虽然内部也是调用的 Object.defineProperty 去定义,但是没有配置 setter/getter 方法,这就类似于 obj.xxx = yyy 的方式直接定义了属性值。

proxy

再看 proxy 函数,它的实现也很简单,只是稍微比 def 复杂些。我们看源码,它在文件 /src/core/instance/state.js 中定义

...
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)
}
...

其中有多处调用了这个函数,随便找一处,我们来看看初始化 options.data 时 initData 中调用 proxy 函数的地方

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  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 (props && hasOwn(props, key)) {
      ...
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key) // 遍历 data 循环调用 proxy
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

从 initData 可以看出,它是遍历 options.data 对象,每个 key 都调用一次 proxy 函数。在 proxy 中通过 Object.defineProperty 在 target 上定义了 key 属性,也就是代理了 target._data.xxx 到 target.xxx。那么换而言之,就是把 Vue 实例上的 this._data.xxx 映射到了 this.xxx 上。这里的 xxx 就是定义在 options.data 中的属性键名。这就解决了我们在选项 options.data 里定义的对象值可以直接在 this 上访问的问题。

var options = {
    data: { // options.data
        id: 123,
        name: 'java'
    },
    created: function() {
        // 在 initData 中通过调用 proxy 方法实现
        // options.data.id => this.id === this._data.id
        this.id = 999;
        // options.data.name => this.name === this._data.name
        this.name = 'javascript';
    }
}
new Vue(options)
defineReactive

最后来看看 defineReactive 函数,它是一个非常重要的函数,它的作用是在一个对象上定义一个响应式的属性。它的实现相比 proxy 略微复杂些,来看看它在文件 /src/core/observer/index.js 中的定义

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 实例化 Dep 对象,主要存储对该属性的依赖,Dep 类后面详细研究
  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 // 获取原有 getter 方法
  const setter = property && property.set // 获取原有 setter 方法
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key] // 尚未定义 getter 方法则通过 obj[key] 获取属性的值
  }
  
  // 对属性的 值 进行响应式观察处理,这个 observe 后面再详细研究
  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()            // 更新属性,通知 依赖者
    }
  })
}

如代码中注释说明,这个 defineReactive 函数先创建一个 Dep 依赖对象,主要负责依赖收集和在修改属性值时派发更新事件,这个类后面详细说。然后调用 Object.getOwnPropertyDescriptor 获取描述符和原有 getter/setter,跟着对属性的值传给 observe 函数,它主要是对参数进行响应式观察处理,这个我们也后面详细说。

然后就是关键点,调用 Object.defineProperty 对属性值重新配置响应式的 getter/setter。对于 getter,先获得属性值,再执行 dep.depend 方法做依赖收集的操作,然后再返回属性值。对于 setter,先设置属性为新值,然后调用 observe 对新值进行响应式观察处理。

vue defineReactive

总结:

这一节我们简单研究学习 def、proxy 和 defineReactive 函数的实现原理。def 只是简单的给数据对象定义属性值;proxy 实现了 this._data.xxx 到 this.xxx 的代理;defineReactive 则实现了数据的响应式,getter 中收集依赖,setter 中通知依赖。它们的底层核心都是依靠调用 Object.defineProperty 修改配置属性的 getter/setter 来实现。

这里我们先学习必要的基础知识,其中的依赖收集与通知依赖,我们在后面章节会继续详细研究学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值