「Vue」对 Vue 的数据响应式研究过程

232 阅读2分钟

本次笔记主要是对 vue 数据响应式过程的研究,研究过程比知识本身更重要,这些方法可以不读源码,也能了解真相.重点在于vue到底对data到了些什么,围绕这个疑问展开我们的推理过程

get 和 set 计算属性


代码实例:

import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

const myData = {
  n: 0,
};
console.log(myData);
const vm = new Vue({
  data: myData,
  template: `
    <div>{{n}}<button @click = "add">+10</button></div>
  `,
  methods: {
    add() {
      this.n += 10;
    },
  },
}).$mount("#xyx");

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


结果:

打印出来的 myData 第一个初始值是{n:0} ,而第二个是{_ ob_: Observer}。
也就是说一开始是{n:0},传给 new Vue 之后立马变成{n:...},但是为什么表现和{n:0}一致呢,我们首先要学习一下 ES6 的 getter 和 setter

举个例子

let obj0 = {
  姓: "高",
  名: "圆圆",
  age: 18,
};

// 需求一,得到姓名

let obj1 = {
  姓: "高",
  名: "圆圆",
  姓名() {
    return this.姓 + this.名;
  },
  age: 18,
};

console.log("需求一:" + obj1.姓名());
// 姓名后面的括号能删掉吗?不能,因为它是函数
// 怎么去掉括号?

// 需求二,姓名不要括号也能得出值

let obj2 = {
  姓: "高",
  名: "圆圆",
  get 姓名() {
    return this.姓 + this.名;
  },
  age: 18,
};

console.log("需求二:" + obj2.姓名);

// 总结:getter 就是这样用的。不加括号的函数,仅此而已。

// 需求三:姓名可以被写

let obj3 = {
  姓: "高",
  名: "圆圆",
  get 姓名() {
    return this.姓 + this.名;
  },
  set 姓名(xxx) {
    this.姓 = xxx[0];
    this.名 = xxx.slice(1);
  },
  age: 18,
};

obj3.姓名 = "刘诗诗";

console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`);
console.log(obj3);

// 总结:setter 就是这样用的。用 = xxx 触发 set 函数


打印出 obj3微信图片_20201010212923.png

所以从这个例子看出 obj3 只有姓,名,get 姓名 和 set 姓名四个属性,并没有姓名这个属性,浏览器在打印这个姓名的时候后面加...,意思就是确实可以对姓名进行读和写,但是并不存在一个姓名的属性。
所以对 n 的解释是并不存在 n 的属性,只是有一个 get n 和 set n 属性来模拟对 n 的操作。

Object.defineProperty


**Object.defineProperty()**  方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

const _xxx = 0;
Object.defineProperty(obj3, "xxx", {
  get() {
    return _xxx;
  },
  //因为xxx是虚拟的通过get创造出来的所有当你返回即return xxx,会回去在调用get()然后再return xxx最后无限循环
  /**
  get(){
  	return xxx
  },
  **/
  set(value) {
    _xxx = value;
  },
});


结果:再次打印出 obj3
微信图片_20201010212944.png

注意:定义的属性 xxx 是不存在!!!

小结:


Object.defineProperty


可以给对象添加属性 value
可以给对象添加 getter/setter
getter/setter 在定义的时候用于对属性的读写进行监控

啥是代理(设计模式)


对 myData 对象的属性读写,全权由另一个对象 vm 负责
那么 vm 就是 mData 的代理
比如 myData.n 不用,偏要 vm.n 来操作 myData.n

vm = new Vue({data:myData})


一.会让 vm 成为 myData 的代理(proxy)
二.会让 myData 的所有属性进行监控
为什么要监控,为防止 myData 的属性变了,vm 不知道
vm 知道了又如何?知道属性变了就可以调用 render(data)呀!
UI = render(data)
代码:

let data0 = {
  n: 0,
};

// 需求一:用 Object.defineProperty 定义 n
let data1 = {};

Object.defineProperty(data1, "n", {
  value: 0,
});

console.log(`需求一:${data1.n}`);
//output 0

// 总结:这煞笔语法把事情搞复杂了?非也,继续看。

// 需求二:n 不能小于 0
// 即 data2.n = -1 应该无效,但 data2.n = 1 有效

let data2 = {};

data2._n = 0; // _n 用来偷偷存储 n 的值

Object.defineProperty(data2, "n", {
  //外部通过data2.n改变里面的值
  get() {
    return this._n; //读data2属性的地方
  },
  set(value) {
    //写data2属性的地方
    if (value < 0) return;
    this._n = value;
  },
});

console.log(`需求二:${data2.n}`);
data2.n = -1;
console.log(`需求二:${data2.n} 设置为 -1 失败`);
data2.n = 1;
console.log(`需求二:${data2.n} 设置为 1 成功`);

// 抬杠:那如果对方直接使用 data2._n 呢?
// 算你狠

// 需求三:使用代理

let data3 = proxy({ data: { n: 0 } }); // 括号里是匿名对象,无法访问

function proxy({ data } /* 解构赋值,别TM老问 */) {
  const obj = {};
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  // 因为我怕你们看不懂
  Object.defineProperty(obj, "n", {
    get() {
      return data.n;
    },
    set(value) {
      if (value < 0) return;
      data.n = value;
    },
  });
  return obj; // obj 就是代理
}

// data3 就是 obj
//把data.n赋值给obj的虚拟n属性 obj代理了data3
console.log(`需求三:${data3.n}`);
data3.n = -1;
console.log(`需求三:${data3.n},设置为 -1 失败`);
data3.n = 1;
console.log(`需求三:${data3.n},设置为 1 成功`);

// 杠精你还有话说吗?
// 杠精说有!你看下面代码
// 需求四

let myData = { n: 0 };
let data4 = proxy({ data: myData }); // 括号里是匿名对象,无法访问

// data3 就是 obj
console.log(`杠精:${data4.n}`);
myData.n = -1;
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`);

