Vue $set 分析

提问

Vue 2.x 里劫持对象仍用 Object.defineProperty() 方法,受此方法的限制,Vue 无法检测到直接数组更改(mobx 旧版本也有过这毛病,他对数组0到999项的内容都现实地 Object.defineProperty 监听了),和对象属性的添加或删除。

举个例子,运行下面代码,能看到点击 changeArr 按钮,页面没有变化,而点击 setArr 按钮,页面上显示的数据就变了。

<!DOCTYPE html>
<html>
<head>
    <title>Form Demo</title>
</head>
<body>
    <div id="app">
        <div>{{arr[0]}}</div>
        <button @click="changeArr">changeArr</button>
        <button @click="setArr">setArr</button>
    </div>

    <!-- Vue.js v2.6.11 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                arr: [1, 2, 3, 4]
            },
            methods: {
                changeArr() {
                    this.arr[0] = 11;
                },
                setArr() {
                    Vue.set(this.arr, 0, 12);
                }
            }
        })
        console.log(app);
    </script>
</body>
</html>

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

结论

  • 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  • 如果目标是对象,判断属性存在,即为响应式,直接赋值
  • 如果 target 本身就不是响应式,直接赋值
  • 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理,并通知被观察者 ob.dep.notify()

附录

Vue 2.6.2 源码

/**
 * 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 (Array.isArray(target) && typeof key === 'number') {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  /*如果是一个对象,并且已经存在了这个key则直接返回*/
  if (hasOwn(target, key)) {
    target[key] = val
    return val
  }
  /*获得target的Oberver实例*/
  const ob = (target : any).__ob__
  /*
    _isVue 一个防止vm实例自身被观察的标志位 ,_isVue为true则代表vm实例,也就是this
    vmCount判断是否为根节点,存在则代表是data的根节点,Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)
  */
  if (target._isVue || (ob && ob.vmCount)) {
    /*  
      Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)。
      https://cn.vuejs.org/v2/guide/reactivity.html#变化检测问题
    */
    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
  }
  /*为对象defineProperty上在变化时通知的属性*/
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
posted @ 2020-03-25 11:20  Ever-Lose  阅读(522)  评论(0编辑  收藏  举报