Vue源码分析之响应式对象(二)

440 阅读5分钟

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

前言

上篇已经分析了defineReactive函数通过Obejct.defineProperty来为对象属性添加getter/setter的过程,但是对于的observe函数的具体实现并没有深入去看,这篇就来看下observe函数。

在前边看new Vue的时候

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

整体来看,observe函数用来为value创建一个Observer实例,最后返回这个Observer实例,如果已经创建过则直接使用。

主要接收两个参数:

  1. 要创建Observer实例的value
  2. 是否是根数据

具体来看下实现细节:

  • 首先判断value的类型,如果是非对象或者Vnode类型,则直接返回。

  • 接下来判断value对象中是否存在__ob__属性,且该属性对应的值是一个Observer类型(即该value对象已经添加了Observer),则直接将Observer赋值给ob变量。

  • 如果没有__ob__,且value是一个简单的对象(非Regexp等情况),则创建一个Observer实例赋值给ob变量。

  • 如果value是根对象,则ob.vmCount值加一。vmCount值是用来标记value作为根数据的个数。在看new Vue实例过程时在初始化过程中会调用initState函数,在它的内部就会调observe(data, true),此时的value就是一个根对象。

  • 最后返回ob变量。

看一下几个工具函数,都定义在src/shared/util.js文件中:

hasOwn

hasOwn函数内部直接调用了hasOwnProperty来判断某个属性是否包含在指定对象中(不包括原型链中的属性)。

const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
  return hasOwnProperty.call(obj, key)
}

isPlainObject

export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

isObject

export function isObject (obj: mixed): boolean %checks {
  return obj !== null && typeof obj === 'object'
}

Observer

接下来看一下Observer类的具体实现:

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have 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)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    ...
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    ...
  }
}

Observer接收三个参数(要转化为Observer的value值,Dep,代表根对象的vms的个数)

添加__ob__属性

初始化完参数后,执行了def(value, '__ob__', this)

先来看下这个def函数,定义在src/core/utils/lang.js

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

def函数就是对Object.defineProperty的封装,它的作用就是在value对象上增加一个__ob__属性,指向这个创建的Observer。其实在开发过程中看data上的数据,会发现,都包含一个__ob__的属性。

image.png

value对象上添加完__ob__属性后,接下来就是处理value对象了。

处理value

  1. 首先判断value是不是数组,如果不是,则调用walk函数去处理value。看一下walk做了什么??
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

walk函数内部还是调用了前边我们已经看过的defineReactive来把value的自身属性都变成响应式的。

  1. 接下来看下value是数组的情况下,要怎么处理??
if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
}

hasProto变量定义在src/core/util/env.js文件中

export const hasProto = '__proto__' in {}

hasProto是用来检测当前运行环境是否支持__proto__,虽然Object.prototype.__proto__在web标准中已经被废除,但是目前大部分的浏览器仍然是支持的。

arrayMethods变量定义在src/core/observer/array.js

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

Vue官方文档提到过Vue对数组是做了特殊处理的,它将数组的七个方法重新做了包裹来实现响应式,但是对于个别数组的操作还是无法监听到变化。

上边的代码就是处理数组的过程:

  • 首先利用Object.create方法创建一个继承自Array原型的空对象arrayMethods
  • 接下来遍历数组的这7个方法,并把他们一一挂到arryaMethods上。对push,unshift,splice三个可以增加数组长度的方法进行了特别处理:首先拿到新增的数据,再调用observeArray把新增数据变成响应式的。
  • 最后调用notify来派发更新

protoAugment

在支持__proto__的环境中,调用protoAugment方法,来看下这个方法:

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

我们知道实例通过__proto__指向构造函数的原型对象。protoAugment函数就是用来把value数组实例的__proto__指向重新包装后的数组对象arrayMethods。

copyAugment

不支持__proto__的环境中,调用copyAugment方法。

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

先来看下arrayKey定义:

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

arrayKeys就是我们拦截的那七个数组方法:["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]

copyAugment方法就是用来把arrayMethods中的数组方法一一添加到result中。

observeArray

将数组对象value通过arrayMethods拦截处理完后,最后执行observeArray

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

这个函数用来遍历数组,然后对每一项都调用observe,将数组中的每一项都变成响应式数据。

总结

  • observe函数用来返回一个Observer
  • Observer类用来为对象添加一个__ob__属性,同时把该对象变为响应式的。
  • 在defineReactive函数中看出响应式的实现是通过使用Object.defineProperty拦截对象来添加getter/setter,但是数组是没有getter/setter的,所以要对数组去特殊处理。