// 我现在改 myData,是不是还能改?!你奈我何
// 艹,算你狠

// 需求五:就算用户擅自修改 myData,也要拦截他

let myData5 = { n: 0 };
let data5 = proxy2({ data: myData5 }); // 括号里是匿名对象,无法访问 data5就是myData5的代理

function proxy2({ data } /* 解构赋值,别TM老问 */) {
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  // 因为我怕你们看不懂
  let value = data.n;
  Object.defineProperty(data, "n", {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue < 0) return;
      value = newValue;
    },
  });
  // 就加了上面几句,这几句话会监听 data

  const obj = {};
  Object.defineProperty(obj, "n", {
    get() {
      return data.n;
    },
    set(value) {
      if (value < 0) return; //这句话多余了
      data.n = value;
    },
  });

  return obj; // obj 就是代理
}

// data3 就是 obj
console.log(`需求五:${data5.n}`);
myData5.n = -1;
console.log(`需求五:${data5.n},设置为 -1 失败了`);
myData5.n = 1;
console.log(`需求五:${data5.n},设置为 1 成功了`);

// 这代码看着眼熟吗?
// let data5 = proxy2({ data:myData5 })
// let vm = new Vue({data: myData})

// 现在我们可以说说 new Vue 做了什么了

数据的响应式

微信图片_20201010213004.png

Vue 的 data 是响应式


const vm = new Vue({data:{n:0}})

  1. vue 对 data 做了两件事.
    第一件事:vue 对 data 里的对象进行篡改和监听.
    第二件事:新生成的对象来代理被窜改的对象
  2. 这两件事就是通过 object.definepropery 的 getter setter 方法来实现数据响应式的

响应式网页是啥?


如果我改变窗口大小,网页内容会做出响应,那就是响应式网页

小 tips:view 更新 data 的话,通过事件监听即可,比如 input 标签监听 'input' 事件就可以实现了。

data 的 bug


如果属性一开始就不存在,就不会被监听

new Vue({
  data: {
    obj: {
      a: 0, // obj.a 会被 Vue 监听 & 代理
    },
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      this.obj.b = 1; //请问,页面中会显示 1 吗?
    },
  },
}).$mount("#app");

解决办法


使用 Vue.set 或 this.$set
作用:
新增 key
自动创建代理和监听(如果没创建过)
触发 UI 更新(但并不会立刻更新)

new Vue({
  data: {
    obj: {
      a: 0, // obj.a 会被 Vue 监听 & 代理
    },
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      // this.obj.b = 1; //请问,页面中会显示 1 吗?
      console.log(Vue.set === this.$set);
      Vue.set(this.obj, "b", 1);
      // this.$set(this.obj, 'b', 1)
    },
  },
}).$mount("#app");

对象中新增的 key


Vue 没有办法事先监听和代理
要使用 set 来新增 key,来创建监听和代理,更新 UI
最好提前把属性都写出来,不要新增 key
但数组做不到[新增 key]

数组中新增的 key


也可以用 set 来新增 key,且创建监听和代理,更新 UI
VUE 的 7 个 API 方便你对数组的进行增减——变异方法
这 7 个 API 会自动处理监听和代理,并且更新 UI
注意:这里的7个api并不是本身数组上我们认为的原型方法,已经被尤雨溪进行了更改

变更方法


Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()


你可以打开控制台,然后对前面例子的 items 数组尝试调用变更方法。比如 example1.items.push({ message: 'Baz' })

英语


reference:引用