二、vue响应式对象

image

Object.defineProperty

Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:

Object.defineProperty(obj, prop, descriptor)
  • obj 是要在其上定义属性的对象;
  • prop 是要定义或修改的属性的名称;
  • descriptor 是将被定义或修改的属性描述符;

get 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。

initState

在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法,它的定义在 src/core/instance/state.js 中。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化props
  if (opts.props) initProps(vm, opts.props)
  // 初始化方法
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化data没有跟数据的话就初始一个
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 出书watach
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState 方法主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作。这里我们重点分析 props 和 data,对于其它属性的初始化我们之后再详细分析。

if (opts.data) {
  initData(vm)
} else {
  observe(vm._data = {}, true /* asRootData */)
}

首先判断 opts.data 是否存在,即 data 选项是否存在,如果存在则调用 initData(vm) 函数初始化 data 选项,否则通过 observe 函数观测一个空的对象,并且 vm._data 引用了该空对象。其中 observe 函数是将 data 转换成响应式数据的核心入口,另外实例对象上的 _data 属性我们在前面的章节中讲解 $data 属性的时候讲到过,$data 属性是一个访问器属性,其代理的值就是 _data。

initProps

// 传入两个参数vue实例和props的参数
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted转换
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (vm.$parent && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

props 的初始化主要过程,就是遍历定义的 props 配置。遍历的过程主要做两件事情:一个是调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性。对于 defineReactive 方法,我们稍后会介绍;另一个是通过 proxy 把 vm._props.xxx 的访问代理到 vm.xxx 上,我们稍后也会介绍

initData

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

data 的初始化主要过程也是做两件事,一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性,observe 我们稍后会介绍。

可以看到,无论是 props 或是 data 的初始化都是把它们变成响应式对象

let data = vm.$options.data // 首先定义 data 变量,它是 vm.$options.data 的引用
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}

vm.$options.data 其实最终被处理成了一个函数,且该函数的执行结果才是真正的数据。在上面的代码中我们发现其中依然存在一个使用 typeof 语句判断 data 数据类型的操作,我们知道经过 mergeOptions 函数处理后 data 选项必然是一个函数,那么这里的判断还有必要吗?答案是有,这是因为 beforeCreate 生命周期钩子函数是在 mergeOptions 函数之后 initData 之前被调用的,如果在 beforeCreate 生命周期钩子函数中修改了 vm.$options.data 的值,那么在 initData 函数中对于 vm.$options.data 类型的判断就是必要的了。

如果 vm.$options.data 的类型为函数,则调用 getData 函数获取真正的数据,getData 函数就定义在 initData 函数的下面

// data 选项是一个函数, 参数是 Vue 实例对象
// getData 函数的作用其实就是通过调用 data 函数获取真正的数据对象并返回
export function getData (data: Function , vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

data.call(vm, vm),而且我们注意到 data.call(vm, vm) 被包裹在 try...catch 语句块中,这是为了捕获 data 函数中可能出现的错误。同时如果有错误发生那么则返回一个空对象作为数据对象:return {}

另外我们注意到在 getData 函数的开头调用了 pushTarget() 函数,并且在 finally 语句块中调用了 popTarget(),这么做的目的是什么呢?这么做是为了防止使用 props 数据初始化 data 数据时收集冗余的依赖,等到我们分析 Vue 是如何收集依赖的时候会回头来说明。总之 getData 函数的作用就是:“通过调用 data 选项从而获取数据对象”

我们再回到 initData 函数中:

data = vm._data = getData(data, vm)

当通过 getData 拿到最终的数据对象后,将该对象赋值给 vm._data 属性,同时重写了 data 变量,此时 data 变量已经不是函数了,而是最终的数据对象

紧接着是一个 if 语句块:

// isPlainObject 函数判断变量 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
  )
}

触发代码:
data 函数返回了一个字符串而不是对象,所以我们需要判断一下 data 函数返回值的类型。
new Vue({
  data () {
    return '我就是不返回对象'
  }
})

接下来:

// proxy data on instance
const keys = Object.keys(data) // 使用 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)
  }
}

这段代码的意思是非生产环境下如果发现在 methods 对象上定义了同样的 key,也就是说 data 数据的 key 与 methods 对象中定义的函数名称相同,那么会打印一个警告,提示开发者:你定义在 methods 对象中的函数名称已经被作为 data 对象中某个数据字段的 key 了,你应该换一个函数名字.

为什么要这么做呢:

const ins = new Vue({
  data: {
    a: 1
  },
  methods: {
    b () {}
  }
})

ins.a // 1
ins.b // function

在这个例子中无论是定义在 data 中的数据对象,还是定义在 methods 对象中的函数,都可以通过实例对象代理访问。所以当 data 数据对象中的 key 与 methods 对象中的 key 冲突时,岂不就会产生覆盖掉的现象,所以为了避免覆盖 Vue 是不允许在 methods 中定义与 data 字段的 key 重名的函数的。而这个工作就是在 while 循环中第一个语句块中的代码去完成的.

