Vue源码系列-由$set引发的纠纷

239 阅读2分钟

前言

之前一直有读Vue的源码,不过总是一个点,一个点的去看,导致只能在短时间记住,时间一久还是不明所以,想着写一篇由一个点一个点串起来的,争取做到浅显易懂。

例子

<template>
  <div id="root">
    <div>{{obj}}</div>
    <button @click="addObj()">点击</button>
  </div>
</template>

<script>

export default {
  name: "App",
  data() {
    return {
      obj: {
        name: "waiterLin",
        age: 22,
      },
  
    };
  },

  methods: {
    addObj(){
      this.obj.sex = "男";
      console.log('obj',this.obj)
    }

  },
};
</script>

1628234643(1).jpg

1628234701(1).jpg 这在新手开发中是很常见的一个问题,为什么数据更新了,但视图不发生改变?就产生了下列的问题
1.如何去解决数据变了,视图不变的问题?
2.为什么这个方法能够解决这个问题?
3.导致这个问题产生的根本原因是什么?
4.Vue3.x是如何解决这个问题的?

如何去解决数据变了,视图不变的问题(this.$set)?

之所以这个放在第一条,是因为在实际开发中,我和代码,有一项能跑就行,既然我不能跑,那就先让代码跑起来
1631864794(1).jpg 聪明的小伙子,早就不到一分钟就知道该怎么做了,心想:就这?还非得水一篇文章?

 addObj(){
       this.$set(this.obj,'sex','男');
    }

为什么这个方法能够解决这个问题?

我们反手直接打开源码

 //接受3个参数
//数组或者对象,key值,val值
 export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  //判断是数组并且符合有效key值
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    //利用数组的变异方法splice,触发响应式
    target.splice(key, 1, val)
    return val
  }
  //key值是否存在
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  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进行处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

从源码中可以知道,存在两种情况
1.第一个参数为数组时,会调用 splice方法触发更新
2.第一个参数为对象时,会判断是否为响应数据,如果不是直接复制,是的话触发依赖收集(defineReactive)

导致这个问题产生的根本原因是什么?

这就要说到,人们津津乐道的Object.defineProperty,Vue的双向数据绑定的核心就是Object.defineProperty的get和set方法,简而言之,对数据对象进行数据劫持并返回更改后的对象.

由Object.defineProperty实现双向数据绑定的简易代码

function observe(obj, callback) {
    let newObj = {};
    Object.keys(obj).forEach((key) => {
        console.log(key)
        Object.defineProperty(newObj,key,{
            enumerable:true,
            configurable:true,
            get(){
                return obj[key]
            },
            set(newVal){
                obj[key] = newVal
                callback(key,obj[key])
            }
        })
    })
     return newObj
   }
  //进行测试
  const person = observe({
        name: 'waiterLin',
        sex: '男'
    },
    (key, value) => {
        console.log(`${key}的值被修改为${value}`)
    }
)

person.name = 'watermelon'
person.sex = '女'

//name的值被修改为watermelon
//sex的值被修改为女

但我们发现Object.defineProperty无法对新增的对象属性进行监听,并且实际上是对Object的key值进行监听,所以需要循环遍历,性能上有所偏颇.

Vue3.x是如何解决这个问题的?

vue3.x采用了Proxy进行重写

关于Proxy

1.Proxy监听的是对象,而不是对象的属性,所以即使对象属性新增了也能监听的到,也就不需要this.$set这种操作
2.Proxy的hander的对象方法有13种,这是Object.defineProperty所不具备的
3.Vue3.x中直接返回的是一个Proxy对象,所以可以不用遍历也能监听到对象的改变

由Proxy实现双向数据绑定的简易代码

function proxyObj(obj,fn){
  return new Proxy(obj,{
    get(target,key){
      return target[key]
    },
    set(target,key,val){
      target[key] = val;
      fn(key,val)
    }
  })
}
  //进行测试
  const person = proxyObj({
        name: 'waiterLin',
        sex: '男'
    },
    (key, value) => {
        console.log(`${key}的值被修改为${value}`)
    }
)

person.name = 'watermelon'
person.sex = '女'

//name的值被修改为watermelon
//sex的值被修改为女

两者进行比较,明显看出由Proxy重写的方法简易了不少。

最后

希望大家看完之后,有一定的收获,写的不好地方也欢迎指出,共同进步.打工人,奥利给。

1631864259(1).jpg