提问

有一个小伙伴和我分享了一道面试题:哪些情况改变data的值,视图不会发生变化?想必我们的项目中都会碰到过这样的问题,我写了一个小demo,data有对象,有字符串数组,有数组对象,又在method里写了一些按钮可以修改对象的值,请问以下哪些修改方式能够直接触发视图更新?

<template>
  <div>
    <div class="objectString">
        <span>{{objectString.id}}</span>
        <button @click="ObjectString_change">change</button>
    </div>
    <div class="arrayNumber">
        <span v-for="item in arrayNumber" :key="item">{{item}}</span>
        <button @click="ArrayNumber_change">change</button>
    </div>
    <div class="arrayString">
        <span v-for="item in arrayString" :key="item">{{item}}</span>
        <button @click="ArrayString_change">change</button>
    </div>
    <div class="arrayObject">
        <span v-for="item in arrayObject" :key="item.id">{{item.id}}</span>
        <button @click="ArrayObject_change">change</button>
    </div>
    <div class="arrayObject">
        <span v-for="item in arrayObject2" :key="item.id">{{item.id}}</span>
        <button @click="ArrayObject2_change">change</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      objectString:{ id:0 },
      arrayNumber:[0,1],
      arrayString:["a","b"],
      arrayObject:[{ id:0 },{ id:1 }],
      arrayObject2:[{ id:0 },{ id:1 }],
    }
  },
  methods: {
    ObjectString_change(){
        this.objectString['id'] = 99
    },
    ArrayNumber_change(){
        this.arrayNumber[0] = 99
    },
    ArrayString_change(){
        this.arrayString[0] = 99
    },
    ArrayObject_change(){
        this.arrayObject[0].id = 99
    },
    ArrayObject2_change(){
        this.arrayObject2[0] = { id:99 }
    },
  }
};
</script>
<style>
    span{
        color:blue;
        padding-right:20px;
    }
</style>

答案是只要是通过数组索引修改都不能触发视图更新( data[0] ),通过修改对象的property都能触发更新( data['id'] )。其实官网已经给出了答案,但我还是想深究一下为什么。

引用Vue官网的解释

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

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

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

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新:

1.Vue.set(vm.items, indexOfItem, newValue)

2.vm.items.splice(indexOfItem, 1, newValue)

知识储备

Object.defineProperty

我们都知道Vue能双向绑定的关键点是:对data上所有property全部 Object.defineProperty 一遍,在data值发生改变的时候会触发 setter 函数内的代码。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。那究竟是谁做了 Object.defineProperty 这件事,为何修改数组下标不会触发,我们只能去GitHub搜索vue的源码一探究竟了。

监听数组

在此之前,了解[Object.defineProperty](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)的基础知识是非常重要的,来看一个监听数组的例子:

let arr = [0,1,2];
/*
key:"0",value:0,
key:"1",value:1,
key:"2",value:2,
*/
for(key in arr){
  observer(key)
}
function observer(key){
    Object.defineProperty(arr,key,{
        set(){
            console.log("set the value")
        },
        get(){
            console.log("get the value")
        }
    })
}

浏览器运行一下,在控制台输入arr[0]=100触发set,输入arr[0]触发get。说明数组通过下标修改是可以被监听的到的!

image.png

Vue源码窥探

我带着“为什么vue不能监听”的疑问阅读了vue源码后发现:原因是Vue根本没有 defineProperty 数组的key!

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

export function observe (value: any, asRootData: ?boolean): Observer | void {
  //不是object类型跳出函数
  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
}

结论

对比我们写的例子可以发现:我们是遍历数组的 key 逐个defineProperty,而他是遍历数组的 value 逐个 defineProperty(而且只有在value类型为 Object 的情况下才会继续),所以通过修改数组的索引 arr[0]=100 自然就不能触发set函数。至于不监听是因为当数组长度过长时,性能的代价与用户体验收益不成正比。
Vue对此提供一种解决方案,通过数组上的 push 等方法触发视图的更新,Vue 做了什么大伙应该能猜得到,它在 vue/observer/array.js 重写了 push 等方法挂载到了 Array.prototype 上。

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
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
  })
})

通过Observer 构造器挂载到实例上。

//vue/src/core/observer/index.js
import { arrayMethods } from './array'
export class Observer {
  constructor (value: any) {
    //...省略一些代码
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
    }
  }
  //省略一些方法
}
function protoAugment (target, src: Object) {
  target.__proto__ = src
}
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])
  }
}

拓展阅读

接下来想知道更多细节的小伙伴可以跟我把整个流程走一遍。

  1. 创建vue实例

vue/src/core/instance/index.js
image.png

  1. 初始化state

data就是我们常说的要写成函数的形式,返回值传入observe函数。

//vue/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data={}
    //...此处隐藏一些无关代码
  }
    //...此处隐藏一些无关代码,代理data到实例上
  // observe data
  observe(data, true /* asRootData */)
}

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()
  }
}
  1. function observe() 和 class Observer

判断data对象原型链上有没有__ob__属性,一上来肯定是没有的,走new Observer构造器,value值是data对象。
Observer构造器会递归地defineProperty数据,如果是对象依次将 key 传入defineReactive(),是数组则遍历数组的值,值是个对象接着走Observer构造器的老路,不是对象就over,递归用的妙啊。

//vue/src/core/observer/index.js
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)
  }
    //...此处隐藏一些无关代码
  return ob
}

//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) {
    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])
    }
  }
}
//def函数
function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

绿豆凉茶
1 声望0 粉丝