一个栗子引发的对this.$set的思考

255 阅读2分钟

一、从一个栗子开始

​ 先看一个简单的例子,代码如下:

<template>
  <div id="app">
    <div class="student-info">
      <p>name: {{ student.name}}</p>
      <p>age: {{ student.age}}</p>
    </div>
    <button @click="addAgeHandler">Add Age</button>
    <button @click="modifyAgeHandler">Modify Age</button>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      student: {
        name: "Alex"
      }
    };
  },
  methods: {
    // case 1
    addAgeHandler() {
        this.student.age = 18;
    }
    // case 2
    addAgeHandler() {
        this.$set(this.student, "age", 18);
    }

    modifyAgeHandler() {
      this.student.age = 20;
    }
  }
};
</script>

​ 在上述代码中,我写了两个addAgeHandler函数,两个函数中都会将student修改为{name:'Alex',age:18}。但是这两个函数的效果是一样的吗,是否会将age的数值正确的显示在页面上呢? ​

​ 实际的情况是:

  • case 1: 点击Add Age后不显示age的数值,点击Modify Age后依旧不显示age的数值

  • case 2: Add Age和Modify Age都会正常显示age的数值

    不知道这是不是有些出乎你的意料,你可能会疑惑为什么case 1中student.age修改后没有渲染到页面上,接下来将为你解答。

二、case 1的问题分析

​ 要搞清楚为什么case 1无法触发age渲染到页面上,我们先要弄明白数据与渲染函数之间的响应式关系是如何建立的。

  1. vue组件初始化时会对data中的所有数据进行observe,observe时会用Object.defineProperty对数据的get和set方法进行拦截,在get中进行依赖收集,在set方法中触发依赖更新;

  2. 在vue组件渲染时,template中使用的变量会被读取,从而触发上一步中设置的get方法,这样就将渲染函数添加到了变量的依赖列表中;

  3. 当变量被修改时,执行set方法,触发其所有的依赖进行更新,渲染函数被重新执行,组件被重新渲染。

    ​ 在我们的的代码中,初始化时student中没有age属性,因此age并没有被observe,从而在页面渲染时即使template中有{{student.age}}也无法将渲染函数收集到age的依赖列表中(因为studet.age没有被observe,没有拦截后的get方法执行,当然也无法收集依赖了)。

    ​ 这就是为什么case 1中虽然修改了student.age,但是无法触发vue组件重新渲染,如果想使用case 1达到重新渲染的目的,可以这样处理:

data() {
    return {
        student: {
            name: "Alex",
            age:undefined
        }
    };
}

​ 初始化时就明确定义student.age属性,那么在初始化时就会对age属性进行observe,在vue组件渲染时触发了依赖收集,从而修改student.age会触发组件重新渲染。

三、case 2的原理分析

​ 接下来我们来看case 2为什么可以触发组件重新渲染,在case 2中使用Vue提供的全局Api-this.$set完成student.age赋值,看一下set方法简化版的源码

// vue源码\src\core\observer\index.js
export function set (target, key, val) {
  // 省略target是array情况下的处理
  ......

  // target的observer
  const ob = (target).__ob__
  
  // 将target.key设置成响应式
  defineReactive(ob.value, key, val)
  // 通知target的依赖更新
  ob.dep.notify()
  return val
}

​ 我们理一下代码的执行顺序:

  1. this.$set(this.student,'age',18)执行;

  2. defineReactive将student.age设置成响应式,对其添加了get和set拦截;

  3. ob.dep.notify()通知student的依赖进行更新,student的依赖中包含了组件的渲染函数,从而触发组件重新渲染 ( 或许你会渲染函数何时被收集到student的依赖中了,这是因为渲染时会访问student.name和student.age,肯定是先访问student,才能访问到student的属性,所以student的get方法会被调用,渲染函数也就被收集到student的依赖中了 ) ;

  4. 组件渲染过程中,遇到变量student.age,触发student.age的get拦截,从而在student.age的依赖中加入组件的渲染函数,完成依赖收集;所以在modify age的时候,直接修改student.age就可以触发组件的重新渲染啦。

​ 值得注意的是,在case 2的addAgeHandler中,引发组件重新渲染的是ob.dep.notify()这句代码,即student的依赖更新导致,而不是student.age的依赖更新导致。modifyAgeHandler中引发组件渲染的才是student.age收集的依赖。虽然都是渲染函数的重新执行,但是触发的主体不一样。