Vue包括
- Vue: 基础用户界面
- Vue-cli: 脚手架, 用于工程化开发
- vue-router: 前端路由
- vuex: 数据保管服务
Vue2核心
Vue是一套用于构建用户界面(数据->页面)的渐进式(自底向上)JavaScript框架
- 采用组件化的模式构建: 每一个组件都是一个
.vue文件, 文件包含了组件需要的JS/CSS/HTML - 声明式编码: 在写代码的时候不需要实现考虑DOM的实现, 也无需直接操作DOM元素(像我们之前使用的NodeJS的拼串实际上就是命令式编码)
- 采用虚拟DOM+Diff算法, 尽量复用DOM节点(在内容发生变化的时候优先在虚拟DOM中修改, 之后使用Diff算法比较变化后有哪些元素发生变化, 只对变化的元素进行更新)
引入Vue开发版: <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
引入后, global就增加了Vue对象
Vue2的全局配置可以在Vue.config中修改,
HelloWorld
一个简单的实例调用Vue
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="root">
<h1>hello, {
{name}}</h1> <!--4. 插值语法将数据写入h1-->
</div>
</body>
<script>
new Vue({
// 1. 创建一个Vue实例, 参数是配置对象
el: '#root', // 2. 选中MVVM到的元素, 参数可以是CSS选择器, 也可以是JS的元素对象(getElement...)
data: {
// 3. 需要绑定的数据, 可以是一个对象
name: "Liu"
}
});
</script>
</html>
尝试修改
<div id="root">
<h1>hello, {
{name}}</h1>
<h1>hello, {
{name}}</h1>
</div>
<script>
new Vue({
el: 'h1', // 有两个元素存在, 相当于我同时选了两个元素
data: {
name: "Liu"
}
});
</script>
发现, 只有第一个元素被选中了, 一个Vue实例只能对应第一个选中的元素, 如果真的想实现可以尝试
<div id="root">
<h1 class = 'R1'>hello, {
{name}}</h1>
<h1 class = 'R2'>hello, {
{name}}</h1>
</div>
<script>
new Vue({
el: '.R1',
data: {
name: "Liu"
}
});
new Vue({
el: '.R2',
data: {
name: "Liu"
}
});
</script>
再去关注插值语法中 {
{ }} 可以写入些什么? 可以写入的是JS表达式, 例如
- MVVM的变量(会被转译为app.data.变量):
name,name.toUpperCase() - 算数表达式:
1+1 - JS的表达式:
Date.now()
我们还需要对元素的属性进行MVVM, 例如a标签的地址, 按照之前的思路我们应该写href={
{url}}, 这是不被接受的, 我们需要指令语法指定标签属性来被Vue绑定, 指定方法是将href={
{url}}改写为: v-bind:href="url"或者:href="url", 此处的href可以是任意属性, 之后Vue会将引号内的内容做替换, 例如
<body>
<div id="root">
<h1>插值语法: hello, {
{name.toUpperCase()}}</h1>
<h1>指令语法: </h1>
<a v-bind:href='url'>click</a>
</div>
</body>
<script>
new Vue({
el: '#root',
data: {
name: "Liu",
url: "https://liukairui.cc"
}
});
</script>
- 插值语法: 用于标签体, 使用
{ { }} - 指令语法: 用于标签属性/标签体/标签事件, 使用
v-XX: = ""
数据绑定
数据绑定有单向数据绑定与双向数据绑定, 区别是
- 单向数据绑定使用
v-bind:进行绑定, 只会在JS文件中对应值发生变化的时候修改DOM, 在DOM中值发生变化时并不会修改JS中变量 - 双向数据绑定使用
v-model:进行绑定, JS文件中对应值发生变化的时候修改DOM, 在DOM中值发生变化会反向修改JS中变量
<body>
<div id="root">
Name1: <input type="text" :value="name">
Name2: <input type="text" v-model:value="name">
</div>
</body>
<script>
new Vue({
el: '#root',
data: {
name: "Liu",
}
});
</script>
可以尝试修改Name1中的值, Name2不变, 修改Name2中的值, Name1发生变化
但是, 并不是所有的属性都可以使用v-model:绑定, v-model:只能绑定到表单类元素, 注意的是v-model:value可以简写成v-model(这是默认值)
el与data的多种写法
- 可以使用
.$mount()指定挂在对象, 从而取代el
使用这种方法的有点是绑定更加灵活, 例如const app = new Vue({ - el: '#root', data: { name: "Liu", } }); + app.$mount('#root')const app = new Vue({ - el: '#root', data: { name: "Liu", } }); - app.$mount('#root') + setTimeout(()=>{app.$mount('#root')},1000); - 可以使用函数代替对象作为data(但是要求函数返回一个对象)
p.s. 这个函数是Vue在执行的时候帮你调用的, 这里要求函数不能写箭头函数, 否则函数内部的this就变成global了, 而不是Vue(在后期后果有点严重)const app = new Vue({ el: '#root', - data: { - name: "Liu", - }, + data() { + return{ + name: 'Liu' + } + }, });
MVVM模型
MVVM的意思是: 模型(JS)-视图(DOM)-视图模型
- 模型中的数据通过视图模型的Bind绑定到视图上
- 模型视图使用listeners监听视图的变化, 修改模型中的数据
可以将他们对应起来
<body>
<div id="root"> <!-- V: 视图 -->
Name1: <input type="text" :value="name">
Name2: <input type="text" v-model:value="name">
</div>
</body>
<script>
const vm = new Vue({
// VM: 视图模型
el: '#root',
data: {
// M: 模型
name: "Liu",
}
});
</script>
可以在浏览器中Console中查看vm, 打印后可以看到是一个Vue对象, 里面有一个name属性正好是我们设置的name, 我们可以尝试调用一些非我们定义的属性, 例如:value="$el"我们发现页面确实显示了vm的$el变量, 由此可见vm确实是一个视图模型, 视图通过调用视图模型变量实现数据绑定
数据代理
数据代理调用了方法是Object.defineProperty(), 使用方法是: Object.defineProperty(对象,键,{值的配置}), 例如
let person = {
name: 'AAA',
isMale: true,
};
Object.defineProperty(person, 'age', {
value: 18,
// enumerable:true, //控制属性是否可以枚举,默认值是false
// writable:true, //控制属性是否可以被修改,默认值是false
// configurable:true //控制属性是否可以被删除,默认值是false
//当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get(){
console.log('有人读取age属性了')
return number
},
//当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value){
console.log('有人修改了age属性,且值是',value)
number = value
}
});
Object.defineProperty()与直接为对象加入元素的区别是, 该方法获得的对象默认不可枚举, 不可修改, 不可删除
数据代理就是通过一个对象, 代理对另一个对象属性的操作(读/写)
简单的数据代理, obj2代理obj1
let obj = {
x:100}
let obj2 = {
y:200}
Object.defineProperty(obj2,'x',{
get(){
return obj.x
},
set(value){
obj.x = value
}
})
在Vue中, 视图模型VM就实现了对模型M的代理, 在定义Vue实例的时候, data中的数据会被链接到app._data中(还要加入一些MVVM用的函数), 而data中的键key又会被app.key代理
- 视图V获取数据(app.key)的时候, VM中的get就会获取M中的数据(app._data.key)
- 视图V设置数据的时候, VM中的set就会修改M中的数据
事件处理
使用v-on:XX绑定事件, 例如v-on:click = 'showAlert', 这样, 在点击之后, Vue会寻找methods中的showAlert函数处理事件
<body>
<div id="root">
<h1>Hi, {
{name}} for here</h1>
<button v-on:click = 'showAlert'>ClickMe</button>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
name: 'Liu',
}
},
methods: {
showAlert(){
alert('你好')
}
},
});
</script>
这里v-on:click可以简写为@click
这里的回调函数的调用方式有
- 在调用方式为
@click = 'showAlert'时, 函数可以接受一个参数event - 在调用方式为
@click = 'showAlert(15)'时, 函数只接受一个变量15, event消失了 - 在调用方式为
@click = 'showAlert(15, $event)'时, 函数接受15与event两个参数, 此时相当于使用关键字手动调用了event
习惯这种'foo(1,2)'调用函数的方式
<body>
<div id="root">
<h1>Hi, {
{name}} for here</h1>
<button @click = 'showAlert'>ClickMe</button>
<button @click = 'showLog($event, 50)'>ClickMe2</button>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
name: 'Liu',
}
},methods: {
showAlert(){
alert('你好')
},
showLog(e, v){
console.log('你好', e, v)
}
},
});
</script>
点击ClickMe2后输出: 你好, 鼠标事件, 50
同时应该知道, 这些函数与data一样在vm上, 但是没有做数据代理(谁会修改回调函数呢?)
既然这么说了, 理论上我们可以把methods的东西放在data里面(反正最终都是绑定到vm上), 但是这会增大Vue的负担, 额外的将这些方法进行了数据代理
事件修饰符
部分标签存在默认行为, 朴素的v-on:不会进行阻止, 例如
<a href = 'baidu.com' @click = 'showLog'>ClickMe2</a>
就会先执行showLog, 然后跳转网页, 我们可以手动阻止, 也可以将v-on:click(@click)改为v-on:click.prevent(@click.prevent)阻止
这里的.prevent就是事件修饰符, Vue中的修饰符有
prevent:阻止默认事件 (常用) ;stop:阻止事件冒泡 (常用) ;once:事件只触发一次 (常用) ;capture:使用事件的捕获模式;self:只有event.target是当前操作的元素时才触发事件;passive:事件的默认行为立即执行,无需等待事件回调执行完毕;
修饰符支持连写@click.stop.once = 'foo'
键盘事件
按键事件的别名
我们希望实现一个input中按下回车在Console输出input内容, 可以这么写
<body>
<div id="root">
<input type="text" placeholder="按下回车" @keyup = 'showInfo'>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
name: 'Liu',
}
},methods: {
showInfo(e){
if(e.keyCode ==13)
console.log(e.target.value);
}
},
});
</script>
我们需要手动判断按下的是不是回车, 实际上Vue提供了很多键盘事件别名实现当特定情况发生再触发事件, 可以将上述代码修改为
<body>
<div id="root">
- <input type="text" placeholder="按下回车" @keyup = 'showInfo'>
+ <input type="text" placeholder="按下回车" @keyup.enter = 'showInfo'>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
name: 'Liu',
}
},methods: {
showInfo(e){
- if(e.keyCode ==13)
console.log(e.target.value);
}
},
});
</script>
@keyup.enter表示只有enter键才能触发, 类似的别名还有
- 回车 => enter
- 删除 => delete (捕获“删除”和“退格”键)
- 退出 => esc
- 空格 => space
- 换行 => tab (特殊,必须配合keydown去使用)
- 上 => up
- 下 => down
- 左 => left
- 右 => right
那么, 他们为什么叫做事件别名呢? 这是因为Vue将JS原生代码中的Enter等键名起了一个别名小写化为了enter, 可以使用@keyup.Enter等验证原名也是可用的
据此, 我们可以借助Vue设置一些没有别名的键的事件@ketup.key, 例如@keyup.s = showInfo实现按下s键时调用showInfo(e)(@keyup.ASCII代码 = showInfo也可以触发, 但是强烈不推荐)
注意: 这里的key应该与JS中的Key大小写相同(例如Alt不能写成alt), 如果想要查看某一个键的key可以让js打印e.key, 同时如果某一个键的Key是多个大写单词拼成的(例如CapsLock), 应该全部转换为小写并在单词连接处加-(例如CapsLock -> caps-lock, 这种命名法叫做kebab-case)
在自定义事件修饰的时候, 系统级修饰键(ctrl、alt、shift、meta)存在特殊规则
- 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发. (同时不会阻止默认行为)
- 配合keydown使用:正常触发事件.
我们也可以自定义别名
Vue.config.keyCodes.myhui = 13; // 回车的ASCII(别名也是kebab-case)
// @keyup.myhui
修饰符支持连写@keyup.ctrl.y = 'foo'(不能与浏览器默认快捷键冲突, 无法阻止默认行为)
计算属性
尝试实现一个组件, 在输入框输入姓名, 显示So, Hi 姓-名, 可以使用最原始的插值语法
<body>
<div id="root">
姓:<input type="text" v-model:value="v_x">
名:<input type="text" v-model:value="v_m">
<p>so, Hi {
{v_x}} {
{v_m}}</p>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
v_m: 'Kr',
v_x: 'L',
}
},
});
</script>
更先进的
<body>
<div id="root">
姓:<input type="text" v-model:value = 'v_x'>
名:<input type="text" v-model:vaule = 'v_m'>
<p>so, Hi {
{getFullName()}}</p>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
v_m: 'Kr',
v_x: 'L',
}
},methods: {
getFullName(){
return this.v_x+'-'+this.v_m.toUpperCase();
}
},
});
</script>
使用指令语法
<body>
<div id="root">
姓:<input type="text" @keyup.enter = 'chgLst'>
名:<input type="text" @keyup.enter = 'chgFst'>
<p>so, Hi {
{v_x}} {
{v_m}}</p>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
v_m: 'Kr',
v_x: 'L',
}
},methods: {
chgLst(e){
this.v_x = e.target.value;
}, chgFst(e){
this.v_m = e.target.value;
}
},
});
</script>
还可以使用计算属性实现, 所谓计算属性就是通过已有的属性计算出我们需要的属性, 在Vue中属性存放在data中, 计算属性被单独的写在computed里面, 如何让Vue在获取数据的同时刷新数据值, 我们想到了Object.defineProperty()中的get属性, 他可以在读取数据的时候执行特定函数, 并得到结果, 实际上Vue就是这么实现的, 并且将结果绑定到vm上.
<body>
<div id="root">
姓:<input type="text" v-model:value = 'v_x'>
名:<input type="text" v-model:vaule = 'v_m'>
<p>so, Hi {
{
fullName}}</p>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
v_m: 'Kr',
v_x: 'L',
}
},computed: {
fullName: {
get(){
return this.v_x+'-'+this.v_m.toUpperCase();
}
}
}
});
</script>
在浏览器中, 我们可以看到app.fullName: (...), 浏览器不会显示数值, 因为我们没有调用get(), 点击(...)之后, 浏览器才会通过get()获取值, 这也证明了computed中值被绑定到了vm中
计算属性采用了缓存机制与插值语法相比性能更优, 当数据没有变化的时候不会反复get()
计算属性还支持Set, 例如我需要有一个按钮, 按下之后就将名字修改成布-吉岛, 可以设置如下
<script>
const app = new Vue({
el: '#root',
data() {
return{
v_m: 'Kr',
v_x: 'L',
}
},computed: {
fullName: {
get(){
return this.v_x+'-'+this.v_m.toUpperCase();
},
set(v){
[this.v_x, this.v_m] = v.split('-')
}
}
},methods: {
initFull(){
this.fullName = '布-吉岛'
}
},
});
</script>
当确定你的计算属性只get不set的时候, 可以做简写
computed: {
fullName(){
return this.v_x+'-'+this.v_m.toUpperCase();
},
}
最终变成了methods的样子`~`
监视属性
当某一个属性发生变化的时候, 执行相关动作, 与计算属性不同的是:
- 计算属性是构造一个虚拟的变量, 用于Vue的调用, 如果检测到构造这个变量的变量被修改了, 就在下次get的时候修改
- 监视属性是监视属性, 变更的时候立刻执行回调函数
我们可以尝试写一个组件, 点击按钮反转天气文字, 使用计算属性可以这样写
<body>
<div id="root">
<h1>今天天气很{
{weather}}</h1>
<button @click = 'togWeather'>togWeather</button>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
v: false
}
},computed: {
weather(){
return this.v?'炎热':'凉爽'
},
}, methods: {
togWeather(){
this.v=!this.v;
}
},
});
</script>
使用监视属性
<body>
<div id="root">
<h1>今天天气很{
{weather}}</h1>
<button @click = 'togWeather'>togWeather</button>
<p>{
{numbers}}{
{numbers.a}}</p>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
v: false,
weather: '布吉岛',
numbers: {
a:1,
b:2,
c:3
}
}
}, methods: {
togWeather(){
this.v=!this.v;
}
}, watch:{
v: {
handler(newValue, oldValue){
this.weather = newValue?'凉爽':'炎热'
},
// immediate: true, // 初始化的时候立即调用函数
},
'numbers.a':{
// 监视对象属性的变化
handler(n,o){
console.log(n,o);
}
},
numbers:{
deep: true, // 开启深度监视, 如果不开启, 只会监视这个对象的地址有没有发生改变
handler(n,o){
console.log(n,o);
}
}
}
});
</script>
监视属性写在单独的watch中
其中handler()就是数据变化后的回调函数, 传入新值和老值
如果想要监控在初始化是时候执行一次就加上immediate, 如果想要检测对象内部值的变化要使用deep, 否则只监视对象地址变化
如果监视属性只有handler可以简写成
watch:{
v(newValue, oldValue){
this.weather = newValue?'凉爽':'炎热'
},
}
可以在vm声明结束后声明变量监控
vm.$watch('监视变量',{
}/function(newV, oldV){
})
计算属性与属性监控的区别
- computed和watch之间的区别:
1.computed能完成的功能,watch都可以完成.
2.watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作.
闲来无事写了一个计算属性的异步调用
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="root">
<h1>今天气温{
{weather.is_hot}}度</h1>
<h1>顺便看看这个计时器ID: {
{weather.counter}}</h1>
<button @click = 'togWeather'>点击让5s后温度下降一度</button>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
// v.val代表温度, 这样每次对象地址变换但是val不变Vue依旧会刷新
// Vue太精明了, this.v = this.v + 1 - 1 + Math.random()*0 不会触发计算属性刷新
v: new Object({
val: 50}),
// 降低一度?
chgMe: false
}
}, methods: {
togWeather(){
this.chgMe=!this.chgMe;
// 修改v触发计算属性
this.v = new Object({
val: this.v.val});
}
}, computed: {
weather(){
// 通过返回new Object刷新地址 防止缓存
return new Object({
is_hot: this.v.val,
counter: setTimeout(() => {
console.log("IN-timeOut");
if(this.chgMe){
this.chgMe = false;
// !用这个方法返回新的v, 但是会触发新的定时器...
// this.v = new Object({val: this.v.val - 1});
// !返回新的v.val, 地址不变, 但是为什么还是触发新的定时器?
this.v.val -= 1;
console.log("IN-chg")
}
}, 5000)
})
}
}
});
</script>
</html>
Class与Style的绑定
- 字符串绑定class(样式名字不确定个数确定)
可以动态指定class, 由于class有多个, 不变的可以正常写, 变化的使用数据绑定
这样我们就只需要维护一个变量<div class="basic" :class = 'classB' @click = 'changeMod'>{ {name}}</div>classB就实现了Class的调整, 用于动态决定class值 - 数组绑定class(样式名字不确定个数不确定)
也可以绑定成数组<div class="basic" :class = 'classArray' @click = 'changeMod'>{ {name}}</div> - 对象绑定class(样式名字确定个数确定动态决定要不要)
对象的TF对应加不加class<div class="basic" :class = 'classObj' @click = 'changeMod'>{ {name}}</div> <!-- ... --> classObj: { atguigu1: false, atguigu2: true, }
对于内联样式
<!-- 绑定style样式--对象写法 -->
<div class="basic" :style="styleObj">{
{name}}</div> <br/><br/>
<!-- 绑定style样式--数组写法 -->
<div class="basic" :style="styleArr">{
{name}}</div>
<!-- ... -->
styleObj2:{
backgroundColor:'orange'
},
styleArr:[
{
fontSize: '40px', // 注意这里要转驼峰命名, 注意单位
color:'blue',
},
{
backgroundColor:'gray'
}
]
条件渲染
符合某些条件再渲染某些元素
v-show渲染
使用v-show属性实现, 值是一个布尔表达式, 例如
不显示之后可以看到h2的<body> <div id="root"> <h2 v-show = "show">{ {welcomText}}</h2> <button @click='togShow'>切换</button> </div> </body> <script> const app = new Vue({ el: '#root', data() { return{ welcomText: '欢迎来到', show: true } }, methods: { togShow(){ this.show = !this.show; } }, computed: { } }); </script>display = nonev-if渲染
也可以实现v-show功能, 但是比较狠, 一旦为false直接删除元素
所以, 如果DOM变化频率高建议用show, 频率低用if, 否则频繁的插入删除节点不易于维护<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> </head> <body> <div id="root"> <h2 v-if = "show">{ {welcomText}}</h2> <button @click='togShow'>切换</button> </div> </body> <script> const app = new Vue({ el: '#root', data() { return{ welcomText: '欢迎来到', show: true } }, methods: { togShow(){ this.show = !this.show; } }, computed: { } }); </script> </html>
v-if还支持v-else-if,v-else, 在连续的if/else-if/else之间不能出现多余的元素
当有一些紧挨的元素想要批量显示, 可以写<body> <div id="root"> <h2 v-if = "show === 1">1</h2> <h2 v-else-if = "show === 2">2</h2> <h2 v-else-if = "show === 3">3</h2> <h2 v-else = "show === 4">4</h2> <button @click='togShow'>切换</button> </div> </body> <script> const app = new Vue({ el: '#root', data() { return{ welcomText: '欢迎来到', show: 1 } }, methods: { togShow(){ this.show = (this.show)%4 + 1; } }, computed: { } }); </script>
比较麻烦, 可以改成<h2 v-if = "show === 1">A</h2> <h2 v-if = "show === 1">A</h2> <h2 v-if = "show === 1">A</h2> <h2 v-if = "show === 2">B</h2> <h2 v-if = "show === 2">B</h2> <h2 v-if = "show === 2">B</h2>
但是修改了DOM结构, 会跟着变很多CSS/JS, 解决方法是直接吧if绑定在template标签上, 这种标签在最终渲染的时候会被隐藏, 例如<div v-if = "show === 1"> <h2>A</h2> <h2>A</h2> <h2>A</h2> </div> <div v-if = "show === 2"> <h2>B</h2> <h2>B</h2> <h2>B</h2> </div>
最终会渲染为<template v-if = "show === 1"> <h2>A</h2> <h2>A</h2> <h2>A</h2> </template> <template v-if = "show === 2"> <h2>B</h2> <h2>B</h2> <h2>B</h2> </template><h2>A</h2> <h2>A</h2> <h2>A</h2>
列表渲染
类似for循环生成多个标签, 使用了与JS的for类似写法
<body>
<div id="root">
<div v-for="(item, index) in items" :key="item.id">{
{item.id}}-{
{item.val}}</div>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
items: [
{
id: 1, val: "Liu"},
{
id: 2, val: "Kai"},
{
id: 3, val: "Rui"}
]
}
}
});
</script>
其中被遍历的对象可以是任意可迭代对象例如
- 数组:
(值,下标) in arr - 对象:
(值,键名) in obj - 字符串:
(字符,下标) in str - 类似py的range写法:
(数字,index) in 数字(v,index) in 5 (1,0) (2,1) (3,2) (4,3) (5,4)
内部的元素要指明是对象.属性
注意这个Key
这里的Key相当于是当前这条迭代对象的唯一标识符, 尽量使用后端提供的ID, 如果不写:key默认会指定为:key=index 这是一种非常危险的做法
首先要明白Vue为什么要在循环中加入这个Key?, 如果仅仅是为了表示循环, key完全可以默认表示index, 而不需要用户指定. 这个Key是用于Vue的diff算法中的, 当数据发生变化后, diff算法会对比虚拟DOM发生了什么变化, 这个时候识别for中哪条元素变化就是通过Key识别的
当我们使用index做Key时, 上面例子渲染的是
<div :key=0>{
{1}}-{
{"Liu"}}</div>
<div :key=1>{
{2}}-{
{"Kai"}}</div>
<div :key=2>{
{3}}-{
{"Rui"}}</div>
<!--
使用的数据是
{id: 1, val: "Liu"},
{id: 2, val: "Kai"},
{id: 3, val: "Rui"}
-->
当我们在后面加入一条新数据后,
<div :key=0>{
{1}}-{
{"Liu"}}</div>
<div :key=1>{
{2}}-{
{"Kai"}}</div>
<div :key=2>{
{3}}-{
{"Rui"}}</div>
<div :key=3>{
{4}}-{
{"CC"}}</div>
<!--
使用的数据是
{id: 1, val: "Liu"},
{id: 2, val: "Kai"},
{id: 3, val: "Rui"},
{id: 4, val: "CC"}
-->
Vue的Diff算法会对比前后虚拟DOM的不同
<div :key=0>{
{1}}-{
{"Liu"}}</div>
<div :key=1>{
{2}}-{
{"Kai"}}</div>
<div :key=2>{
{3}}-{
{"Rui"}}</div>
+ <div :key=4>{
{4}}-{
{"CC"}}</div>
Key=1,2,3没有变化, 于是在浏览器DOM中加入一条数据
看起来使用index也没有什么问题, 但是如果我们的数据是
{id: 4, val: "CC"},
{id: 1, val: "Liu"},
{id: 2, val: "Kai"},
{id: 3, val: "Rui"}
呢?
使用id渲染结果是
<div :key=4>{
{4}}-{
{"CC"}}</div>
<div :key=1>{
{1}}-{
{"Liu"}}</div>
<div :key=2>{
{2}}-{
{"Kai"}}</div>
<div :key=3>{
{3}}-{
{"Rui"}}</div>
Vue对比之前代码发现,
+ <div :key=4>{
{4}}-{
{"CC"}}</div>
<div :key=1>{
{1}}-{
{"Liu"}}</div>
<div :key=2>{
{2}}-{
{"Kai"}}</div>
<div :key=3>{
{3}}-{
{"Rui"}}</div>
于是在浏览器DOM中增加一个标签
使用index渲染结果是
<div :key=0>{
{4}}-{
{"CC"}}</div>
<div :key=1>{
{1}}-{
{"Liu"}}</div>
<div :key=2>{
{2}}-{
{"Kai"}}</div>
<div :key=3>{
{3}}-{
{"Rui"}}</div>
Vue对比之前代码发现,
- <div :key=0>{
{1}}-{
{"Liu"}}</div>
+ <div :key=0>{
{4}}-{
{"CC"}}</div>
- <div :key=1>{
{2}}-{
{"Kai"}}</div>
+ <div :key=1>{
{1}}-{
{"Liu"}}</div>
- <div :key=2>{
{3}}-{
{"Rui"}}</div>
+ <div :key=2>{
{2}}-{
{"Kai"}}</div>
+ <div :key=3>{
{3}}-{
{"Rui"}}</div>
Vue可不认识你是在前面加入了元素, 他只认识Key, 这样Vue就需要重新在浏览器DOM中渲染所有的标签, 造成巨大的资源损失
同时, 对于表单等输入类数据, 存在十分严重的问题, 如果在input中输入了数据, 在乱序修改数组后输入数据会保留在相对位置不变, 例如
<div :key=1>{
{1}}-{
{"Liu"}}<input/></div>
<div :key=2>{
{2}}-{
{"Kai"}}<input/> <!--我在这里输入了AAA--> </div>
<div :key=3>{
{3}}-{
{"Rui"}}<input/></div>
用:key=index后
在前面加入数据
- <div :key=0>{
{1}}-{
{"Liu"}}<input></div>
+ <div :key=0>{
{4}}-{
{"CC"}}<input></div>
- <div :key=1>{
{2}}-{
{"Kai"}}<input></div>
+ <div :key=1>{
{1}}-{
{"Liu"}}<input><!--Vue认准了输入AAA的input在Key=1里面--></div>
- <div :key=2>{
{3}}-{
{"Rui"}}<input></div>
+ <div :key=2>{
{2}}-{
{"Kai"}}<input></div>
+ <div :key=3>{
{3}}-{
{"Rui"}}<input></div>
直接造成了输入框内容移位
- 用index作为key可能会引发的问题:
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低. - 如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题.
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
- 开发中如何选择key?:
- 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值.
- 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的.
通过列表渲染实现搜索功能, 搜索排序
<body>
<div id="root">
<input type="text" v-model:value = 'searchTxt'>
<button @click='searchBy = 0'>Norm</button>
<button @click='searchBy = 1'>A->Z</button>
<button @click='searchBy = -1'>Z->A</button>
<div v-for="(item, index) in searchRes" :key="item.id">{
{index+1}}-{
{item.val}}</div>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
items: [
{
id: 1, val: "123"},
{
id: 2, val: "234"},
{
id: 3, val: "211"},
],
searchBy: 0,
searchTxt: ''
}
},
computed:{
searchRes(){
const res = this.items.filter((d)=>{
return d.val.includes(this.searchTxt)
});
res.sort((n,o)=>{
return this.searchBy*(n.val-o.val);
})
return res;
}
}
});
</script>
Vue数据代理与检测的原理
Vue的数据代理之前说是使用Object.defineProperty实现的, 但是他具体是怎么实现的呢, 想当然的, 我们这样实现
const data = {
demo: 10,
};
Object.defineProperty(data, 'demo', {
get() {
console.log("YOU GET");
return this.demo;
},
set(v) {
console.log("YOU SET and SYNC to DOM");
this.demo = v;
},
});
console.log(data.demo);
data.demo = 11;
但是JS会报错
RangeError: Maximum call stack size exceeded
at Object.get [as demo] (/home/liukairui/CODE/code-snippet/Vue/VueDemo/demo.js:7:17)
at Object.get [as demo] (/home/liukairui/CODE/code-snippet/Vue/VueDemo/demo.js:7:17)
at Object.get [as demo] (/home/liukairui/CODE/code-snippet/Vue/VueDemo/demo.js:7:17)
显示与之前学的不同, 以前我们使用Object.defineProperty是维护一个变量, 然后get的时候通过其他变量获得值得, 但是这次是获取他自己, 这样JS又会调用get返回自己, 造成无限递归, 爆栈
那Vue是如何避免无限递归的呢, 使用一个中间层, 在get的时候获取原始数据, 在Set的时候修改原始数据并更新DOM, 我们实现一个简单的例子
const data = {
demo1: 10,
demo2: 10,
demo3: 10,
demo4: 10,
};
function Observer(obj) {
let keys = Object.keys(obj); // 获取被监视对象的全部Key
keys.forEach((d)=>{
Object.defineProperty(this, d, {
get() {
return obj[d];
},
set(v) {
obj[d] = v;
console.log("我开始刷新DOM了")
},
});
});
}
let _data = new Observer(data);
console.log(_data.demo1);
_data.demo1 = 15;
console.log(_data.demo1);
console.dir(_data);
console.dir(data);
结果是
10
我开始刷新DOM了
15
Observer {}
{ demo1: 15, demo2: 10, demo3: 10, demo4: 10 }
可以看到开发者工具中vm有_data, 这里的_data相当于我们的Observer, 可以看到_data中有每一个属性的get, 但是他不是Object.defineProperty是Vue自定义的
get items: ƒ reactiveGetter()
set items: ƒ reactiveSetter(newVal)
响应式的get和set, 还记得代码中data的属性p会同时绑定到vm.p与vm._data.p, 这两个p是完全等价的, 当我们修改任何一个属性的时候会调用这个响应式的get/set
同时Vue的响应式get/set可以对对象进行"穿透", 不论对象有多少层, 或者数组有多少个元素, Vue都可以递归将对象内部的对象设置get/set, 直到内部是一个非对象
例如, 我们使用Vue定义
const app = new Vue({
el: '#root',
data() {
return{
items: [
{
id: 1,
bas: {
name: "A",
age: 12,
}
},
],
}
}
});
看到app._data
items: Array(1)
0:
bas: Object
age: 12
name: "A"
__ob__: Observer {
value: {
…}, dep: Dep, vmCount: 0}
get age: ƒ reactiveGetter() // item.has.age监视
set age: ƒ reactiveSetter(newVal)
get name: ƒ reactiveGetter() // item.has.name监视
set name: ƒ reactiveSetter(newVal)
[[Prototype]]: Object
id: 1
__ob__: Observer {
value: {
…}, dep: Dep, vmCount: 0}
get bas: ƒ reactiveGetter() // item.has监视
set bas: ƒ reactiveSetter(newVal)
get id: ƒ reactiveGetter() // item.id监视
set id: ƒ reactiveSetter(newVal)
[[Prototype]]: Object
length: 1
__ob__: Observer {
value: Array(1), dep: Dep, vmCount: 0}
[[Prototype]]: Array
__ob__: Observer {
value: {
…}, dep: Dep, vmCount: 1}
get items: ƒ reactiveGetter() // 对于item的get
set items: ƒ reactiveSet(newVal)
在实际开发中会遇到这样的一个问题
<body>
<div id="root">
<div v-for="item in items">{
{item.name}}</div>
<button @click='chg'>修改值</button>
</div>
</body>
<script>
const app = new Vue({
el: '#root',
data() {
return{
items: [
{
name: 1},
{
name: 2},
{
name: 3},
{
name: 4},
],
}
}, methods: {
chg(){
// this.items[0].name = 'S'; // Ok
this.items[0] = {
name: 'S'}; // Err
}
},
});
</script>
执行被注释的代码, 页面修改成功, 但是执行没有注释的代码, 那么页面就修改失败了, 这是因为对这个对象整体赋值的时候没有为他内部的元素加入reactiveGetter()
那么, 如果我们希望实现将
data() {
return{
info: {
name: 4},
}
}
变为
data() {
return{
info: {
name: 4, birth: '2000'},
}
}
应该怎么处理呢, 首先应该使用类似Object.defineProperty的方法加入get与set, 使属性可以被监视代理, 可以使用Vue.set(对象, '属性', 值)或者vm.$set(对象, '属性', 值), 例如vm.set(vm.info, 'bitrh', '2000')
对于对象很好说, 对于数组, 我们将
data() {
return{
info: [{
name: 4}]
}
}
变为
data() {
return{
info: [{
name: 4}, {
birth: '2000'}],
}
}
我们知道, 如果直接对JS的Array进行push那么birth所在对象应该是不会被监控, 但是在列表渲染小节中中我们发现这是可行的, 这是因为Vue对Array的push, pop…进行了重新封装, 不需要什么手动Set, 但是如果我们还是想要对数组中的元素进行直接替换, 我们需要使用vm.$set(vm.arr,下标,newVal)
注意, $set的数据不能是Vue对象, 也不能在data上直接set数据, 建议set到data.obj上
数据劫持: 将data中的属性转化为geter/geter形式, 每次修改获取数据都被上述函数劫持, 代为修改读取
表单数据
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="root">
<form @submit.prevent="ajaxSubmit">
<!-- 用trim去除两边空格 -->
帐号<input v-model.trim='userInfo.account' type="text"><br><br>
密码<input v-model='userInfo.passwd' type="password"><br><br>
<!-- 这里加了两个number, 第一个是让Vue知道输入的是数字, 第二个是浏览器限制输入内容必须是数字 -->
年龄<input v-model.number='userInfo.age' type="number"><br><br>
性别<br><br>
<!-- 要亲自配置value -->
男<input type="radio" name="sex" v-model='userInfo.sex' value="男"><br><br>
女<input type="radio" name="sex" v-model='userInfo.sex' value="女"><br><br>
爱好<br><br>
<!-- 要亲自配置value -->
男<input type="checkbox" v-model="userInfo.hobby" value="男">
女<input type="checkbox" v-model="userInfo.hobby" value="女">
都<input type="checkbox" v-model="userInfo.hobby" value="都">
位置<br><br>
<select v-model="userInfo.pos">
<option value="N">N</option>
<option value="S">S</option>
<option value="W">W</option>
<option value="E">E</option>
</select><br><br>
多行<br><br>
<!-- lazy标记使得内容只在失去焦点的时候才MVVM, 提高了性能 -->
<textarea v-model.lazy='userInfo.multext'></textarea>
同意<input type="checkbox" v-model="userIn
最低0.47元/天 解锁文章
2559

被折叠的 条评论
为什么被折叠?