第二个 if 语句块:

// 检测props里面是否有很data同名的
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
  )
  // 判断定义在 data 中的 key 是否是保留键
} else if (!isReserved(key)) {
  proxy(vm, `_data`, key)
}

另外这里有一个优先级的关系:props优先级 > data优先级 > methods优先级

!isReserved(key),该条件的意思是判断定义在 data 中的 key 是否是保留键

isReserved 函数通过判断一个字符串的第一个字符是不是 $ 或 _ 来决定其是否是保留的,Vue 是不会代理那些键名以 $ 或 _ 开头的字段的,因为 Vue 自身的属性和方法都是以 $ 或 _ 开头的,所以这么做是为了避免与 Vue 自身的属性和方法相冲突。

如果 key 既不是以 $ 开头,又不是以 _ 开头,那么将执行 proxy 函数,实现实例对象的代理访问:

proxy

下代理,代理的作用是把 props 和 data 上的属性代理到 vm 实例上,这也就是为什么比如我们定义了如下 props,却可以通过 vm 实例访问到它。

let comP = {
  props: {
    msg: 'hello'
  },
  methods: {
    say() {
      console.log(this.msg)
    }
  }
}

say 函数中通过 this.msg 访问到我们定义在 props 中的 msg,这个过程发生在 proxy 阶段

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

例子:

class Vue {
    constructor(data) {
        this.data = data;
        this.initData();
    }
    initData(){
        this.proxy(this, `data`, 'a');
    }
    proxy(target, sourceKey, key){
        const sharedPropertyDefinition = {
            enumerable: true,
            configurable: true,
            get: ()=>{},
            set: ()=>{}
        }
        sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
        }
        sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
        }
        Object.defineProperty(target, key, sharedPropertyDefinition)
    }
}

const vue = new Vue({
    a: '1'
})

console.log(vue.data.a, vue.a)

proxy 方法的实现很简单,通过 Object.defineProperty 把 target[sourceKey][key] 的读写变成了对 target[key] 的读写。所以对于 props 而言,对 vm._props.xxx 的读写变成了 vm.xxx 的读写,而对于 vm._props.xxx 我们可以访问到定义在 props 中的属性,所以我们就可以通过 vm.xxx 访问到定义在 props 中的 xxx 属性了。同理,对于 data 而言,对 vm._data.xxxx 的读写变成了对 vm.xxxx 的读写,而对于 vm._data.xxxx 我们可以访问到定义在 data 函数返回对象中的属性,所以我们就可以通过 vm.xxxx 访问到定义在 data 函数返回对象中的 xxxx 属性了

最后经过一系列的处理,initData 函数来到了最后一句代码:

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

调用 observe 函数将 data 数据对象转换成响应式的,可以说这句代码才是响应系统的开始,不过在讲解 observe 函数之前我们有必要总结一下 initData 函数所做的事情,通过前面的分析可知 initData 函数主要完成如下工作:

  • 根据 vm.$options.data 选项获取真正想要的数据(注意:此时 vm.$options.data 是函数)
  • 校验得到的数据是否是一个纯对象
  • 检查数据对象 data 上的键是否与 props 对象上的键冲突
  • 检查 methods 对象上的键是否与 data 对象上的键冲突
  • 在 Vue 实例对象上添加代理访问数据对象的同名属性
  • 最后调用 observe 函数开启响应式之路

observe

observe 的功能就是用来监测数据的变化,它的定义在 src/core/observer/index.js 中:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
 // 第一个参数是要观测的数据,第二个参数是一个布尔值,代表将要被观测的数据是否是根级数据
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 观测的数据不是一个对象或者是 VNode 实例,则直接 return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  
  let ob: Observer | void
  // 如果有__ob__的就直接返回,优化性能
  //if 分支的判断条件,首先使用 hasOwn 函数检测数据对象 value 自身是否含有 __ob__ //属性,并且 __ob__ 属性应该是 Observer 的实例。如果为真则直接将数据对象自身的 __ob__ //属性的值作为 ob 的值:ob = value.__ob__。那么 __ob__ //是什么呢?其实当一个数据对象被观测之后将会在该对象上定义 __ob__ 属性,所以 if //分支的作用是用来避免重复观测一个数据对象
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
    
  } 
  // 如果数据对象上没有定义 __ob__ 属性,那么说明该对象没有被观测过
  else if (
    shouldObserve &&
    // 2、来判断是否是服务端渲染
    !isServerRendering() &&
    // 3、只有当数据对象是数组或纯对象
    (Array.isArray(value) || isPlainObject(value)) &&
    // 4、要被观测的数据对象必须是可扩展的
    // 不可扩展:Object.preventExtensions()、Object.freeze() 以及 Object.seal()
    Object.isExtensible(value) &&
    // 5、Vue 实例对象拥有 _isVue 属性,所以这个条件用来避免 Vue 实例对象被观测
    !value._isVue
  ) {
    // 执行 ob = new Observer(value) 对数据对象进行观测
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

1、shouldObserve

shouldObserve 变量也定义在 core/observer/index.js 文件内,如下:

/**
 * In some cases we may want to disable observation inside a component's
 * update computation.
 */
export let shouldObserve: boolean = true

export function toggleObserving (value: boolean) {
  shouldObserve = value
}

该变量的初始值为 true,在 shouldObserve 变量的下面定义了 toggleObserving 函数,该函数接收一个布尔值参数,用来切换 shouldObserve 变量的真假值,我们可以把 shouldObserve 想象成一个开关,为 true 时说明打开了开关,此时可以对数据进行观测,为 false 时可以理解为关闭了开关,此时数据对象将不会被观测

满足上面的5个条件:接下来我们来看一下 Observer 的作用。

Observer

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
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
    // 为每一个value设置一个__ob__
    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.
   * 遍历obj的key,去添加get和set变成响应式对象
   */
  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])
    }
  }
}
简化后的代码:
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    // 省略...
  }

  walk (obj: Object) {
    // 省略...
  }
  
  observeArray (items: Array<any>) {
    // 省略...
  }
}

