Skip to content
On this page

这是 Vue2.x 源码分析的第一篇,介绍在 vue.js 中如何对 ObjectArray 进行变化侦测的

Vue.js 的渲染过程是声明式的,当内部的状态发生了变化,需要不断进行重新渲染,变化侦测就是来监听内部状态的变化。

一、对 Object 的监测

在 JS 中可以使用 Object.definePropertyProxy 这两个方法来监测变化,Vue 2.x 版本考虑到兼容性使用的是 Object.defineProperty 方法,而在 Vue 3.x 中使用的是 Proxy,后续介绍

在 Vue 源码中,对 Object.defineProperty 封装如下

js
/*
@filePath: src/core/observer/index.js
*/
function defineReactive(obj, key, val){
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      return value
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val) {
        return
      }
      val = newVal
    }
  })
}

所以,当从datakey中读取数据时,get函数被触发;当往datakey中设置数据,set函数被触发

1.1、如何收集依赖?

在上面使用 Object.defineProperty 对数据监测,是因为当数据发生变化的时候,需要通知到那些使用到该数据的地方,例如👇

html
<template>
  <div>{{ title }}<div>
</template>

在组件的 tempalte 中使用来数据 title, 所以当 title 变化了,需要通知到 tempalte

总结就是:getter中收集依赖,在setter中触发(通知)依赖

1.2、收集的依赖存在哪?

在 Vue 源码中,使用了一个 Dep 类来封装依赖收集的代码,通过这个类,可以收集依赖删除依赖或者向依赖发通知

js
/*
@filePath: src/core/observer/dep.js
*/
export default class Dep {
  constructor () {
    // 用了存储依赖
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (window.target) {
      this.addSub(window.target)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

defineReactive 函数中,就可以使用 Dep 类了

js
/*
@filePath: src/core/observer/index.js
*/
function defineReactive(obj, key, val){
  // 1、每个 key 都创建一个实例来存储该属性的依赖
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 2、当获取属性时,收集依赖
      dep.depend()
      return value
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val) {
        return
      }
      val = newVal
      // 3、当属性发生变化,通知依赖
      dep.notify()
    }
  })
}

在收集依赖前,我们假设了依赖保存在 window.target 上了。此时,我们知道了收集的依赖是存储到 Dep 中的

1.3、依赖为何物?

依赖,也就是使用到数据的地方,同时也是数据发生变化时,需要通知到的地方。以 data 中的数据为例,在 templatecomputedwatch 等多个地方都可以使用到,所以,Vue.js 内部抽象出一个能集中处理这些情况的类,收集依赖时,只需要收集这个类的实例,同时通知也只需要通知到它一个,它再通知到其他地方,这个类叫做 Watcher

所以,收集依赖,就是收集 Watcher 实例

js
/*
@filePath: src/core/observer/watcher.js
*/
export class Watcher{
  constructor(vm, expOrFn, cb){
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    // 1、初始化,调用 this.get() => this.getter()
    this.value = this.get()
  }
  get(){
    // 2、当前 Watcher 实例保存在 window.target 上
    window.target = this
    let value = this.getter.call(this.vm, this.vm)
    window.target = undefined
    return value
  }
  update(){
    // 3、属性变化,通知依赖会调用 update 方法, 再去执行回调函数cb
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

所以,无论是 vm.$watch('obj.name',(value, oldValue) => {}) 还是模版中使用obj.name, 当数据发生变化时,都需要通过 Watcher 来进行通知,执行各种的回调函数

1.4、监测所有的 key

defineReactive 方法是监测数据中的单个属性,而我们需要监测到所有的属性(包括子属性),将其都转换成 getter/setter 的形式

js
/*
@filePath: src/core/observer/index.js
*/
export class Observer {
  constructor (value: any) {
    this.value = value
    if (!Array.isArray(value)) {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

function defineReactive(obj, key, val){
  /*省略*/
}

1.5、局限性

Vue.js 通过 Object.defineProperty 来将对象的 key 转换成 getter/setter 的形式来追踪变化,但 getter/setter 只能追踪一个数据是否被修改,无法追踪新增的属性已经删除的属性,为了解决这问题,Vue.js 提供了 $set$delete 两个API

1.6、小结

关于如何监测Object,汇总如下图👇

监测Object

二、对 Array 的监测

对 Array 的监测不能使用 getter/setter 方法,是因为我们常通过 Array 原型上的方法来改变数组的内容

通过汇总,Array 原型上可以改变数组自身内容的方法共 7 个,分别是:push, pop, shift, unshift, slice, sort, reverse

2.1、拦截器

想要监测数组的变化,只需用一个拦截器覆盖 Array原型,当使用上述 7 种方法修改数组时,执行的时拦截器中的方法

js
/*
@filePath: src/core/observer/array.js
*/
import { def } from '../util/index'
// 1、基于 Array.prototype 创建一个拦截器
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

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

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  // 2、在拦截器上重新定义上述 7 个方法
  def(arrayMethods, method, function mutator (...args) {
    // 3、执行方法本身的逻辑,返回执行结果
    const result = original.apply(this, args)
    return result
  })
})

2.2、如何收集依赖?

对数组而言,通知依赖肯定是在拦截器中,那么,数组在哪里收集依赖呢?看个例子

js
{
  list: [1,2,3]
}

以上面数据为例,只有通过 list 属性,才能获取这个数组,那么读取 list 属性,一定为触发list属性的 getter 方法,所以数组也是在 getter 中收集依赖

所以,Array 在 getter 中收集依赖,在拦截器中触发(通知)依赖

2.3、存储依赖

Array 的依赖也是通过 Dep 实例来进行保存,但与 Object 不同的是,Vue.js 把 Array 的 dep 放到 Observer 中了,而对象属性对应的 dep 则在 defineReactive 函数中

之所以放在 Observer 中, 是因为在 getter 和拦截器中都需要访问到这个 dep 实例

js
/*
@filePath: src/core/observer/index.js
*/
export class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    if (Array.isArray(value)) {
      this.observeArray(value) 
    } else {
      this.walk(value)
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      // 2、可能是对象数组,需要监测数组每一项
      observe(items[i])
    }
  }
}

2.4、收集依赖

我们需要在 getter 中访问到 dep 实例,才能收集依赖,代码如下

js
/*
@filePath: src/core/observer/index.js
*/
function defineReactive(obj, key, val){
  // 1、此处假设 val 是个数组
  let childOb = observer(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 2、收集依赖
      if (childOb) {
        childOb.dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      if (newVal === val) {
        return
      }
      val = newVal
    }
  })
}

export function observe (value){
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__ // 当数据已经检测过,直接返回
  } else {
    ob = new Observer(value) // 进行检测
  }
  return ob
}

以数据 list: [1,2,3] 为例,当访问属性 list, 会对数组 [1,2,3] 进行监测

2.5、通知依赖

Array 发生变化时,需要在拦截器中访问到 Observer 实例,通过其 dep 属性通知依赖

js
/*
@filePath: src/core/observer/index.js
*/
export class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      this.observeArray(value) 
    } else {
      this.walk(value)
    }
  }
  //...
}

value 上增加 __ob__ 属性即可访问到 Observer 实例,继而可以访问 dep 属性

接下来,只需要在拦截器中通知依赖, 同时,当我们使用 push, unshift, splice 时会往数组中添加新元素,新增的元素也需要被监测到。

js
/*
@filePath: 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'
]

methodsToPatch.forEach(function (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
  })
})

2.6、局限性

通过 Array 下标修改元素的值,或者设置 list.length = 0 来清空数组等,Vue.js 都无法监测数组的变化