附:「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」
深入理解 options .data
内存图
Vue对data做了什么?
重要议题,后面的代码会围绕这个问题进行扩展,看完所有代码后,再回来思考下这个问题。
1. 前 log 和 后 log (问题起源)
这个代码做了两个事情:
(1)改变了 n 的值
(2)输出log,实例前 和 实例后
// 引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
const myData = {
n: 0
};
console.log(myData); // 本节课精髓
new Vue({
data: myData,
template: `
<div>
{{n}}
<button @click="add()">+10</button>
</div>
`,
methods: {
// 第二种方式
add() {
// myData.n += 10;
this.n += 10;
}
}
}).$mount("#app");
// 第一种方式
setTimeout(() => {
myData.n += 10;
console.log(myData); // 本节课精髓
}, 3000);
代码:codesandbox.io/s/goofy-ban…
我们要做的就是分析这两个 log输出的结果到底是不是一样的?
先说在前面,我们主观认为它们都是一样的,感觉没有什么不同,但是实际前后的log是不同的。
前log:
后log:
跟前log一点都不一样,还多了这么多东西!
- n:(...)是什么鬼?
- get 和 set 又是什么?
带着这个疑问,继续看下面的代码 ↓ ↓ ↓
2. getter 和 setter
- 这个语法是ES6中的
- get 和 set 可以对一个属性进行读写(这个属性是虚拟的,我们暂且管他叫虚拟属性把)
我们写了一个 obj1 的对象,在这个 obj1对象中写了一个 xm 函数,函数中的代码指向外面的变量,最后调用这个函数输出结果
let obj1 = {
x: "何",
m: "顺昌",
xm(){
return this.x + this.m;
}
}
console.log(obj1.xm()) // 何顺昌
需求1:能不能不加函数括号拿到值?
我:没问题!
let obj2 = {
x: "何",
m: "顺昌",
get xm(){
return this.x + this.m;
}
}
console.log(obj2.xm) // 何顺昌
解析:只需要在函数前面加一个 get,这样我们获取这个值的时候就可以不用加括号,直接当属性来使用即可
需求2:你这个只能读么?给我加一个改的功能!
我:ok (有get 就有 set)
let obj3 = {
x: "何",
m: "顺昌",
get xm() {
return this.x + this.m;
},
set xm(xxx) {
this.x = xxx[0];
this.m = xxx.substring(1);
}
};
obj3.xm = "何小明";
console.log(`姓:${obj3.x},名:${obj3.m}`) // 姓:何,名:小明
解析:
- 在函数前加一个 set 就可以修改值了,当我们
obj3.xm = "何小明"相当于就触发了这个 set 函数。
看完这个代码,你看懂 后log的问题了么?
我们把上面代码的 obj3 打印出来看下
console.log(obj3)
输出结果:
发现多了一个属性,我们的代码也没有这个属性呀?
再回头看下我们最开始的问题!===> 后log
发现它们很相似,有xm:(....) ,有 get 和 set
所以我初步判定:后log之所以多了这么多东西,应该是添加了 get 和 set
问题:什么时候被做的手脚?我咋不知道?!
带着什么时候做手脚的疑问,继续看下面的代码 ↓ ↓ ↓
3. Object.defineProperty
在定义一个对象之后,你还想在额外添加一个虚拟属性,就可以使用这个函数
代码
声明了一个 data0对象,这个对象里有一个 key:n,value:0
let data0 = {
n:0
}
需求1: 用 Object.defineProperty 定义 n(实现上面代码的效果)
let data1 = {};
// 在data1上添加一个n属性,这个n属性的值是0,不是value0!
Object.defineProperty(data1, "n", {
value: 0
});
console.log(`需求一:${data1.n}`);
吐槽:这煞笔语法把事情搞复杂了?(我直接用data0写法不可以么)非也,继续看。
需求2:n不能小于0 (即 data2.n = -1 无效 ,data2.n = 1有效 )
let data2 = {};
data2._n = 0; // _n 用来偷偷存储 n 的值
Object.defineProperty(data2, "n", {
get() {
return this._n; // 读,_n存着真实的值
},
set(value) {
if (value < 0) return; // 小于0白写
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 呢?
我:算你狠!
需求3:使用代理
let data3 = proxy({ data: { n: 0 } }); // 括号里是匿名对象,无法访问
function proxy({ data }) { // 这里的data就是上面代码的 n:0
const obj = {};
// 对上面这个 obj添加一个 n属性,此时对n的操作就是对data的操作
Object.defineProperty(obj, "n", {
get() {
return data.n;
},
set(value) {
if (value < 0) return;
data.n = value;
}
});
return obj; // obj 就是data的代理
}
// data3 就是 obj
console.log(`需求三:${data3.n}`);
data3.n = -1;
console.log(`需求三:${data3.n},设置为 -1 失败`);
data3.n = 1;
console.log(`需求三:${data3.n},设置为 1 成功`);
解析:只暴露了代理对象(房产中介),没有暴露真实对象(房东)。(就像你没有办法和房东直接说话,只能通过中介来把话传递)
我:杠精还有话说吗?
抬精说:有,你看下面代码!
需求4:
声明了一个变量,把变量当作引用
let myData = { n: 0 };
let data4 = proxy({ data: myData }); // 这里的myData 就是 n:0
console.log(`杠精:${data4.n}`); // 0
myData.n = -1;
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`); // -1
抬精说:我现在改 myData,是不是还能改?!你奈我何
我:艹,算你狠
我现在要解决的问题:
- myData.n = -1; 的时候也能对它做一个限制
- 就好像我跟房东说,只要有租客找你,你都要告诉我它准备干嘛,因为我们俩签了合同的,租客只能是我的,
需求5:就算用户擅自修改 myData,也要拦截它 (这个需求是100%拦截!)
- 就好像我跟房东说,只要有租客找你私自签合同,你都要告诉我,因为我们俩是签了合同的,即使您跟租客签了合同,中介费你也要一样给我。
let myData5 = { n: 0 };
let data5 = proxy2({ data: myData5 }); // 括号里是匿名对象,无法访问
function proxy2({ data }) {
// 1. 监听 myData5
let value = data.n; // 保存最开始的n,也就是n:0
Object.defineProperty(data, "n", {
get() {
return value;
},
set(newValue) {
if (newValue < 0) return; // 如果新值小于0,就不赋值
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 成功了`);
解析:
- 这个100%拦截就可以得出我们做手脚的那个问题了!!(看一下这个代码是不是很眼熟?)
- 所以基本判定是 new Vue 做的手脚,当我们把mydata传给 Vue,Vue就对其做了手脚,这也是为什么前后打印出的log有不同的原因!
小结
(1)Object.defineProperty
- 可以给对象添加属性 value
- 可以给对象添加 getter 和 setter
- getter / setter 用于对属性的读写进行监控
(2)啥是代理?(设计模式)
- 对 myData 对象的属性读写,全权由另一个对象 vm 负责
- 那么 vm 就是 myData 的代理(类似房东的房子让房产中介来管,vm就是房产中介)
- 比如 myData.n 不用,偏要用 vm.n 来操作 myData.n
(3) vm = new Vue({data:myData})
- vm 会成为 myData的代理(proxy)
- 会对 myData的所有属性进行监控
问题:
- 为什么要监控? ===> 是为了防止 myData 的属性变了,vm不知道
- vm知道了又如何? ===> 知道属性变了就可以调用render(data)呀!
- UI = render(data) ===> 视图发生变化
示意图(100%拦截)
- 注意,全程都是在数据({n:0n:0})的上面进行get 和 set的,并没有直接覆盖删除数据({n:0n:0})
- 对这个({n:0n:0})进行监听,监听后得到的是监听后的对象
- 此时再对监听后的对象进行代理
- 代理全程负责数据({n:0n:0})的读和写
数据响应式
1. 响应式
什么是数据响应式?
- 我打你一拳,你会喊疼,那你就是响应式的
- 即一个物体对外界的刺激做出反应,那它就是响应式的。
- 事事有回应
Vue中的 data是响应式
const vm = new Vue({data:{n:0}})- 我如果修改 vm.n,那么 UI中的 n 就会响应我
- Vue2是通过 Object.defineProperty 来实现数据响应式的
响应式网页
如果我改变网页窗口大小,网页内容会做出响应,这个就是响应式网页
2. Object.defineProperty有两个bug(无法监听,也就是无法做到数据响应式)
(1)对象中新增key(避免:提前写好)
Object.defineProperty(obj,"n",{...})
必须要由一个 "n",才会监听 & 代理 obj.n 对吧!
假设这个前端开发者很菜,没有给 n 怎么办?
情况一:开发者没有定义 n 直接使用
- 代码:codesandbox.io/s/empty-fie… (Vue会给出一个警告)
情况二:后添加
- 代码:codesandbox.io/s/youthful-… (Vue只会检查第一层)
- 如果我点击 set b,视图会显示 1吗? ==> 不会,因为Vue没法监听一开始不存在的 obj.b
解决办法
- 提前把 key 声明好
- 使用 Vue.set 或者 this.$set
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
// this.$set(this.obj, "b", 1);
Vue.set(this.obj, "b", 1); // 使用Vue.set
}
}
}).$mount("#app");
输出结果:点击set b 后显示了数字1
- Vue.set做了什么?
- 新增 key
- 自动创建代理和监听(如果没有创建过)
- 触发 UI 更新(但并不会立刻更新,因为是异步函数)
(2)数组中新增key(解决办法:使用尤雨溪的变更方法对其进行增删改查操作)
- data中有数组怎么办?
- 你没办法提前声明所有 key
- 示例1,数组的长度可以一直增加,下标就是 key
- 所以我们没有办法提前把数组的key都声明出来
- Vue也不能 检测对你新增了小标
- 难道每次改数组都要用 Vue.set 或者 this.$set?
- 尤雨溪的做法(新增了一层原型)
- 篡改了数组的API,见文档:变更方法
- 这 7个API都会被Vue篡改,调用后会更新UI
- 篡改的代码如下:ES6 / ES5
ES5新增原型写法
const vueArrayPrototype = {
push: function () {
console.log(" push ");
return Array.prototype.push.apply(this, arguments);
}
};
vueArrayPrototype.__proto__ = Array.prototype;
const array = Object.create(vueArrayPrototype);
array.push(1);
ES6新增原型写法
class VueArray extends Array {
push(...args) {
const oldLength = this.length; // this就是当前数组
super.push(...args);
console.log(" push ");
for (let i = oldLength; i < this.length; i++) {
Vue.set(this, i, this[i]);
// 将每个新增的 key 告诉 value
}
}
}
Vue;
3. 总结
对象中新增的key
- Vue没有办法事先监听和代理
- 所以建议是提前写好
数组中新增更多key
- 尤雨溪篡改了7个API方便我们对数组进行增删