vue2源码 - set 和 数组方法的重写

325 阅读3分钟

set 和 数组方法 其实都是对新属性执行下列操作

  1. 劫持新属性(Object.defineProperty)
  2. 手动通知 watcher 更新(dep.notify()

set

vue 在初始化时对 data 中的属性进行了监测,实现了数据驱动视图更新,可查看我上次写的:vue源码 - 利用 Object.defineProperty 进行数据监测

未在 data 中初始化的属性是非响应的,也就是修改这些数据,并不会驱动视图更新,Vue 为了解决这个问题,定义了一个 set 方法

<template>
  <div>
    {{ obj.b }}
  </div>
</template>
var vm = new Vue({
  data:{
    obj: {
      a: 1
      // 未初始化 b
    }
  },

  mounted () {
    this.obj.b = 2 // 视图不会更新
    /*
      this.$set(this.obj, 'b', 1) // 视图会更新
    */
  }
})

下面看一下 set 源码,定义在 src/core/global-api/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

可以看到 set 方法先判断了要设置的目标属性是否数组,是则调用 splice 进行设置,此处的 splice 是经过 vue 覆盖的方法,是具有响应处理的,并不是原生的 splice,关于这些方法此处先忽略,详情查下面的数组方法

之后通过 in 判断要设置的属性是否已定义,若已定义,说明本身就是响应式的,直接设置即可

之后判断是否是设置 vue 实例的属性,是则警告

之后判断目标是否存在 __ob__ 属性,如果不存在说明目标不是响应式的,直接设置即可

如果之前的判断都没触发,则通过 defineReactive 监测属性,并通过 ob.dep.notify() 手动更新视图,这两个操作的详情可查看:vue源码 - 利用 Object.defineProperty 进行数据监测,经过这两步,设置的属性就变成响应式的了

数组方法

上文说到 set 中对于数组是使用 splice 处理的,而 splice 是经过 vue 重写的,vue 对一些数组常用的方法都进行了重写,增加了响应式处理,下面看下这部分源码

vue源码 - 利用 Object.defineProperty 进行数据监测 这篇文章中可以看到 vue 在实例化 Observer 时,针对数组类型进行了相应处理

export class Observer {
  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 {
      // ...
    }
  }
}

这里的 hasProto 是判断对象中是否存在 __proto__ 这个指向原型对象的属性,是则调用 protoAugment 方法,否则调用 copyAugment,对于支持 Vue 的浏览器一般都就走到 protoAugment 了,这个方法直接将 vue 处理过的方法 arrayMethods 覆盖原型中的方法

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

下面看看 arrayMethods ,看看 Vue 是如何重写这写数组方法的,源码在 在 src/core/observer/array.js

import { def } from '../util/index'

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

可以看到,arrayMethos 先继承了 Array,然后循环重写一系列数组方法,这些方法调用原始方法拿到结果并返回

对于 push unshift splice 做了特殊处理,因为这三个方法是可以为数组添加子元素的,这些子元素要进行响应式处理

此处比较巧妙的是使用拓展符接收参数,这样参数就是一个数组,对于 push unshift 拿到参数数组,对于 splice 则切割拿到 index 为 2 之后的元素,这些都是新添加的元素,然后传给 ob.observeArray(inserted) 进行处理,observeArray 会循环数组进行响应式处理,这个方法就不详细写了,其实也就是循环数组调用上面说到的 defineReactive, 可查看vue源码 - 利用 Object.defineProperty 进行数据监测

Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

这样处理之后就对新添加的元素进行了监测,这些新元素就是响应式的了