vue-$set的实现方式

51 阅读2分钟

引出

  • 当我们给响应式的对象新增属性时,新增的属性并不会渲染到页面中
  • 对于响应式的数组,增加元素、修改数组长度时,数组的这些变化也不会反映到页面中

那么如何让新增的对象或数组实现响应式及时渲染页面呢?

使用this.$set()

官方定义

Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value)方法将响应属性添加到嵌套的对象上。

// Vue.set(object, key, value) 
<template>
    <div>{{obj.k}}</div>
</template>
<script>
export default {
    data() {
        return {
            obj: {
                s: '1',
                z: '2'
            }
        }   
    },
    mounted() {
        this.$set(this.obj, 'k', '3')
    }
}
</script>

$set原理

源码:

// src/core/observer/index.js
function set(target: Array<any> | Object, key: any, val: any): any {
  // isUndef 是判断 target 是不是等于 undefined 或者 null 。
  //isPrimitive 是判断 target 的数据类型是不是 string、number、symbol、boolean 中的一种
  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
  }

  // vue给响应式对象(比如 data 里定义的对象)都加了一个 __ob__ 属性,
  // 如果一个对象有这个 __ob__ 属性,那么就说明这个对象是响应式对象,我们修改对象已有属性的时候就会触发页面渲染。
  // 非 data 里定义的就不是响应式对象。
  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
}

  • vue的set方法默认传入三个参数,需要修改的对象引用target,对象的键值(数组的indexkey,要修改的值val。如果传入的targetvue本身、set方法默认传入三个参数,需要修改的对象引用target,对象的键值(数组的index)key,要修改的值val。如果传入的target为vue本身、 data,或者boolean,string,number,symbol等原始数据类型,则修改无效。
  • 对于数组或者对象已有属性的修改,直接修改相应的值。
  • 对于新的属性值,如果操作对象是可观测数据,则将属性添加为可观测属性值,并主动触发通知。如果是普通对象,则直接修改相应的属性值。

$delete的实现方式也是差不多的

上面代码里面已经注释了一些其他情况的分析,这边我们重点分析下面这段代码:

const ob = (target: any).__ob__
if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()

首先会判断target对象的__ob__属性是否存在。那这个__ob__属性是个啥?当vue对数据进行观测时会给这个数据增加一个不可枚举的__ob__属性,值里面包含了三个属性value,dep,vmCount三个属性。下图是数据data进行数据观测之后的数据结构的变化。

关于__ob__属性的dep是如何收集依赖的可以去看一下src/core/observer/index.js文件里的defineReactive方法。

const data = {
  a: 1
};

const data = {
  a: 1,
  __ob__: { // __ob__是不可枚举属性
    value: data, // 这边的value是指向data数据本身的
    dep: dep实例对象, // new Dep(),收集了观测data对象的所有watcher
    vmCount: 0
  }
}

如果target对象不存在__ob__属性就说明target对象并不是一个响应式的数据,所以我们只需要对属性进行修改,然后返回值就可以了。

如果target是一个响应式的数据,那我们需要用defineReactive方法去给target增加一个响应式的属性,然后ob.dep.notify()通知所有观测了data数据的watcher,触发视图更新。

defineReactive方法是vue实现数据观测的主要方法。主要是将数据对象的数据属性转换为访问器属性,从而来收集和触发依赖。源码在src/core/observer/index.js文件里的,这边就不多进行解释了。