3 Vue2 数据响应式,new Vue()时怎么处理传入的data

106 阅读2分钟

数据data作为参数,传入到Vue实例后,有一些特殊的变化

import Vue from "vue/dist/vue"
Vue.config.productionTip = false;
const myData = {
  n:0,
}
console.log("myData_1",myData);
const vm = new Vue({
  data: myData,
  template:`
    <div>{{n}}<button @click="add">+10</button></div>
  `,
  methods:{
    add(){
      this.n += 10;
    }
  }
}).$mount('#app');

setTimeout(() => {
  myData.n += 10;
  console.log("myData_2",myData);
},3000);

以上代码运行结果

myData_1 {n: 0}
myData_2 {__ob__: Observer}

为什么会有这样的变化?


用get和set声明对象中的方法

let obj = {
  姓:"张",
  名:"无忌",
  get 姓名(){
    return this.姓 + this.名;
  },
  set 姓名(name){
    this.姓 = name[1];
    this.名 = name.substring(1);
  },
  age: 18,
}
console.log("输出结果:"+obj.姓名); // 输出结果:张无忌

obj.姓名 = "令狐冲";
console.log("输出结果:"+obj.姓名); // 输出结果:狐狐冲

对象中使用了get声明的函数,使用起来的时候,不需要加括号,就好像使用一个属性,可以理解为虚拟属性;
对象中使用了set声明的函数,使用起来的时候,直接用“=”赋值,就好比给对象的属性赋值;

Object.defineProperty,给对象添加虚拟属性

let obj = {
  姓:"张",
  名:"无忌",
  get 姓名(){
    return this.姓 + this.名;
  },
  set 姓名(name){
    this.姓 = name[0];
    this.名 = name.substring(1);
  },
  age: 18,
}
Object.defineProperty(obj,"infos",{
  get(){
    return this.姓名 + this.age;
  },
  set(value){
    this.姓 = value[0];
    this.名 = value.substring(1,3);
    this.age = value.substring(3)
  }
})
console.log(obj.infos); // 张无忌18
obj.infos = "韦小宝22";
console.log(obj.infos); // 韦小宝22

数据的代理

// 核心数据{n:0}通过代理暴露出来
let myVueData = proxy({data: {n: 0}});
function proxy({data}) {
  const obj = {};
  // 实际上应该通过遍历的方法得到data的所有key,包括n,这里是间写
  Object.defineProperty(obj,'n',{
    get(){
      return data.n;
    },
    set(value){
      if(value<0) return;
      data.n = value;
    }
  })
  return obj; // 代理对象被暴露出来
}

console.log("myVueData.n = " + myVueData.n); // myVueData.n = 0
myVueData.n = -1; // 代理对象设置值时,进行了拦截,没有设置到n上
console.log("myVueData.n = " + myVueData.n); // myVueData.n = 0
myVueData.n = 100;
console.log("myVueData.n = " + myVueData.n); // myVueData.n = 100

核心数据{n: 0}通过代理暴露出来,用对象myVueData读取n的值,可以保证数据安全;

代理的同时,监听原数据

let priData = {n:0}; // 原数据
let proPriData = proxy2({data: priData}); // 代理数据
function proxy2({data}){
  // 删除原数据data中的属性,重新添加
  let value = data.n; // 先把核心数据取出来,实际上应该通过遍历的方法得到data的所有key,包括n,这里是简写
  // Object.defineProperty(data,'n',{})会覆盖原来的属性n,新的属性n取的值和设置的值都与原来的n关联,但是更安全,新的n修改,也更容易被控制
  Object.defineProperty(data,'n',{
    get(){
      return value;
    },
    set(newValue){
      if(newValue < 0) return;
      value = newValue;
      console.log("数据已修改了,n = " + this.n);
    }
  })
  // 以上几句话,让data数据改变,能够被代理对象获知并监听,在程序其他位置修改data的值,代理对象也可以知道(比如外部调用priData.n = 100,实际会调用set方法)
  const obj = {};
  Object.defineProperty(obj,'n',{
    get(){
      return data.n;
    },
    set(value){
      if(value<0) return;
      data.n = value;
    }
  })
  return obj;
}
proPriData.n = 100; // 控制台打印出: 数据已修改了,n = 100
priData.n = 1000; // 控制台打印出: 数据已修改了,n = 100

