看个栗子:
const myData = {n : 0}console.log(myData)new vue({}).$mount('#jeff')
声明一个 mydata 对象, consolog.log 之后如下:
但是如果把 mydata 传入vue实例里则如下:
const myData = {n : 0}console.log(myData)new vue({data:myData}).$mount('#jeff')
则打印出来的 mydata 为:
n的值为什么会从数字 0 变成 (...)
一开始是 {n:0} ,传给 new Vue 之后立马变成 {n:(...)} ,这是什么东西?
在解释之前先了解ES6的 getter和setter
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.名}`)// 总结:setter 就是这样用的。用 = xxx 触发 set 函数
但。。。这和 (...) 有什么关系?
可以把需求三打印出来console.log(obj3) 如图
看,也出现 (...) ,说明之前的栗子里出现的 (...) ,也是一个getter和setter。
而且在代码里,并没有 姓名:(...) ,在浏览器里却出现了,说明可以对这个不存在的属性进行读和写。
读和写的操作就是getter和setter。
Object.defineProperty
这是ES6新语法:官方文档
以刚刚的需求三为栗子:
如果我想对需求三添加一个新的getter和setter如下:
let _xxx = 0Object.defineProperty(obj3, 'xxx',{get(){return _xxx},set(value){_xxx = value}})
xxx是随便起的虚拟属性,意思是给obj3添加一个xxx的属性_xxx是存放xxx的属性的
小结:可以给对象添加属性value,并且给对象添加getter/setter,然后对属性的读和写进行监控。
代理和监听
看栗子:
基础
let data0 = {n: 0}
需求一:用Object.defineProperty定义n
let data1 = {}Object.defineProperty(data, 'n',{value: 0})console.log(`需求一:${data1.n}`)//为什么我要写这么复杂的语法,就不可以直接n:0呢?
需求二:n 不能小于 0,即 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}data2._n = 0 // _n 用来偷偷存储 n 的值Object.defineProperty(data2, 'n', {get(){return this._n},set(value){if(value < 0) returnthis._n = value}})console.log(`需求二:${data2.n}`)data2.n = -1console.log(`需求二:${data2.n} 设置为 -1 失败`)data2.n = 1console.log(`需求二:${data2.n} 设置为 1 成功`)//那如果对方直接使用 data2._n 呢?
需求三:使用代理
let data3 = proxy({data:{n:0}}) //括号里是匿名对象,无法访问function proxy({data/*使用了解构赋值*/}){const obj = {}// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化Object.defineProperty(obj, 'n', {get(){return data.n},set(value){if(value < 0)returndata.n = value}})return obj //obj就是代理}// data3 就是 objconsole.log(`需求三:${data3.n}`)data3.n = -1console.log(`需求三:${data3.n},设置为 -1 失败`)data3.n = 1console.log(`需求三:${data3.n},设置为 1 成功`)//如果把匿名函数改一下。。
说明上面的解构赋值:
//完整写法是function proxy(opction){ //接受一个opction参数。const data = opction.data;}//解构赋值写法function proxy(opction){ //接受一个opction参数。const {data} = opction;}//简化解构赋值的写法function proxy({data}){//直接省略}
:::success 什么是代理?(设计模式)
- 对
myData对象的属性读写,全权由另一个对象vm负责。 - 那么
vm就是myData的代理。 - 比如
myData.n不用,偏用vm.n来操作myData.n:::
需求四:
let myData = {n:0}let data4 = proxy({data:myDate}) //括号里是匿名对象,无法访问。。。。。。// data3 就是 objconsole.log(`杠精:${data4.n}`)myData.n = -1console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)//杠精居然成功了,可以使data4小于0。还有什么办法呢?
需求五:如何拦截杠精,就算用户擅自修改 myData,也要拦截他
let myData5 = {n:0}let data5 = proxy2({data:myData5})function proxy2({data}){let value = data.nObject.defineProperty(data, 'n',{get(){return value},set(newValue){if(newValue<0)returnvalue = newValue}})// 就加了上面几句,这几句话会监听 dataconst obj = {}Object.defineProperty(obj, 'n', {get(){return data.n},set(value){if(value<0)return//这句话多余了data.n = value}})return obj // obj 就是代理})console.log(`需求五:${data5.n}`)myData5.n = -1console.log(`需求五:${data5.n},设置为 -1 失败了`)myData5.n = 1console.log(`需求五:${data5.n},设置为 1 成功了`)
这代码看着眼熟吗?
let data5 = proxy2({ data:myData5 })let vm = new Vue({data: myData})
现在我们可以说说 new Vue 做了什么了
vm = new Vue({data: myData})
- 会让
vm成为myData的代理(proxy) - 会对
myData的所有属性进行监控 - 如果
mydata的属性变了,就可以调用render(data)。
如图:
什么是响应式?
若一个物体能对外界的刺激做出反应,那么就是响应式。
而Vue的data是响应式,即通过 Object.defineProperty 来实现数据响应式。
bug
Vue有个小bug,是Object.defineProperty 问题。
假如,有个比较水的前端工程师没有给 'n' 怎么办?必须要有一个 'n' 才能监听&代理 obj.n 才行,如下代码:
new Vue({data: {},template: `<div>{{n}}</div>`}).$mount("#app");
data里没有 'n' 属性,Vue只会提出警告。
这时,有个方法可以绕过这个警告
new Vue({data: {obj: {//vue的警告只会检查第一层,也就是这里。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的警告只会检查第一层,所以只需要在data下再声明一个对象obj,然后在obj对象里存放数据,就会绕过vue的警告。这是一个bug。
因此调试错误很难发现问题。
解决方案:
使用vue提供的api —— Vue.set 和 this.$set ,代码如下:
new Vue({。。。。。。。。methods: {setB() {Vue.set(this.obj, 'b', 1 ) //请问,页面中会显示 1 吗?//orthis.$set(this.obj, 'b', 1)//和上面一样的}}}).$mount("#app");
这两个api的作用:
- 新增key
- 自动创建代理和监听,前提没有创建过。
- 触发UI更新(但不会立即更新)
因为Vue没办法事先监听和代理,所以使用set来新增key,自动创建代理和监听,UI更新。
建议:最好事先把属性写出来,不要新增key。
数组变异
栗子:
new Vue({data: {array: ["a", "b", "c"]},template: `<div>{{array}}<button @click="setD">set d</button></div>`,methods: {setD() {this.array[3] = "d"; //请问,页面中会显示 'd' 吗?// 等下,你为什么不用 this.array.push('d')}}}).$mount("#app");
- 数组,长度可以一直增加,下标是key。没办法提前把数组的key都声明出来。
- Vue也不能检测并新增下标。
- 要是每次都使用
Vue.set或者this.$set那也太傻了。
解决办法
使用Vue提供的篡改数组api,见官方文档:数组检测更新
push()pop()shift()unshift()splice()sort()reverse()
例如:
new Vue({data: {array: ["a", "b", "c"]},template: `<div>{{array}}<button @click="setD">set d</button></div>`,methods: {setD() {this.array.push("d");}}}).$mount("#app");
这样就可以解决问题了。但又有了新的问题:上面Vue提供的7种方法和JS的有什么区别?
查看开发者工具,如图:
和JS的不一样。而JS的七种方法在 __proto__:Array 下面。
如果使用了Vue提供的这七种api会做两件事情:
- 先调用JS对应的方法
- 然后在自动创建监听和代理。
其实就是添加了一个原型链,没有vue之前原型链是直接是Array对象,有了vue之后在连接Array对象直接新增一个vue的api对象。