Observer 类的实例对象将拥有三个实例属性,分别是 value、dep 和 vmCount 以及两个实例方法 walk 和 observeArray。Observer 类的构造函数接收一个参数,即数据对象。

constructor 方法的全部代码
constructor (value: any) {
  this.value = value
  this.dep = new Dep() // 这个“筐”并不属于某一个字段,后面我们会发现,这个筐是属于某一个对象或数组的
  this.vmCount = 0
  def(value, '__ob__', this) // 初始化完成三个实例属性之后,使用 def 函数,为数据对象定义了一个 __ob__ 属性,这个属性的值就是当前 Observer 实例对象
  if (Array.isArray(value)) {
    const augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
def
/**
 * Define a property.
 def 函数其实就是 Object.defineProperty 函数的简单封装
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,// 那是false,定义不可枚举的属性  在walk中循环的时候,不会取到那个属性
    writable: true,
    configurable: true
  })
}

例子:

const data = {
  a: 1
}

那么经过 def 函数处理之后,data 对象应该变成如下这个样子:

const data = {
  a: 1,
  // __ob__ 是不可枚举的属性
  __ob__: {
    value: data, // value 属性指向 data 数据对象本身,这是一个循环引用
    dep: dep实例对象, // new Dep()
    vmCount: 0
  }
}

回到 Observer 的构造函数,接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法,那么我们来看一下这个方法是做什么的。

defineReactive

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter,它的定义在 src/core/observer/index.js 中:

defineReactive 函数的核心就是 将数据对象的数据属性转换为访问器属性

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
 // 这个 dep 常量所引用的 Dep 实例对象才与我们前面讲过的“筐”的作用相同
 // 即 每一个数据字段都通过闭包引用着属于自己的 dep 常量
 // 每次调用 defineReactive 定义访问器属性时,该属性的 setter/getter 都闭包引用了一个属于自己的“筐
  const dep = new Dep() 
  // 不可配置的直接返回
  // 获取该字段可能已有的属性描述对象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  
  // 判断该字段是否是可配置的
  // 一个不可配置的属性是不能使用也没必要使用 Object.defineProperty 改变其属性定义的。
  if (property && property.configurable === false) {
    return
  }

  // 保存了来自 property 对象的 get 和 set 
  // 避免原有的 set 和 get 方法被覆盖
  const getter = property && property.get
  const setter = property && property.set
  
  // 下面会特殊说明
  if ((!getter || setter) && arguments.length === 2) {
    // 获取到了对象属性的值 val,但是 val 本身有可能也是一个对象
    val = obj[key]
    
  }
  // 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象
  // walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是 undefined
  // 默认就是深度观测
  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()
    }
  })
}
被观测后的数据对象的样子

假设我们有如下数据对象:

const data = {
  a: {
    b: 1
  }
}

observe(data)

数据对象 data 拥有一个叫做 a 的属性,且属性 a 的值是另外一个对象,该对象拥有一个叫做 b 的属性。那么经过 observe 处理之后, data 和 data.a 这两个对象都被定义了 ob 属性,并且访问器属性 a 和 b 的 setter/getter 都通过闭包引用着属于自己的 Dep 实例对象和 childOb 对象:

const data = {
  // 属性 a 通过 setter/getter 通过闭包引用着 dep 和 childOb
  a: {
    // 属性 b 通过 setter/getter 通过闭包引用着 dep 和 childOb
    b: 1
    __ob__: {a, dep, vmCount}
  }
  __ob__: {data, dep, vmCount}
}

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。

核心就是利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新

posted @ 2018-11-29 11:45  快乐~  阅读(929)  评论(0编辑  收藏  举报