其实,vue正式这种模式处理数据,比较:

proPriData = proxy2({data: priData}); // 代理数据
vm = new Vue({data:{n:0}}); // vue实例

当我们写vm = new Vue({data:myData})时:

  • 会让vm成为数据myData的代理
  • 会对myData所哟属性值变化进行监听,这样,vm就能得到数据myData的变化,从而调动render()刷新UI,这说明Vue是数据响应式的,能够响应data的变化

开头例子变化的原因:

myData_1 {n: 0} // 此时,n是对象的一个属性
myData_2 {__ob__: Observer} // 对象传入Vue实例后,vm成为了数据的代理对象,数据内部的属性n被删除后,重新加了虚拟属性n,数你属性n是一个观察/监听对象

Vue 的 data 的 bug :视图使用了data没有的属性

import Vue from "vue/dist/vue"
Vue.config.productionTip = false;
const myData = {
  n: 0,
}
console.log("myData_1", myData);
new Vue({
  data: myData,
  template: `
    <div>{{ n }}
    <hr>
    m: {{m}}
    <hr>
    <button @click="add">+10</button>
    </div>
  `,
  methods: {
    add() {
      this.n += 10;
    }
  }
}).$mount('#app');

上面的代码中,不会显示m的值,因为传入给Vue的对象中,没有属性m,Vue也就没法进行代理,设置虚拟属性,页面会报错误警告

vue.js?7193:5076 [Vue warn]: Property or method "m" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: v2.vuejs.org/v2/guide/re…. (found in )

Vue 的 data 的 bug 的规避

  • 规避方法1,在data中把要用的属性声明好,不使用没有声明的属性;
  • 规避方法2,通过Vue.set 和 this.$set 设置没有定义的属性;
const myData = {
  n: 0,
  obj: {},
}
console.log("myData_1", myData);
new Vue({
  data: myData,
  template: `
    <div>{{ n }}
    <hr>
    obj.x : {{obj.x}} &nbsp; <button @click="setObjx">set obj.x</button>
    <hr>
    <button @click="add">+10</button>
    </div>
  `,
  methods: {
    add() {
      this.n += 10;
    },
    setObjx(){
      console.log("setObjx");
      // this.obj.x = "xyz"; // 这是不行的,因为代理对象obj上,没有属性或者虚拟属性x
      this.$set(this.obj, 'x', (this.obj.x | 0) +1);// 这是可以的,增加数据属性,或者给没有定义的属性赋值,Vue会自动创建新的属性
      // Vue.set(this.obj, 'x', 'xyz123');
    }
  }
}).$mount('#app');
  • Vue.set 和 this.$set 的作用:
    • 给对象新增key
    • 自动创建代理和监听
    • 触发UI更新(异步更新)

数组的变更方法

import Vue from "vue/dist/vue"
Vue.config.productionTip = false;
new Vue({
  data: {
    array: ["a","b","c"],
  },
  template: `
    <div>
      {{array}}
      <hr>
      <button @click="setD">setD</button>
    </div>
  `,
  methods: {
    setD(){
      // this.array[3] = "d"; // 不会显示 d ,因为数组 array: ["a","b","c"] 实质上是类似 array: {0:"a",1:"b",2:"c"} 的特殊对象, 没有3这个属性
      // this.$set(this.array,3,"d"); // 会显示d,设置对象没有的属性
      this.array.push("d"); // 会显示d,因为数组内部结果已经被修改了,新数组方法会在数组操作后添加set方法
    }
  }
}).$mount('#app');

代码this.array[3] = "d"不能给数组添加新的元素;
代码this.$set(this.array,3,"d")可以给数组添加新的元素,就是设置对象没有的元素;
代码this.array.push("d")可以给数组添加新的元素,这是因为数组传给Vue后,Vue会修改数组的原型链,加一层原型,加的原型里包括pop、push、reverse、shift、sort、splice、unshift等7个方法,这7个方法会在数组操作后,执行set方法,给新数组元素加监听,Vue文档 数组变更方法
结论:数组新增 key 最好通过 7 个 API