对响应式数据对象以及它的 getter 和 setter 部分做了了解,但是对于一些特殊情况是需要注意的

对象添加属性

使用Object.defineProperty实现响应式的对象,当去给这个对象添加一个新的属性时是不能够触发它的setter的。比如

  1. var vm = new Vue({
  2. data:{
  3. a:1
  4. }
  5. })
  6. // vm.b 是非响应的
  7. vm.b = 2

Vue为了解决添加新属性的问题,定义了全局API方法Vue.set
定义在src/core/global-api/index.js中

  1. Vue.set = set

set方法的实现定义在src/core/observer/index.js中

  1. /**
  2. * Set a property on an object. Adds the new property and
  3. * triggers change notification if the property doesn't
  4. * already exist.
  5. */
  6. export function set (target: Array<any> | Object, key: any, val: any): any {
  7. if (process.env.NODE_ENV !== 'production' &&
  8. (isUndef(target) || isPrimitive(target))
  9. ) {
  10. warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  11. }
  12. // 数组
  13. if (Array.isArray(target) && isValidArrayIndex(key)) {
  14. target.length = Math.max(target.length, key)
  15. target.splice(key, 1, val)
  16. return val
  17. }
  18. // key若已经存在于target中则直接赋值返回,因为这样的变化是可以观测到的
  19. if (key in target && !(key in Object.prototype)) {
  20. target[key] = val
  21. return val
  22. }
  23. const ob = (target: any).__ob__
  24. if (target._isVue || (ob && ob.vmCount)) {
  25. process.env.NODE_ENV !== 'production' && warn(
  26. 'Avoid adding reactive properties to a Vue instance or its root $data ' +
  27. 'at runtime - declare it upfront in the data option.'
  28. )
  29. return val
  30. }
  31. // 不存在则说明target不是一个响应式的对象,直接赋值返回
  32. if (!ob) {
  33. target[key] = val
  34. return val
  35. }
  36. // 把新添加的属性变成响应式对象
  37. defineReactive(ob.value, key, val)
  38. // 手动触发依赖通知
  39. ob.dep.notify()
  40. return val
  41. }

defineReactive方法

  1. /**
  2. * Define a reactive property on an Object.
  3. */
  4. export function defineReactive (
  5. obj: Object,
  6. key: string,
  7. val: any,
  8. customSetter?: ?Function,
  9. shallow?: boolean
  10. ) {
  11. // ...
  12. let childOb = !shallow && observe(val)
  13. Object.defineProperty(obj, key, {
  14. enumerable: true,
  15. configurable: true,
  16. get: function reactiveGetter () {
  17. const value = getter ? getter.call(obj) : val
  18. if (Dep.target) {
  19. dep.depend()
  20. if (childOb) {
  21. childOb.dep.depend() // 收集依赖
  22. if (Array.isArray(value)) {
  23. dependArray(value)
  24. }
  25. }
  26. }
  27. return value
  28. },
  29. // ...
  30. })
  31. }

在 getter 过程中判断了 childOb,并调用了 childOb.dep.depend() 收集了依赖,这就是为什么执行 Vue.set 的时候通过 ob.dep.notify() 能够通知到 watcher,从而让添加新的属性到对象也可以检测到变化
如果 value 是个数组,那么就通过 dependArray 把数组每个元素也去做依赖收集

数组

Vue不能检测到以下变动的数组

  1. 当利用索引值直接设置一个项时 例如vm.items[indexOfItem] = newValue
  2. 当修改数组长度时 例如vm.items.length = newLength

对于第一种情况可以使用 Vue.set(example1.items, indexOfItem, newValue)
对于第二种情况可以使用 vm.item.splice(newLength)

之前分析中在通过observe方法去观察对象时会实例化Observer,在它的构造函数中是专门对数组做了处理
定义在src/core/observer/index.js中

  1. /**
  2. * Observer class that is attached to each observed
  3. * object. Once attached, the observer converts the target
  4. * object's property keys into getter/setters that
  5. * collect dependencies and dispatch updates.
  6. */
  7. export class Observer {
  8. value: any;
  9. dep: Dep;
  10. vmCount: number; // number of vms that have this object as root $data
  11. constructor (value: any) {
  12. this.value = value
  13. this.dep = new Dep()
  14. this.vmCount = 0
  15. def(value, '__ob__', this)
  16. if (Array.isArray(value)) {
  17. // hasProto 判断对象中是否存在__proto__
  18. if (hasProto) {
  19. protoAugment(value, arrayMethods)
  20. } else {
  21. copyAugment(value, arrayMethods, arrayKeys)
  22. }
  23. this.observeArray(value)
  24. } else {
  25. this.walk(value)
  26. }
  27. }
  28. // ...
  29. }

protoAugment和copyAugment函数的定义

  1. /**
  2. * Augment a target Object or Array by intercepting
  3. * the prototype chain using __proto__
  4. * 大部分现代浏览器都会走到protoAugment
  5. * 实际上就把value的原型指向了arrayMethods
  6. */
  7. function protoAugment (target, src: Object) {
  8. /* eslint-disable no-proto */
  9. target.__proto__ = src
  10. /* eslint-enable no-proto */
  11. }
  12. /**
  13. * Augment a target Object or Array by defining
  14. * hidden properties.
  15. */
  16. /* istanbul ignore next */
  17. function copyAugment (target: Object, src: Object, keys: Array<string>) {
  18. for (let i = 0, l = keys.length; i < l; i++) {
  19. const key = keys[i]
  20. // 通过Object.defineProperty去定义它自身的属性值
  21. def(target, key, src[key])
  22. }
  23. }

arrayMethods定义在src/core/observer/array.js中

  1. /*
  2. * not type checking this file because flow doesn't play well with
  3. * dynamically accessing methods on Array prototype
  4. */
  5. import { def } from '../util/index'
  6. const arrayProto = Array.prototype // 继承Array
  7. export const arrayMethods = Object.create(arrayProto)
  8. // 数组中所有能改变数组自身的方法
  9. const methodsToPatch = [
  10. 'push',
  11. 'pop',
  12. 'shift',
  13. 'unshift',
  14. 'splice',
  15. 'sort',
  16. 'reverse'
  17. ]
  18. /**
  19. * Intercept mutating methods and emit events
  20. * 对数组中所有能改变数组自身的方法 进行重写
  21. */
  22. methodsToPatch.forEach(function (method) {
  23. // cache original method
  24. const original = arrayProto[method]
  25. def(arrayMethods, method, function mutator (...args) {
  26. // 先执行方法本身原有的逻辑
  27. const result = original.apply(this, args)
  28. const ob = this.__ob__
  29. // 对能增加数组长度的3个方法 push unshift splice做判断
  30. // 获取插入的值
  31. let inserted
  32. switch (method) {
  33. case 'push':
  34. case 'unshift':
  35. inserted = args
  36. break
  37. case 'splice':
  38. inserted = args.slice(2)
  39. break
  40. }
  41. // 把新添加的值变成一个响应式对象
  42. if (inserted) ob.observeArray(inserted)
  43. // notify change
  44. // 手动触发依赖通知
  45. ob.dep.notify()
  46. return result
  47. })
  48. })

很好的解释了调用vm.items.splice(newLength)方法可以检测到变化