2、vue响应式对象

image

Object.defineProperty

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

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

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

initState

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

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 上,咱们稍后也会介绍ide

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 的初始化都是把它们变成响应式对象oop

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 &&
    // 二、来判断是不是服务端渲染
    !isServerRendering() &&
    // 三、只有当数据对象是数组或纯对象
    (Array.isArray(value) || isPlainObject(value)) &&
    // 四、要被观测的数据对象必须是可扩展的
    // 不可扩展:Object.preventExtensions()、Object.freeze() 以及 Object.seal()
    Object.isExtensible(value) &&
    // 五、Vue 实例对象拥有 _isVue 属性,因此这个条件用来避免 Vue 实例对象被观测
    !value._isVue
  ) {
    // 执行 ob = new Observer(value) 对数据对象进行观测
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

一、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 作的事情是派发更新

相关文章
相关标签/搜索