简介
Vue是一个动态构建用户界面的渐进式 Javascript 框架
特点
- 遵循mvvm模式
- 编码简洁,体积小,运行效率高,适合移动/pc 端开发
- 它本身只关注UI,也可以引入其他第三方库开发项目
浅尝一下
引入Vue.js
<script src="https://cn.vuejs.org/js/vue.js"></script>
HTML容器:
<!-- 准备一个容器 -->
<div id="root">
<!-- 插值 -->
<p>Hello {{name}}</p>
<p>事{{name2}}捏</p>
</div>
Vue脚本:
//创建Vue实例
const x = new Vue({
//配置对象
el: '#root', //el 用于指定当前Vue实例为哪个容器服务,css选择器格式
data: { //data 中用于指定存储的数据,数据供el所指定的容器去使用
name: 'kanokano',
name2: '嘉然'
}
});
Vue的工作条件
- 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
- root容器里的代码依然符合html的规范标准,只是混入了vue的一些语法
- root容器里的代码被称为Vue模板
- 一个容器对应一个Vue实例,反之亦然,Vue实例配合组件一起使用
- {{}}中需要写的是js表达式,且变量可以自动读取到data中所有的属性
- 一旦data中的数据发生改变,模板(页面)中的目标也会自动更新
模板语法
插值语法
插值语法直接在html标签体内使用Vue插值模板,填入data中的属性即可
<div id="root">
<!-- 插值 -->
<p>Hello {{name}}</p>
</div>
//创建Vue实例
const x = new Vue({
//配置对象
el: '#root',
data: {
name: 'kanokano',
}
});
指令语法
指令语法以 v- 开头,用于解析标签(包括标签属性,标签体内容,绑定事件等)
举例 (v-bind)
使用v-bind:来模板化href属性
<div id="root">
<a v-bind:href="url">kanokano的博客</a>
</div>
const x = new Vue({
//配置对象
el: '#root',
data: {
url: 'https://kanokano.cn'
}
});
有时,v-bind: 也可以简写为 :
<div id="root">
<a :href="url">kanokano的博客</a>
</div>
总结:标签体内用插值语法,标签属性用指令语法
数据绑定
由于v-bind是单向的数据绑定,只能通过对象传递到DOM
有了单向数据绑定这个概念,那就一定会有双向数据绑定
双向数据绑定使用:v-model:
<span>单向数据绑定:</span><input type="text" :value="name2"><br>
<span>双向数据绑定:</span><input type="text" v-model:value="name2">
注意,以下代码是错误的
<h1 v-model:data="name"></h1>
这是因为 v-model 只支持表单类等输入型元素内
v-model:value 可以简写为 v-model 因为 v-model 默认收集的就是value的值
<span>双向数据绑定:</span><input type="text" v-model="name2">
el与data的两种写法
el的两种写法:
对象属性
const v = new Vue({
el: '#root',
data: {
text: "你好呀"
}
});
使用实例对象的原型对象上的$mount方法挂载元素
const v = new Vue({
data: {
text: "你好呀"
}
});
v.$mount('#root')
data的两种写法
对象式:
data: {
text: "你好呀"
}
函数式:
data: function(){
//此处的this式Vue的实例对象
console.log(this)
return {
name:'你好呀'
}
}
注意,函数写法不可以是箭头函数,因为会扰乱this的指向(原本this指向一个Vue对象 )
//函数式可适用于vue组件
MVVM模型
- M:模型(Model): 对应 data中的数据
- V:视图(View): 模板
- VM:视图模型(ViewModel): vue实例对象
<body>
<!-- View -->
<div id="root">
<p>{{kano}}</p>
</div>
<script>
new Vue({ //ViewModel
el: '#root',
data: { //Model
kano: "kano"
}
});
</script>
</body>
数据代理
Object.defineproperty 方法
此方法可以设置一个对象的属性,该方法有三个参数,区别于直接赋值,该方法的作用更多,更高级
简单用法:
let singer = {
name: 'kano'
}
Object.defineProperty(singer, 'age', {
value: 8
})
console.log(singer);
这样就能给一个对象添加属性了,但是你会发现,这个属性居然不能修改,也不能删除,更不能被遍历出来!
其实是因为 Object.defineProperty 方法默认创建出来的属性,他的 writable, enumerable, configurable的值都为false.
console.log(Object.getOwnPropertyDescriptors(singer));
//该方法返回指定对象所有 自身属性 的描述对象
想要解决无法修改,配置,遍历属性的问题,其实很简单,只需要在第三个参数中指定属性的特征即可:
Object.defineProperty(singer, 'age', {
value: 8,
enumerable: true, //控制属性是否可被枚举
writable: true, //控制属性是否可被修改
configurable: true //控制属性是否可被删除
});
singer.age=11;
console.log(singer);
以上代码配置了可修改,可配置,可被枚举的属性特征,所以是可以遍历且值可以被修改删除
getter与setter
如标题所言,JavaScript已经开始逐渐像java/c#类这样的面向对象语言靠拢了(喜)
直接看使用方法:
//数据代理
let msg = 'hello~';
Object.defineProperty(singer, 'msg', {
get: () => {
console.log('调用了getter');
return msg
},
set: value => {
console.log('调用了setter');
msg = value
}
});
console.log(singer);
当读取msg属性的时候,get访问器就会自动调用函数获取相应的值,返回的值就是msg属性的值
修改(set访问器)也是一样,有java/C#类语言的基础会更好理解
这样,通过get访问器就可以实现代理msg变量了,实现了数据的双向/单向同步
总结:Object.defineProperty 是一个比较高级的给对象添加属性的方法,不仅增加了对象属性的安全性,也可以让我们更灵活使用对象中的属性,更重要的是 这个方法可以设置访问代理,Vue的数据双向绑定就是依照这个方法构建的
数据代理实例
我们可以通过一个简单的例子来实现数据代理:
let obj = {
a: 10
};
let objj = {
b: 20
};
`Object.defineProperty(objj, 'a', {
get: () => obj.a,
set: (value) => {
obj.a = value
}
})
console.log(objj.a);
console.log(obj.a);
obj.a = 111;
console.log(objj.a);
console.log(obj.a);`
Vue中的数据代理
- Vue中的数据代理:
- V通过vm对象来代理data对象中属性的操作读和写
- Vue中数据代理的好处:
- 更加方便操作data中的数据
- 基本原理:
- 通过Object。defineProperty() 把data对象中所有的属性添加到vm上,为每一个添加到vm上的属性,都指定一个getter/setter 在里面进行读写data中对应的属性
原理图:
示例代码:
let data = {
name: 'kano',
age: 10
}
var vm = new Vue({
el: '#root',
data
});
console.log(data === vm._data); //true
vm.name = "kanokano"
console.log(data.name, vm._data.name); //一致
事件处理
事件的基本使用
- 使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名
- 事件的回调需要配置在methods对象中,最终会在vm上
- methods中配置的函数不能使用箭头函数,会造成this指向错误
- methods中配置的函数,都是被Vue所管理的函数,this的指向是 vm 或 组件的实例对象
- @click 中的字符串可以使用函数写法进行传参,
@click="fun"
和@click="fun($event)"
是一样的 - 函数传参的时候如有多个参数,会造成event无法使用,可以在实参列表中,添加$event 进行占位,就可以使用了
代码演示
<div id="root">
<h1>{{hello}}</h1>
<button v-on:click="showInfo">点我</button>
<!-- 简写 -->
<!-- 函数没有参数时候可以省略括号 -->
<!-- 想用event可以使用$event关键词占位 -->
<button @click="showInfo1(123,$event)">点我点我</button>
</div>
const vm = new Vue({
el: '#root',
data: {
hello: '你好!'
},
methods: { //methods内的方法不做数据代理
showInfo(e) {
alert('okk')
console.log(this); //这里的this是vm
console.log(e.target.innerText);
},
showInfo1(num, e) {
alert(num)
console.log(this); //这里的this是vm
console.log(e.target.innerText);
}
}
});
事件修饰符
Vue中的事件修饰符:
- prevent:阻止默认事件(常用)
- stop:阻止事件冒泡(常用)
- once:事件只触发一次(常用)
- capture:使用事件捕获模式
- self:只有event.target 是当前操作的元素才会触发事件
- passive:事件的默认行为是立即执行,无需等待事件回调即可执行完毕
注意,事件修饰符是可以连用的 比如 @click.stop.prevent 先阻止冒泡。再阻止默认行为
<div id="root">
<h1>{{hello}}</h1>
<!-- .prevent就可以阻止默认行为 -->
<!-- .prevent就是事件修饰符 -->
<a href="//baidu.com" @click.prevent="showInfo">点我</a>
&lt;!-- 阻止事件冒泡(常用) --&gt;
&lt;div @click="showInfo" style="background-color:skyblue"&gt;
&lt;button @click.stop="showInfo"&gt;点点点&lt;/button&gt;
&lt;/div&gt;
&lt;!-- 一次性事件 --&gt;
&lt;button @click.once="showInfo"&gt;我是一次性的&lt;/button&gt;
&lt;!-- 事件捕获模式 --&gt;
&lt;!-- 点击紫色盒子,先输出1再输出2 --&gt;
&lt;div @click.capture="show(1)" style="background-color:skyblue;padding: 10px;"&gt;1
&lt;div @click.capture="show(2)" style="background-color:blueviolet;"&gt;2&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 只有event.target 是当前操作的元素才会触发事件,点击紫色盒子的时候会触发事件,此时e.target是点击的那个button,然后会触发事件冒泡 --&gt;
&lt;!-- 事件冒泡到上层绑定事件的元素时,由于加了.self修饰,发现了触发事件的event不是自己,就不会触发事件操作 --&gt;
&lt;div @click.self="show(1)" style="background-color:skyblue;padding: 10px;"&gt;1
&lt;button @click="show(2)" style="background-color:blueviolet;"&gt;2&lt;/button&gt;
&lt;/div&gt;
&lt;!-- 事件的默认行为是立即执行,无需等待事件回调即可执行完毕 --&gt;
&lt;!-- wheel是鼠标滚轮事件 --&gt;
&lt;ul @wheel.passive="scrollbig"&gt;
&lt;li&gt;1111&lt;/li&gt;
&lt;li&gt;2222&lt;/li&gt;
&lt;li&gt;3333&lt;/li&gt;
&lt;li&gt;4444&lt;/li&gt;
&lt;/ul&gt;
</div>
const vm = new Vue({
el: '#root',
data: {
hello: '你好!'
},
methods: { //methods内的方法不做数据代理
showInfo(e) {
alert('okk')
console.log(this); //这里的this是vm
console.log(e.target.innerText);
},
show(s) {
console.log(s);
},
scrollbig() {
for (var i = 0; i < 999; i++) {
console.log('dddd');
}
}
}
});
键盘事件
-
常用按键别名:
- 回车 enter删除 delete (删除和退格都行
- 退出 esc空格 space换行 tab (特殊,必须配合keydown)
- 上 up
- 下 down
- 左 left
- 右 right
-
Vue未提供别名的按键,可以使用按键原始值的key值绑定,但要注意要转为 keybab-case 短横线小写命名
-
系统修饰键(特殊) ctrl alt shift 等
- 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放,事件才能触发
- 配合keydown使用,正常触发事件
-
可以使用keyCode指定具体的按键(不建议)
-
Vue.config.keyCodes.自定义按键名=键码,可以自定义按键别名
-
可以利用修饰符连写的特性,进行组合键的侦听
<div id="root"> <h2>{{name}}</h2> <input type="text" placeholder="按下回车键提示输入" @keyup.enter="into"> <!-- 修饰符连写 --> <input type="text" placeholder="按下ctrl+y提示输入" @keyup.ctrl.y="into"> </div> <script> Vue.config.keyCodes = 41; new Vue({ el: '#root', data: { name: '键盘事件演示' }, methods: { into(e) { console.log(e.target.value); } } }); </script>
计算属性
- 定义:要用的属性不存在,要通过已有的属性计算得来。
- 原理:底层借助了Object.defineproperty方法提供的getter和setter
- get函数什么时候执行?
- 初次读取时会执行一次
- 当依赖的数据发生改变的时候会被再次调用
- 优势:与methods实现相比,内部有缓存机制,效率更高,调试方便
- 注意:
- 计算属性最终会出现在vm上,直接读取使用即可
- 如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生变化(firstname、lastname)
代码演示
<div id="root">
<!-- v-model 双向数据绑定 -->
<p> 姓:<input type="text" v-model="firstname"> </p>
<p> 名:<input type="text" v-model="lastname"></p>
<!-- 上面的input内容只要发生变化,vue的模板都会重新解析一遍html,从而带动下面的函数的重载 -->
<p> 姓名:<span>{{fullname}}</span></p>
</div>
const vm = new Vue({
el: '#root',
data: {
firstname: '张',
lastname: '三'
},
//计算属性,computed内的就是计算属性,用来处理复杂的属性计算,里面的属性值也是用数据代理
//计算属性是有缓存的,get调用了一次,如果值没有改变,再次调用属性的时候就不会调用get函数了
//初次读取fullname时候,get会被调用,所依赖的数据发生变化时,get也会被调用
//computed 属性中的get不能被手动调用,Vue会在使用到属性的时候自动调用
computed: {
fullname: {
get() {
//此处的this是vm
return this.firstname + '-' + this.lastname;
},
//当fullname被修改的时候自动调用set
set(value) {
let arr = value.split('-');
//改的是这两个变量,而不是fullname
this.firstname = arr[0];
this.lastname = arr[1]
}
}
}
});
此外,fullname还有一种简写形式:
//fullname的简写形式
fullname() {
return this.firstname + '-' + this.lastname;
}
可以把属性写成一个函数,函数名就是属性名(es6)函数体默认就有getter的作用
注意!! 不要把fullname理解为一个函数了,它本身还是一个属性,函数只是负责计算返回结果,在模板内只需要写属性名即可!!!
监视属性watch
- 当被监视的属性变化时,回调函数自动调用,进行相关操作
- 监视的属性必须存在,才能进行监视操作
- 监视的两种写法
- new Vue 时传入 watch配置
- 通过vm.$watch('属性名',配置)
代码演示
<div id="root">
<h1>今天天气很{{Info}}</h1>
<button v-on:click="changeWeather">切换天气</button>
<!-- 如果函数功能很简单,可以直接在click里面写语句 -->
<button v-on:click="isHot = !isHot;">切换天气</button>
<!-- 但是这里面不能写alert等函数,因为Vue中的原型对象不包含window对象 -->
<!-- 解决方法:可以在vm实例中添加window属性,指向window对象即可 -->
<!-- <button v-on:click="isHot = !isHot;">切换天气</button> -->
</div>
const vm = new Vue({
el: '#root',
data: {
isHot: true
},
methods: {
changeWeather() {
this.isHot = !this.isHot;
}
},
computed: {
Info() {
return this.isHot ? '炎热' : '凉爽';
}
},
//监视属性
// watch: {
// isHot: {
// //立即执行,初始化时候,让handler调用一次
// immediate: true,
// //handler当ishot发生改变时调用
// handler() {
// console.log('isHot被修改了');
// }
// }
// },
});
`//监视属性的另一种写法$watch('属性名',配置)
//属性名如果找不到的话也不会报错
vm.$watch('isHot', {
handler() {
console.log('isHot被修改了');
}
})`
这里有一个小问题注意:当模板里不使用info变量的时候,点击切换天气,vue开发者工具里的变量显示可能不会更新,但vm内属性实际是更新了的
深度监视
看完了监视属性的简单使用,这时候我会提出一个需求,那就是,如果data属性内有一个对象,我们该怎么监视该对象里面的特定属性呢?
监视多级结构中某属性的变化
答案是使用 '对象名.属性名':{监视配置}
看代码:
data: {
isHot: true,
numbers: {
a: 1,
b: 1
}
}
还原成原始写法,就可以使用 点. 来进行单个数据的监视了,也就是深度监视
'numbers.a': {
handler() {
console.log('a被修改了');
}
},
这样就可以监视多级结构中的某个属性的变化了
深度监视的简易写法
如果监视属性里面的语句很简单,就可以使用简易写法:
//正常写法
isHot: {
// immediate: true,
// deep: true,
handler(newValue, oldValue) {
console.log('isHot被修改了');
}
}
//简写
isHot() {
console.log('isHot被修改了');
}
外部写法也是如此:
//正常外部写法
vm.$watch('isHot', {
immediate: true,
deep: true,
handler(newValue, oldValue) {
console.log('isHot被修改了');
}
})
`//简写
vm.$watch('isHot', function(newValue, oldValue) {
console.log('isHot被修改了', newValue, oldValue);
});`
watch 对比 computed
computed和watch的区别:
- computed能完成的功能,watch都可以完成
- watch能完成的功能,computed不一定能完成,例如,watch可以执行异步操作
重要的两个小原则:
- 所被vue管理的函数,最好写成普通函数,这样this的指向才会是vm
- 所有不会被vue所管理的函数,(定时器,ajax回调)。最好写成箭头函数,这样this指向才是vm
比如下面的姓名案例,用watch实现:
<div id="root">
<p> 姓:<input type="text" v-model="firstname"> </p>
<p> 名:<input type="text" v-model="lastname"></p>
<p> 姓名:<span>{{fullName}}</span></p>
</div>
new Vue({
el: '#root',
data: {
firstname: '张',
lastname: '三',
fullName: '张-三'
},
watch: {
firstname(newValue) {
//可以写定时器
setTimeout(() => {
this.fullName = newValue + '-' + this.lastname;
}, 1000);
},
lastname(newValue) {
this.fullName = this.firstname + '-' + newValue;
}
},
});
以上代码,会发现我在firstname的处理函数内写了一个定时器,达到了数据更改之后一秒后才执行更改的目的
绑定样式
绑定class样式
在vue中,绑定class的方式是使用 v-bind来动态处理class样式的
一共有三种方式:
- 字符串写法,适用于:样式类名不确定 ,需要动态指定
- 数组写法,适用于:样式类名不确定 ,个数也不确定,名字也不确定,需要动态指定
- 对象写法,适用于:样式类名确定 ,个数也确定,名字也确定,需要动态决定用不用
代码:
<div id="root">
<!-- 绑定class样式 字符串写法,适用于:样式类名不确定 ,需要动态指定 -->
<div class="basic normal" :class="mood" @click="changeMood">{{a}}1</div>
<hr>
<!-- 绑定class样式 数组写法,适用于:样式类名不确定 ,个数也不确定,名字也不确定,需要动态指定 -->
<div class="basic" :class="array">{{a}}2</div>
<button @click="md">点我删除类</button>
<hr>
<!-- 绑定class样式 对象写法,适用于:样式类名确定 ,个数也确定,名字也确定,需要动态决定用不用 -->
<div class="basic" :class="classObj">{{a}}2</div>
</div>
new Vue({
el: '#root',
data: {
a: 'hello',
mood: 'normal',
//存放类的数组
array: ['normal', 'happy', 'sad'],
classObj: {
//类的对象写法,false为不启用
happy: true,
sad: true
}
},
methods: {
changeMood() {
const arr = ['happy', 'sad', 'normal'];
const index = Math.floor(Math.random() * 3);
this.mood = arr[index]
},
md() {
this.array.shift();
}
},
});
绑定style样式
绑定style样式的方法和绑定class样式大同小异
使用 :style="styleObj"
进行绑定
对象名可以使用驼峰命名的css属性 fontSize: '44px'
也可以是原始css属性,但需要加上引号 'font-size': '44px'
代码演示
<!-- 绑定style样式 也是对象写法 -->
<div class="basic" :style="styleObj">{{a}}2</div>
`<!-- 绑定style样式 数组对象写法 -->
<div class="basic" :style="[styleObj1,styleObj]">{{a}}2</div>`
data: {
//样式的对象写法
styleObj: {
//vue中的css属性写法
// fontSize: '44px'
//也可以写原生css写法
'font-size': '44px'
},
styleObj1: {
//vue中的css属性写法
// fontSize: '44px'
//也可以写原生css写法
color: 'red'
}
}
条件渲染
1.v-if
写法:
- v-if="表达式"
- v-else-if="表达式"
- v-else="表达式"
适用于:切换频率较低的场景
特点:不展示DOM元素直接被移除
注意:v-if可以用:v-else-if ,v-else一起使用,但中间不能有元素打断
2.v-show
写法:v-show="表达式"
适用于:切换频率较高的场景
特点:不展示dom元素,未被移除,仅仅是使用样式隐藏掉
注意:使用v-if时候,元素可能无法被获取到,但是使用v-show是一定可以获取到的
v-if可以配合template模板使用
代码演示
<div id="root">
<!-- 条件渲染 相当于css的display -->
<h2 v-show="true">{{a}}</h2>
<h2 v-show="1===1">{{a}}</h2>
<h2 v-show="aa">{{a}}</h2>
&lt;hr&gt;
&lt;!-- 使用v-if做条件渲染 影响结构--&gt;
&lt;h2 v-if="aa" @click="n++"&gt;{{n}}&lt;/h2&gt;
&lt;h2 v-if="true"&gt;{{a}}&lt;/h2&gt;
&lt;h2 v-if="1===1"&gt;{{a}}&lt;/h2&gt;
&lt;!-- v-else-if --&gt;
&lt;!-- if和elseif 中间不能有元素打断 --&gt;
&lt;div v-if="n===1"&gt;1111&lt;/div&gt;
&lt;div v-else-if="n===2"&gt;2222&lt;/div&gt;
&lt;div v-else-if="n===3"&gt;3333&lt;/div&gt;
&lt;!-- v-else 不用写参数,是上面条件都不符合的情况 --&gt;
&lt;div v-else&gt;好&lt;/div&gt;
&lt;!-- 使用template模板配合if渲染多个元素 --&gt;
&lt;!-- 使用template模板只能配合if,不能配合show --&gt;
&lt;template v-if="n===4"&gt;
&lt;h2 v-show="true"&gt;template{{a}}&lt;/h2&gt;
&lt;h2 v-show="1===1"&gt;template{{a}}&lt;/h2&gt;
&lt;h2 v-show="aa"&gt;template{{a}}&lt;/h2&gt;
&lt;/template&gt;
</div>
new Vue({
el: '#root',
data: {
a: 'hello',
n: 1,
aa: true
}
});
v-for 指令
v-for指令可以用来循环遍历对象、数组、字符串、数字 到相应个数的标签中
作用、语法:
- 用于展示列表数据
- 语法:
v-for="(item,index) in xxx" :key="yyy"
- 可遍历: 数组、对象、字符串、数字
注意:只要你使用了遍历语法,你就必须要使用key这个关键字,给每一个li设定唯一的标识
格式::key="xxx"
代码演示:
<div id="root">
<h2>人员列表</h2>
<ul>
<!-- 使用v-for进行遍历输出 -->
<!-- 只要你使用了遍历语法,你就必须要使用key这个关键字,给每一个li设定唯一的标识 -->
<li v-for="p in persons" :key="p.id">{{p.name}}</li>
</ul>
<ul>
<!-- key的另一种写法 -->
<!-- a就相当于item,b就相当于index -->
<li v-for="(a,b) in persons" :key="b">A:{{a}} B:{{b}} </li>
&lt;/ul&gt;
&lt;ul&gt;
&lt;!-- 也可以使用of 遍历对象(in也可以) 这样a就是value b就是key了 --&gt;
&lt;li v-for="(a,b) of car" :key="b"&gt;key: {{b}} ---- value :{{a}}&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;!-- 甚至还能遍历字符串 --&gt;
&lt;li v-for="(a,b) in str" :key="b"&gt;key: {{b}} ---- value :{{a}}&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;!-- 甚至还能遍历输出数字 --&gt;
&lt;li v-for="(a,b) in 10" :key="b"&gt;key: {{b}} ---- value :{{a}}&lt;/li&gt;
&lt;/ul&gt;
</div>
new Vue({
el: '#root',
data: {
persons: [{
id: '001',
name: '张三',
age: '18'
}, {
id: '002',
name: '李四',
age: '19'
}, {
id: '003',
name: '王五',
age: '20'
}],
car: {
name: '马自达',
price: '114514'
},
str: 'kanokano123'
}
});
v-for中key的作用与坑
key在v-for中扮演者极为重要的角色,key是vue作为判断数据的唯一性的重要标识
所以,key的唯一性成了一个非常重要的前提保证
这里就要提一下Vue在生成数据的时候的步骤了:
- 首先vue会在渲染页面前,在计算机内存中计算并生成一个虚拟DOM ,然后才会渲染成真实DOM文档到页面上
- 如果需要在相同的地方进行重新渲染,Vue就会依次对比新数据和之前的虚拟DOM 数据是否一致,如果发现旧数据有一致的地方,则会复用旧的虚拟DOM 缓存渲染的真实DOM ,以提高渲染效率,如果不一致,则会按照新dom来重新渲染
既然是依次对比,所以,这里就会牵扯到顺序问题,vue是按照key进行顺序遍历对比的,所以,key一定要是一个唯一的值!!!
要点归纳:
- 虚拟DOM中key的作用:
- key是虚拟dom对象的标识,当状态中的数据发生变化时,Vue会根据新数据生成"新虚拟dom"
随后Vue进行"新虚拟dom" 与 "旧虚拟dom" 的差异比较,规则如下:
- key是虚拟dom对象的标识,当状态中的数据发生变化时,Vue会根据新数据生成"新虚拟dom"
- 对比规则:
- 旧虚拟DOM中找到了与新的虚拟DOM相同的key
- 若虚拟DOM中的内容没有改变,则会直接复用之前的真实dom
- 若虚拟DOM中的内容改变了,则会生成新的真实DOM,随后替换掉页面中之前的真实DOM
- 旧虚拟DOM中未找到与新虚拟DOM相同的key
- 创建真实的DOM,随后渲染到页面
- 旧虚拟DOM中找到了与新的虚拟DOM相同的key
- 用index作为key可能会引发的问题:
- 若对数据进行:逆序添加,逆序删除等破坏顺序的操作:
- 产生没有必要的真实DOM更改 => 界面没有问题,但是渲染效率降低
- 如果结构中还包括输入类的dom
- 会产生错误的DOM更新 => 界面有问题,顺序错乱
- 若对数据进行:逆序添加,逆序删除等破坏顺序的操作:
- 开发中如何选择key?
- 最好使用每条数据的唯一标识作为key,比如id,手机号,身份证号,学号等唯一值
- 如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于显示,使用index作为key是没有问题的
错误示例
<div id="root">
<h2>人员列表</h2>
<ul>
<button @click="add">点我添加老六</button>
<!-- 使用v-for进行遍历输出 -->
<li v-for="(i,index) in persons" :key="index">
{{i.name}}-{{i.id}}
<input type="text">
</li>
</ul>
</div>
data: {
persons: [{
id: '001',
name: '张三',
age: '18'
}, {
id: '002',
name: '李四',
age: '19'
}, {
id: '003',
name: '王五',
age: '20'
}]
},
methods: {
add() {
let p = {
id: '004',
name: '老六',
age: 21
};
//往数组第一个写入数据
this.persons.unshift(p);
}
}
点击之前:
点击之后 :
你会发现,老六旁边的输入框本应该是空的,结果变成了张三,导致下面的输入框全都错了一位
这是因为:Vue在对比新老虚拟DOM的时候,发现张三在index 0的位置,无法渲染,但是旁边的输入框在虚拟DOM中都是空的,Vue则会复用之前的实际DOM,然后继续往后依次比对,发现每一个的姓名和index都对不上,所以只能使用新虚拟DOM重新渲染名字了,但是,输入框在新老虚拟DOM中,都是空的,Vue就会误以为可以直接复用,就会把前三个复用掉,最后才会渲染新的input元素到最后
这里的主要问题就是,index不是唯一的,修改DOM树的时候容易造成查找不正确,从而进行错误的操作
解决方法
把 :key="index"
替换为 :key="i.id"
即可
或者在这个项目例子里,没有必要非要把老六放在第一位,可以使用push方法插入数据到数组尾部,这样就不会打乱index的标号顺序了
使用列表过滤实现简单搜索与排序
基本原理就是,使用计算属性于filter,sort函数
切记,计算属性执行的条件是:页面刚加载时和依赖数据发生变化时
<div id="root">
<h2>人员列表</h2>
<input type="text" placeholder="请输入姓名" v-model="Keyword">
<button @click="sortType=2">年龄升序</button>
<button @click="sortType=1">年龄降序</button>
<button @click="sortType=0">原顺序</button>
<ul>
<!-- key的另一种写法 -->
<!-- a就相当于item,b就相当于index -->
<li v-for="(a,b) in findPersons" :key="a.id">{{a.name}}-{{a.age}}-{{a.sex}}</li>
</ul>
</div>
new Vue({
el: '#root',
data: {
Keyword: '',
sortType: 0, //0原序 1 降序 2升序
persons: [{
id: '000',
name: '张三',
age: '11',
sex: '男'
}, {
id: '001',
name: '张麻子',
age: '28',
sex: '男'
}, {
id: '002',
name: '李四',
age: '19',
sex: '男'
}, {
id: '003',
name: '王五',
age: '50',
sex: '男'
}, {
id: '004',
name: '马冬梅',
age: '12',
sex: '女'
}],
// findPersons: []
},
//使用计算属性实现
computed: {
//这玩意开始的时候执行一次
//所依赖的keyword变化时候又会执行一次
//里面用到了sortType 所以sortType改变的时候也会触发这个函数
findPersons() {
const arr = this.persons.filter((p) => {
//true就是符合,返回一个新数组
//indexof对于空字符串的返回是0(特别重要)
return p.name.indexOf(this.Keyword) !== -1
});
//别急着返回,判断一下是否要排序
if (this.sortType) {
arr.sort((a, b) => {
//由于a和b在这里都是对象,不能直接比,我们要获取里面的age
return this.sortType == 1 ? b.age - a.age : a.age - b.age
})
}
return arr;
}
}
//#region
//使用监视属性实现
// watch: {
// Keyword: {
// //立即执行一次,否则页面没数据
// immediate: true,
// handler(val) {
// this.findPersons = this.persons.filter((p) => {
// //true就是符合,返回一个新数组
// //indexof对于空字符串的返回是0(特别重要)
// return p.name.indexOf(val) !== -1
// })
// }
// }
// },
//#endregion
});
Vue数据侦测
实现类似vue的简单数据检测
代码演示
//需要传入的data对象
let data = {
name: 'kano',
adress: 'Janpan'
};
//创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data);
console.log(obs);
//创建一个vm
let vm = {};
vm._data = obs;
vm.data = obs;
//创建一个名叫监视者的构造函数
function Observer(obj) {
//汇总对象中所有的属性,形成一个数组
const keys = Object.keys(obj);
//遍历
keys.forEach((k) => {
Object.defineProperty(this, k, {
get() {
return obj[k];
},
set(val) {
console.log("我被修改了,我要去处理页面响应");
obj[k] = val;
}
})
})
}
不完善的地方:无法找出数组中的对象并赋予getter setter , 多层对象也一样
使用Vue.set方法新增响应式属性
首先得注意的是:
这个set方法只能给vue data里面某个对象增加属性 !!!
也就是要操作的对象不能是Vue实例,或者Vue实例的根数据对象(data、_data)
语法
Vue.set(vue实例, '属性', 值或对象)
例子
const vm = new Vue({
el: '#root',
data: {
name: 'kanokano',
adress: '湖南',
student: {
name: 'tony',
// sex: '男',
}
},
});
`//vm.$set(vm.student, 'sex', '男')
//或者
Vue.set(this.student, 'sex', '男')`
数组更新检测
Vue将被监听的数组的变更方法进行了包装,所以也可以触发网页元素更新,方法包括:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
注意:使用下标索引直接修改数组数据是不会触发视图更新的!!!
错误示范
vm.hobby[0]='ddd';
上面的操作是不会生效的(
对于非修改性质的方法,比如filter() ,过滤完成后可以直接替换赋值掉原对象
vm.hobby = vm.hobby.filter((x)=>{
return x != '抽烟'
})
代码演示
const vm = new Vue({
el: '#root',
data: {
name: 'Xukun Cai',
hobby: ['唱', '跳', 'rap', '篮球'],
}
});
//添加元素
vm.hobby.push('你干嘛~ 哎哟');
vm.hobby.shift();
此外,除了可以用上面提到的几个方法操作数组之外,其实也可以使用Vue.set方法进行变更数据:
Vue.set(vm.hobby,0,'唱');
//或者
vm.$set(vm.hobby,0,'唱');
总结
Vue监视数据的原理:
-
Vue会监视data中所有层次的数据
-
如何检测对象中的数据
- 通过setter实现监视,且要造new Vue 时就要传入要监测的数据
- 对象中后追击啊的属性,Vue默认不做响应式处理
- 如需给后添加的属性做响应式,请使用如下API
vue.set(target,propertyName/index,value)
vm.$set(target,propertyName/index,value)
- 通过setter实现监视,且要造new Vue 时就要传入要监测的数据
-
如何监测数组中的数据?
- 通过包装数组对应更新元素的方法来实现,本质就是
- 调用原生对应的方法对数组进行更新
- 重新解析没模板,进而更新页面
- 通过包装数组对应更新元素的方法来实现,本质就是
-
vue修改数组中某一个元素一定要使用支持的方法
- API: push , pop , shift , unshift , splice , sort , reverse
- Vue.set 或者 vm.$set
特别注意的是:Vue.set 和 vm.$set 不能给vm或者vm的根数据对象添加属性!
对于非修改性质的方法,比如filter() ,过滤完成后可以直接替换赋值掉原对象
使用v-model收集表单数据
要点:
若:<input type="text">
则v-model收集的是value的值。
若:<input type="radio">
则v-model收集的是value的值,且要给标签配置value值
若:<input type="checkbox">
:
- 没有配置input的value属性:,那么收集的就是checked(布尔值)
- 配置input的value属性:
- v-model的初始属性就是非数组,那么收集的就是checked(布尔值)
- v-model的初始值是数组,那么收集的就是value组成的数组
备注:v-model的三个修饰符:
- lazy:失去焦点再收集数据
- number:输入字符串转换为有效数字
- trim:输入首尾空格过滤
代码演示
<div id="root">
<form @submit.prevent="demo">
<!-- trim修饰符,这个就不说了 -->
<label for="username"> 账号:</label><input type="text" id="username" v-model.trim="userInfo.account">
<br><br>
<label for="passwd"> 密码:</label><input type="text" id="passwd" v-model.trim="userInfo.password">
<br><br> 性别: 男
<input type="radio" name="sex" value="男" v-model="userInfo.sex"> 女<input type="radio" name="sex" value="女" v-model="userInfo.sex">
<br><br> 年龄
<!-- v-model也是有修饰符的,这里可以使用修饰符使传入的数据为数字类型 -->
<input type="number" name="age" value="男" v-model.number="userInfo.age">
<br><br> 爱好: 唱
<input type="checkbox" v-model="userInfo.hobby" value="唱"> 跳
<input type="checkbox" v-model="userInfo.hobby" value="跳"> rap
<input type="checkbox" v-model="userInfo.hobby" value="rap"> 篮球
<input type="checkbox" v-model="userInfo.hobby" value="篮球">
<br><br> 所属校区
<!-- 下拉框的v-model需要配置在select上而不是option上 -->
<select name="scholl" v-model="userInfo.city">
<option value="">请选择校区</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="shenzhen">深圳</option>
<option value="wuhan">武汉</option>
</select> 其他信息
<br><br>
<!-- lazy修饰符可以让用户输入完成之后再做出数据捕获的动作,节省资源 -->
<textarea cols="30" rows="10" v-model.lazy="userInfo.other"></textarea>
<br>
<input type="checkbox" v-model="userInfo.accept">阅读并接受<a href="//kanokano.cn">用户协议</a>
<br><br>
<button>提交</button>
</form>
</div>
new Vue({
el: '#root',
data: {
userInfo: {
account: '',
password: '',
//这里面要是有值的话,因为model的双向绑定,渲染的时候就会选中填入的值
//sex由于是radio,需要手动配置标签的value属性
sex: '',
age: '',
//hobby 是复选框,所以也需要手动配置value属性
//复选框需要的属性,必须是数组类型的
hobby: [],
city: 'beijing',
other: '',
accept: false
}
},
methods: {
demo() {
console.log(JSON.stringify(this.userInfo));
}
},
});
v-text 指令
**作用:**向其所在的节点中渲染文本内容
与插值语法的区别:会替换标签里面现有的文本
演示
<div id="root">
<div>{{name}}ddd</div>
<!-- v-text -->
<!-- 和插值语法的区别就是会替换标签里面现有的文本 -->
<div v-text="name">ddd</div>
</div>
new Vue({
el: '#root',
data: {
name: 'kano'
}
});
v-html 指令
作用:向指定节点中渲染包含html解构的内容,使用上和v-text没有太大区别,但还是有点
与插值语法的区别:
- v-html会替换掉节点中所有内容,插值语法则不会
- v-html可以识别html结构
!严重注意:v-html会有安全性的问题:
- 在网站上动态渲染任意HTML都是非常危险的,容易导致xss攻击
- 一定要在可信任内容上使用v-html 永远不要在用户提交的内容上使用!
演示
<div id="root">
<div>{{name}}ddd</div>
<!-- 可以解析html -->
<div v-html="html">ddd</div>
</div>
<script>
new Vue({
el: '#root',
//好用,但容易xss注入
data: {
html: '<a href="javascript:alert(document.cookie);">dddd</a>'
}
});
</script>
v-cloak指令
- 本质是一个特殊属性,Vue实例创建完毕并接管容器之后,会删掉v-cloak属性
- 使用css配合v-cloak可以解决网速慢时候页面展示出{{xxx}}的问题
演示
<style>
/*这里需要使用属性选择器进行选择性隐藏*/
[v-cloak]{
display: none;
}
</style>
<div id="root">
<div v-cloak>{{name}}</div>
<!-- 有时候我们需要把vue写在后面,这样会导致页面闪现问题 -->
<script src="../vue.js"></script>
</div>
new Vue({
el: '#root',
data: {
name: 'kano'
}
});
v-once指令
- v-once所在的节点在初次动态渲染之后,就视为静态内容了,
- 以后数据的改变不会引起v-once所在的结构的更新,可以用于优化性能
代码演示
<div id="root">
<div v-once>初始化的n是:{{n}}</div>
<div >当前的n是:{{n}}</div>
<button @click="n++">点我n+1</button>
<script src="../vue.js"></script>
</div>
new Vue({
el: '#root',
data: {
n:1
}
});
v-pre指令
- 跳过其所在节点的编译过程
- 可利用它跳过,没有使用指令的语法,没有使用插值与法的节点,会加快编译
代码演示
<div id="root">
<div v-pre>Vue</div>
<div v-once v-pre>初始化的n是:{{n}}</div>
<div v-pre>当前的n是:{{n}}</div>
<button @click="n++">点我n+1</button>
</div>
自定义指令
我们可以使用directives
编写自定义指令
directives内可以有两种写法:
- 函数式,属性是函数,适用于处理简单一点的事务
- 对象式,里面有三个函数:bind,inserted,update
- 指令与元素成功绑定时会执行bind
- 指令被所在元素插入页面时会执行inserted
- 指令所在的模板被重新解析时会执行update
注意,以上函数的this指向都为window
注意,指令定义的时候不加 v- 但使用的时候要加
局部写法:
new Vue({
directives:{指令名:配置对象}
})
//或者
new Vue({
directives(){}
})
全局写法:
Vue.directive('指令名',{bind,inserted,update})
或 Vue.directive('指令名',回调函数)
big函数何时会被调用?
- 指令与元素成功绑定时(绑定时候是虚拟dom状态,还没有放入页面)
- 指令所在的模板被重新解析时
特别注意!!!
自定义指令不能使用驼峰名称或者大写字母,因为Vue会把指令名上所有大写字母转成小写字母!!,推荐使用-(需要还原为带引号的属性名)或者_作为分隔符
例子
定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大十倍
<div id="root">
<!-- 定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大十倍 -->
<h2>当前的n是: <span v-text="n"></span></h2>
<h2>放大十倍的n是: <span v-big="n"></span></h2>
<button @click="n++">点我+1</button>
</div>
.....
//自定义指令区域
directives:{
//两个参数,一个是dom,另一个是参数对象
big(ele,binding){
ele.innerText = parseInt(binding.value) * 10;
},
}
....
定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点
<div id="root">
<!-- 定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点 -->
<input type="text" v-fbind:value="n">
</div>
当我们想要在一开始的时候获取input元素的焦点,除了可以在input标签里面添加autofocus之外,
我们还可以使用自定义指令,但是自定义指令如果写成函数式,就会出现页面加载后,自动对焦不起作用问题:
fbind(ele,binding){
ele.value = binding.value;
//无效
ele.focus()
}
其原因在于:一开始dom并没有先渲染出来,只有点击+1后才会生效,因为元素已经在页面里面,所以执行focus就会生效
正确写法:
//自定义指令区域
directives:{
fbind:{
//三个固定函数
//指令与元素成功绑定时
bind(ele,binding){
ele.value = binding.value;
},
//指令被所在元素插入页面时
inserted(ele,binding){
ele.focus();
},
//指令所在的模板被重新解析时
update(ele,binding){
ele.value = binding.value;
}
}
}
生命周期
Vue的生命周期中一共要执行很多重要的函数,mounted函数则是其中的一个
简介:
- 生命周期又叫生命周期回调函数、生命周期函数,生命周期钩子
- Vue在关键时刻帮我们调用的一些特殊名称的函数
- 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的
- 生命周期函数中的this指向的是VM 或 组件的实例对象
流程图
分析生命周期
vue实例的整个生命周期分别要执行如下几组函数:
- beforeCreate( ) 和 created( )
- beforeMount( ) 和 mounted( )
- beforeUpdate( ) 和 updated( )
- beforeDestroy( ) 和 destroyed( )
每个生命周期函数的作用:
beforeCreate函数
这个阶段初始化生命周期和事件,但是代理并没有开始
在这个阶段,无法通过vm访问倒data中的数据,method中的方法
created函数
生命周期和事件初始化完毕
在这个阶段,可以通过vm访问倒data中的数据,methods中的方法
beforeMount函数
在这个阶段,页面呈现的是未经Vue编译的DOM结构,故而在这期间,所有对dom的操作最终都是无效的
此时会发现,页面的基本结构已经渲染到页面上了,但是插值语法等并没有进行解析
这是因为在这期间,Vue并没有将真实DOM放到页面上,此时的DOM还是虚拟的
mounted函数
Vue完成模板解析并把真实DOM元素放入页面后(完成挂载)会调用一个名叫mounted的函数
这个函数整在个Vue的生命周期中只调用一次
此时页面中呈现的是经过VUe编译的ODM,对DOM的操作均有效(但不推荐)
至此初始化的过程就结束了
p.s: 这里一般可以进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件,等其他初始化操作
beforeUpdate函数
当Vue准备发生响应式变化的时候,会触发beforeUpdate系列函数
此时,数据是新的,但页面是旧的
即:页面尚未和数据保持同步
updated函数
当数据更新操作执行完毕后,会触发updated系列函数
此时,数据是新的,页面也是新的
即:数据和页面保持同步
beforeDestroy函数
这个函数是在Vue实例销毁之前所执行的
此时,vm中所有的 data,method 指令等等都处于可用状态,马上要执行销毁作业,
但是在这里所有的对this的操作都不会触发页面更新了
一般在这个阶段可以做:关闭定时器,取消订阅消息,解绑自定义事件等操作
destroyed函数
Vue销毁实例之后会执行destroyed回调函数
此时vm已经销毁
以上就是Vue实例的整个生命周期了
生命周期函数演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../vue.js"></script>
</head>
<body>
<div id="root">
<!-- 准备好一个容器 -->
<h2>当前的n值是:{{n}}</h2>
<button @click="add">点我n+1</button>
<button @click="bye">点我销毁容器</button>
</div>
<script>
const vm = new Vue({
//如果没有el的话,Vue的生命周期就会暂停在created函数
//直到遇到vm.$mount(el)调用
el: '#root',
data: {
n:1
},
methods: {
add(){
console.log('add()');
this.n++;
},
bye(){
console.log('destroy()');
this.$destroy()
}
},
beforeCreate() {
console.log('beforeCreate');
// console.log(this.n);
//此时this里面没有n
// debugger;
},
created() {
console.log('created');
// console.log(this.n);
},
beforeMount() {
console.log('BeforeMount');
//此时会发现,页面的基本结构已经渲染到页面上了,但是插值语法的{{n}}并没有解析成数字
// debugger;
},
mounted(){
console.log('mounted');
// debugger;
},
beforeUpdate() {
console.log('beforeUpdate');
console.log(this.n);
// debugger;
},
updated() {
console.log('updated');
// this.n++;//别整活儿,会不停的触发数据更新操作
},
beforeDestroy() {
console.log('beforeDestroy');
this.n++;//不起作用
},
destroyed() {
console.log('destroyed');
},
});
</script>
</body>
</html>
总结生命周期
常用的生命周期钩子:
- mounted:可以发送Ajax请求、启动定时器、绑定自定义事、订阅消息等【初始化操作】
- beforeDestroy:清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】
关于销毁Vue实例
- 销毁后借助Vue开发者工具不会看到任何信息
- 销毁后自定义事件会失效,但是原生DOM事件依旧有效(vue2.7版本之后无效)
- 一般不会在beforeDestory中操作数据,因为那样不会出发数据/页面更新流程了
Vue组件化编程
传统方式与组件化方式的区别
传统方式编写应用的问题:
- 依赖关系混乱,不好维护
- 代码复用率不大,耦合性较高
组件化编程方式的优点:
- 依赖关系清晰,方便维护
- 代码复用率非常高,组件之间互不影响
组件的概念
所谓的组件就是实现应用中局部功能代码和资源的集合
模块与组件、模块化与组件化
模块
理解:向外提供特定功能的 js 程序,一般为一个js文件
使用原因: js文件多而杂
作用:复用js,简化js的编写,提高js运行效率
组件
理解:用来实现局部(特定)功能效果的代码集合(html/css/js/image)
使用原因:一个界面的功能很复杂
作用:复用编码,简化项目编码,提高运行效率
模块化
当应用中的js都以模块来编写的,那这个应用就是一个模块化的应用
组件化
当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用
非单文件组件与单文件组件
非单文件组件
一个文件中包含有N个组件
单文件组件
一个文件中只包含有一个组件
非单文件组件编写
Vue使用组件的三大步骤
- 定义组件(创建组件)
- 注册组件
- 使用组件(写组件标签)
如何定义?
使用Vue.extend(options)*创建,其中options和new Vue(options) 时传入的options几乎一样,但也有区别
区别如下:
- el不要写,因为最终左右的组件都要经过一个vm管理,由vm中的el决定服务哪个容器。
- data必须写成函数,为什么? --- 避免组件被复用时,数据存在引用关系,使用函数形式,其中的return可以自动复制对象(深拷贝)
备注1:使用template可以配置组件html解构
备注2:vue.extend()方法其实是vue的一个构造器,继承自vue 使用基础 Vue 构造器,创建一个"子类"。参数是一个包含组件选项的对象。
如何注册?
- 局部注册:靠newVue时候传入的components选项
- 全局注册:靠Vue.component('组件名',组件);
编写组件标签
使用 <组件名></组件名>
来在页面上引用组件标签
代码示例
<div id="root">
<!-- 3.编写组件标签 -->
<school></school>
<!-- 复用,事件对象互相独立 -->
<school></school>
<h1>{{msg}}</h1>
<student></student>
<hr>
<!-- 全局组件 -->
<hello></hello>
</div>
//1.注册组件
//创建一个school组件
const school = Vue.extend({
//不要写el配置项,因为左右组件最终都有vm实例管理
//写template
template: `
<div>
<h2>学校名称 {{name}}</h2>
<h2>学校地址 {{address}}</h2>
<button @click="ChangeName">点我修改学校名为加里顿</button>
</div>
`,
// el: '#root',
//data必须写为函数式,因为对象式为引用类型,会造成一些问题
//使用函数形式,其中的return可以自动复制对象(深拷贝)
data() {
return {
name: 'MIT',
address: 'USA'
}
},
methods: {
ChangeName() {
this.name = "加里顿"
}
},
})
//创建一个student组件
const student = Vue.extend({
//写template
template: &lt;div&gt; &lt;h2&gt;学生姓名 {{name}}&lt;/h2&gt; &lt;h2&gt;学生年龄 {{age}}&lt;/h2&gt; &lt;/div&gt;
,
data() {
return {
name: 'kano',
age: 19
}
}
})
//全局组件
const globalHello = Vue.extend({
template: &lt;div&gt; hello1234 &lt;/div&gt;
,
data() {
return {
name: 'TOM'
}
}
})
//全局注册组件
Vue.component('hello', globalHello)
//2.注册组件(局部注册)
//创建vm new Vue({ el: '#root', //填入组件 components: { //es6缩写 school, student }, data: { msg: 'hello' } });
组件的命名方法
对于单个单词组成:首字母大写即可
对于多单词组成:
- 全部小写,使用短横线 ' - ' (kebab-case命名) 连接 :my-component
- 每一个单词的首字母大写 (camelCase命名) :MyComponent (只能在脚手架环境中使用)
备注:
- 组件名尽可能回避原生的标签名,例如:h2、H2 都不可以
- 可以使用name配置项指定组件在开发者工具中呈现的名字
关于组件标签:
- 第一种写法:
<kano></kano>
- 第二种写法:
<kano/>
- 备注:不使用脚手架时, 会导致后续组件无法渲染
一个简写形式:
const school = Vue.extend(options)
可简写为:const school = options
可以简写是因为最后vm中的components会判断传入的是extends过的还是原对象,从而做出不同的反应
组件的嵌套
只需要注意一点:想嵌套到哪个组件就放到哪个组件内
代码示例:
<div id="root">
<h1>{{msg}}</h1>
</div>
//创建一个student组件
const student = Vue.extend({
//写template
template: `
<div>
<h2>学生姓名 {{name}}</h2>
<h2>学生年龄 {{age}}</h2>
</div>
`,
data() {
return {
name: 'kano',
age: 19
}
}
})
//school组件
const school = Vue.extend({
//可以使用name属性强制指定组件在开发者工具中呈现的组件名
name: 'kano',
// student是school的子组件,所以要写在school的模板里面
template: &lt;div&gt; scholl组件开始 &lt;h2&gt;学校名称 {{name}}&lt;/h2&gt; &lt;h2&gt;学校地址 {{address}}&lt;/h2&gt; &lt;button @click="ChangeName"&gt;点我修改学校名为加里顿&lt;/button&gt; &lt;student&gt;&lt;/student&gt; scholl组件结束 &lt;/div&gt;
,
data() {
return {
name: 'MIT',
address: 'USA'
}
},
methods: {
ChangeName() {
this.name = "加里顿"
}
},
//嵌套注册(注意student要在school前面)
components: {
student
}
})
//定义hello组件
const hello = Vue.extend({
template: &lt;h1&gt;{{msg}}&lt;/h1&gt;
,
data() {
return {
msg: 'hello world'
}
}
})
//定义app组件 const app = Vue.extend({ template: ``&lt;div&gt; &lt;school&gt;&lt;/school&gt; &lt;hello&gt;&lt;/hello&gt; &lt;/div&gt;``, components: { school, hello } }) //创建vm new Vue({ template: ``&lt;app&gt;&lt;/app&gt;``, el: '#root', //注册组件(局部) components: { //es6缩写 app }, data: { msg: 'hello' } });
以上代码组件结构如下:
<root>
<app>
<kano>
<hello>
总结:被嵌套属性需要在template属性中写上嵌套者的组件标签,components中写上嵌套者的组件名即可完成嵌套操作
VueComponent构造函数
关于VueComponent:
- school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
- 我们只需要写
<school/>
或<school></school>
,Vue解析时会帮我们创建school组件的实例对象
即Vue帮我们执行的:new VueComponent(options)
- 特别注意:每次调用Vue.extend。返回的都是一个全新的VueComponent!!
所以每个标签组件就可以复用,因为他们都是独立的个体实例对象 - 关于this的指向:
- 组件配置中:
- data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】
- new Vue()配置中
- data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】
- 组件配置中:
- VueComponent的实例对象,以后简称vc (组件实例对象)
Vue的示例对象,以后简称vm
VueComponent内部实现
如果我们创建了多个组件,分别输出组件的值:
//创建一个hello组件
const hello = Vue.extend({
template: `<h2>{{msg}}</h2>`,
data() {
return {
msg: '你好'
}
}
})
console.log(hello);
//school组件
//school组件
const school = Vue.extend({
//可以使用name属性强制指定组件在开发者工具中呈现的组件名
name: 'school',
// student是school的子组件,所以要写在school的模板里面
template: `
<div>
<h2>学校名称 {{name}}</h2>
<h2>学校地址 {{address}}</h2>
<button @click="ShowName">点我提示</button>
</div>
`,
data() {
return {
name: 'MIT',
address: 'USA'
}
},
methods: {
ShowName() {
//这里的this是VueComponent的实例对象
//不过在没渲染的时候,VueComponent并没有执行,所以这个this其实就是构造函数中的this
alert(this.name)
}
},
})
console.log(school);
结果都为
ƒ VueComponent(options) {
this._init(options);
}
但是从Vue.extend的源码来看,这两个VueComponent并不是同一个函数
因为Vue.extend源码是这样写的:
大致代码如下:
Vue.extend = function (extendOptions) {
//省略。。。
var Sub = function VueComponent(options) {
this._init(options);
};
//省略。。。
return Sub;
};
每次调用Vue.extend的时候,都会创建一个新变量用于接收新构造函数
所以每个组件的VueComponent都成为了独立的个体
直到需要渲染到页面时候,就会执行这些构造函数
如何获取Vue所管理的组件实例对象
可以使用
vm.$children
属性获取
vm.$children
属性返回的是一个数组,里面有vm管理的组件实例
例:
//创建vm
vm = new Vue({
el: '#root',
components: {
school,
hello
}
});
console.log(vm.$children);
备注:如果组件里面也有子组件,那这个组件的$children属性里面就会显示存放子组件
vm与vc的本质区别
vm与vc相比:
- vc没有独立的el属性
- vc的data必须写成函数式
- vc是VueComponent的实例对象,而vm是Vue的实例对象
组件的一个重要的内置关系
概述
一个重要的内置关系:VueComponent.prototype.proto === Vue.prototype
为什么要有这个关系:让组件实例对象(vc)可以访问到Vue原型上的属性和方法
这涉及到原型链的问题,我们知道,VueComponent 和 Vue 实例化之后两者的内置的方法和属性其实都大同小异,其中的关键在于,Vue复用了一些方法与属性:指 通过更改原型指向的方式共享Vue上的一些属性和方法 这种方式也叫 原型链继承
代码演示:
//创建hello组件
const hello = Vue.extend({
template: `<h1>hello</h1>`
});
`//创建vm
vm = new Vue({
el: '#root',
components: {
hello
}
});
console.log(hello.prototype.`proto` === Vue.prototype) //true`
Vue的原型链大致为:
Vue --> Vue的原型对象 --> Object的原型对象 --> null
VueComponent的原型链大致为:
VueComponent --> VueComponent的原型对象--> Vue的原型对象 --> Object的原型对象 --> null
图解
单文件组件的编写
简介
单文件组件文件的扩展名是.vue
文件内可以写三种标签:
<template>
标签 ,用于填写预解析模板,也就是组件的结构<script>
标签,用于填写组件交互相关的代码(数据,方法等)<style>
标签,用于填写组件的样式
注意:多个vue单文件组件,最终会汇总于App.vue文件中,方便统一管理
script标签中由于填写的是组件相关的交互代码,故需要使用export向外暴露需要的变量
目录结构
一般一个单文件组件写成的工程的文件树是这样的
- Root
- School.vue
- Student.vue
- App.vue
- main.js
- index.html
其中:
- App.vue汇总了所有的组件
- main.js内实例化了vm
- index.html提供了网页基础结构
代码演示:
School.vue
<template>
<!-- 组件的结构 -->
<div>
<h2 class="demo">学校名称 {{name}}</h2>
<h2>学校地址 {{address}}</h2>
<button @click="ShowName">点我提示</button>
</div>
</template>
<script>
//简写 Vue.extend可以省略
export default {
name: 'School',
data() {
return {
name: 'MIT',
address: 'USA'
}
},
methods: {
ShowName() {
alert(this.name)
}
},
}
</script>
<style> /
组件的样式/ .demo{ color: pink; } </style>
Student.vue
<template>
<div>
<h2>学生姓名 {{name}}</h2>
<h2>学生年龄 {{age}}</h2>
</div>
</template>
`<script>
//创建一个student组件
export default {
name:'Student',
data() {
return {
name: 'kano',
age: 19
}
}
}
</script>`
App.vue
<template>
<div>
<School></School>
<Student></Student>
</div>
</template>
`<script>
// 组件最终必须汇总于App.vue
// 引入组件
import School from './School.vue';
import Student from './Student.vue';
export default {
name:'App',
components:{
School,
Student
}
}
</script>`
main.js
// vm初始化在这
import App from './App.vue';
new Vue({
el: '#root',
components: { App }
})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../vue.js"></script>
</head>
<body>
<div id="root">
<App></App>
</div>
<!-- 运行不了,需要在脚手架环境 -->
<script type="module" src="./main.js"></script>
</body>
</html>
不过,你会发现,以上代码根本执行不了,原因是需要Vue脚手架的支持,脚手架将在下面小节讲,这里只是熟悉一下单文件组件的编写方法((
Vue脚手架
介绍
Vue脚手架又叫 Vue CLI(Command Line Interface)
安装
第一步:安装Node.js 后,在命令行直接输入
npm install -g @vue/cli
第二步:切换到你要创建项目的目录,然后使用命令
vue create xxxxx
第三步:启动项目
npm run serve
目录结构及文件描述
目录树如下:
│ babel.config.js babel的配置文件
│ jsconfig.json babel的配置文件
│ package.json 应用包的配置文件
│ README.md
│ vue.config.js 配置webpack相关
│ yarn.lock
│
├─public
│ favicon.ico 页签图标
│ index.html 主页面
│
└─src
│ App.vue
│ main.js
│
├─assets 存放静态资源
│ logo.png
│
└─components 存放组件
HelloWorld.vue
其中:
babel.config.js 是babel的控制文件,用于将es6语法向下转换为es5的兼容语法,一般不需要做改动
package.json 是包的管理文件,用来记录vue整个工程的名字,版本,主要包,启动参数等
yarn.lock 是yarn的依赖包版本锁,用于控制依赖包及依赖包版本
public 目录用于存放静态html资源,也是网页的入口
src 目录用于存放源码,下面细说:
- src 根目录一般存放 main.js App.vue
- asserts目录里面主要是存放一些前端资源文件,如图片媒体等
- components目录主要存放Vue的组件
README.MD 是说明文档啦~
P.S:如果安装了lint工具的话,记得在编写程序的时候不要使用它,不然会影响编程效率(
执行vue serve
执行了serve操作之后,vue首先就会执行main.js 之后会按照流程创建vm。引入组件等操作
注意:run 之后 可能会有lint的报错导致无法启动,为了方便,可以直接在vue.config.js里面的defineConfig函数对象参数中添加 lintOnSave:false
即可
Render函数
问题
我们之前在main.js内Vue函数里面是这样写的
new Vue({
el: '#app',
compoments: { App }
})
但是,当我们使用脚手架打包运行的时候,会提示如下错误
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
(found in )
通过提示可知道:现有的vue运行时环境并不能使用模板编译器,可以选择使用render函数,或者使用完整的带解析器的vue运行环境
因为我们引用的时候直接写了import Vue from 'vue'
在vue的源代码内其实是引用了 dist/vue.runtime.esm.js
(ECMAScript Module)
p.s:不引入完整的vue执行和渲染效率会高一点,占用空间会低一点
解决方法
- 使用完整版的Vue:
import Vue from 'vue/dist/vue'
- 使用render函数
Render函数原理
render函数包含一个参数,类型为function 默认是createElement函数
render(createElement) {
console.log(typeof createElement); //function
return createElement(App)
}
箭头函数写法:
new Vue({
el: '#app',
render: h => h(App)
})
关于不同版本的vue
- vue.js 与 vue.runtime.xx.js的区别:
- vue.js是完整版的Vue,包含:核心功能+模板解析器
- vue.runtime.xxx.j是运行版的Vue,只包含:核心功能,没有模板的解析器
- 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容
修改默认配置
Vue脚手架隐藏了所有的 webpack 相关的配置,如果香查看具体的webpack的配置,请执行: vue inspect > output.js
可以修改
entry: {
app: [
'./src/main.js'
]
}
这个对象来自定义入口函数的名字
但直接修改这一句显然是不会奏效的,需要修改vue.config.js
vue.config.js 是一个可选的配置文件,如果项目的(和package.json同级的)根目录中存在这个文件,那么它会被@vue/cli-service自动加载。你也可以使用package.json中的 vue字段,但是注意这种写法需要你严格遵照JSON的格式来写。
如果我们想要修改main.js入口文件的名字,需要进入vue.config.js中的pages对象中进行修改:
//自定义pages
pages: {
index: {
// page 的入口
entry: 'src/main.js',
// 模板来源
template: 'public/index.html',
// 在 dist/index.html 的输出
filename: 'index.html',
// 当使用 title 选项时,
// template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
title: 'Index Page',
// 在这个页面中包含的块,默认情况下会包含
// 提取出来的通用 chunk 和 vendor chunk。
chunks: ['chunk-vendors', 'chunk-common', 'index']
}
}
其他配置项可以i对脚手架进行个性化定制,详见:配置参考 | Vue CLI (vuejs.org)
使用ref标记DOM
有些时候我们需要操作DOM,但在Vue中使用原生DOM操作指令显得不是太友好,所以Vue为我们提供了一个比较好用的操作DOM的方法:ref标记
只需要给template标签中的元素添加一个ref属性即可:
<h1 v-text="msg" ref="title"></h1>
然后就可以使用vc中的:$refs
进行控制DOM等操作
...
// 使用$refs可以输出设定的ref标记的DOM
console.log(this.$refs.title);
...
但这里有个需要注意的点:如果给组件标签打上ref属性,那$refs的输出则是该组件标签与之对应的组件实例对象:
<School ref="school" />
...
//组件标签的ref就是与之对应的组件
console.log(this.$refs.school);
...
ref总结:
- ref属性被用来给元素或子组件注册引用信息 (id的替代者)
- 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
- 使用方式:
- 打标识:
- 获取: this.$refs.msg
配置项porps
功能: 让组件接收外部传过来的数据
-
传输数据:
<Demo name="xxx"/>
-
接收数据:
-
第一种方式(只接收):
props:['name']
-
第二种方式(限制类型)
props:{ name:Number }
-
第三种方式(限制类型,限制必要性,指定默认值)
props:{ name:{ type:String, //类型 required:true, //必要性 default: '33' } }
-
备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发生警告,若业务需求需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
混入(mixin)
功能: 可以把多个组件公用的配置提取成一个混入对象
使用方式:
-
第一步定义混合,例如:
export const mixin = { data(){...}, methods:{...} ... }
-
第二步使用混入,例如:
- 全局混入:Vue.mixin(mixin)
- 局部混入:mixins:[mixin]
代码示例:
准备混入文件(mixin.js)
//分别暴露,抽离方法成混合
export const mixin = {
methods: {
showName() {
alert(this.name)
}
},
mounted() {
console.log('加载完成');
},
}
export const mixin2 = {
data() {
return {
msg: 'kanokano'
}
}
}
局部混入(组件.vue):
//局部混合
import {mixin,mixin2} from '../mixin.js'
export default {
name:'School',
data(){
return{
name:'MIT',
address:'ddddd'
}
},
// mixins:[mixin,mixin2]
}
全局混入(main.js):
//全局混合
import { mixin, mixin2 } from './mixin'
Vue.mixin(mixin)
Vue.mixin(mixin2)
插件
功能:用于增强Vue
本质:包含一个install方法的一个对象,install的第一个参数是Vue ,第二个以后的参数是插件使用者传递的数据。
定义插件:
对象.instll = function(Vue,options){
//添加全局过滤器
Vue.filter(...);
//添加全局指令
Vue.directive(...);
//配置全局混合(入)
Vue.mixin(...);
//添加实例方法
Vue.prototype.$myMethod = function(){...};
Vue.prototype.$myProperty = xxxx;;
}
使用插件:Vue.use(插件名)
scoped(局部)样式
在编写组件的时候,我们在每个组件中的style样式里面编写很多自定义样式,但是每个样式名字很容易重合,如果名字重合了,会出现样式覆盖的问题。
比如: 在student.vue
中,编写了demo
样式,在school.vue
中同样也编写了demo样式,而且在app.vue
中先引入了school
,后引入student
,就会出现后者的样式覆盖掉前者的同名样式的问题
想要解决这个问题的话,需要使用一个全新的配置项: scoped
用法: 只需要在style标签中添加即可
<style scoped>
.demo{
background-color: skyblue;
}
</style>
样式块兼容less
想要在style 块中兼容less ,那么就需要在style 标签里面添加 lang属性,写法如下:
<style scoped lang="less">
.demo{
background-color: orange;
.sex{
background-color: green;
}
}
</style>
注意:直接保存可能会报错,因为没有安装webpack 的 less -loader插件
使用 npm i less-loader
即可
*不写lang属性默认就是css
组件化编码流程(通用)
- 实现静态组件:抽取组件。使用组件实现静态页面效果
- 展示动态数据:
- 数据的类型、名称是什么?
- 数据保存在哪个组件?
- 交互------从绑定事件监听开始
TodoList案例
代码下载:https://kanokano.cn/wp-content/uploads/2022/08/todo.zip
总结案例
- 组件化编码流程:
- 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
- 实现动态组件:考虑好数据的存放位置,数据是一个组件再用,还是一些组件再用:
- 一个组件再用,放在组件自身就行
- 一些组件再用:放在他们的共同的父组件上(状态提升)
- 实现交互:从绑定事件开始
- props适用于:
- 父组件 ==》 子组件 通信
- 子组件 ==》 父组件 通信(要求父先给子一个函数)
- 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以被修改的
- props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这么做
- 可以通过本地存储进行保存状态
组件的自定义事件
-
一种组件间通信的方式,适用于:子组件===》 父组件
-
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)/
-
解绑自定义事件:
-
第一种方式,在父组件中:
<Demo @kano="test"/>
或者<Demo v-on:kano="test">
-
第二种方式,在父组件中:
<demo ref="dd" />
.... mounted(){ this.$refs.dd.$on('kano',this.test) }
-
若想让自定义事件只能触发一次,只需要加上
once
修饰符或者$once
方法 -
触发自定义事件:
this.$emit('kano',传参)
-
解绑自定义事件:
this.$off('kano')
不写参数则为全部解绑,多参数需写成数组形式 -
组件上也可以绑定原生DOM事件,但需要native修饰符,例如
<Student @click.native="show"/>
-
注意:通过
this.$refs.xxx.$on(xxx,回调函数)
绑定自定义事件时,回调要门配置在method中,要么使用箭头函数,总是需要注意this的指向
-
P.S: 组件的自定义事件可以在Todolist中用,替换掉全选等方法
全局事件总线
全局事件总线(Global Event Bus):可以实现任意组件之间的互相通信,数据传递
全局事件总线,其实不是一个特定的接口或者方法,而是前人总结出来的一种实现组件间互相通信的结构
原理与使用
由于Vue有一个内置的关系:VueComponent.prototype.__proto__ === Vue.prototype
我们可以直接利用这个内置关系,在Vue.prototype上创建一个总线的载体$bus
(一般为vm的实例对象)
安装全局事件总线:
new Vue({
......
//生命周期最开始的钩子
beforeCreate() {
//this就是这个vm
//事件总线$bus是我们自己定义的
Vue.prototype.$bus = this;
}
})
使用事件总线:
1.接收数据:A组件想要接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身
methods(){
demo(data){......}
}
......
mounted(){
this.$bus.$on('xxx',this.demo)
}
2.提供数据:this.$bus.$emit('xxx',数据)
P.S:最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件
//School.vue
...
beforeDestroy(){
//用完就销毁,防止占用总线
this.$bus.$off('hello');
}
小结
全局事件总线,其实就是在普通的自定义事件上,稍作修改,把事件绑定在了一个所有组件都能访问的原型对象(VM)上而已
消息订阅与发布(pubsub)
概念
pubsub 是一种组件间通信的方式,适用于任意组件间通信。
使用步骤
-
安装pubsub:
npm i pubsub-js
-
引入:
import pubsub from pubsub-js
-
接收数据:A组件想要接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
methods(){ demo(data){...} } ...... mounted(){ this.pid = pubsub.subscrible('xxx',this.demo)//订阅消息 }
-
提供数据:
pubsub.publish('xx',数据)
-
最好在beforeDestroy钩子中,用
pubsub.unscrible(pid)
去 取消订阅
这个组件能实现类似全局事件总线的功能,但是相比全局事件总线来说,需要手动安装,没有全局事件总线的原生优势
$nextTick
这个是在TodoList项目中使用的一个新的方法
语法 :this.$nextTick(回调函数)
作用:在下一次DOM更新结束后执行指定的回调方法
nextTick主要是用于处理数据动态变化后,DOM还未及时更新的问题,用nextTick就可以获取数据更新后最新DOM的变化:
在Todolist项目中,当我们点击编辑按钮时,会出现一个input框,我们就需要在点击编辑按钮后立马获取input的焦点,如果直接在编辑按钮的点击事件中focus input框的话,是无效的,因为Vue要先执行完method里面的东西之后,才会更新DOM文档
这时候就会想到,可以用异步代码去滞后执行自动对焦。比如定时器
当然是有更合理的解决方法的,Vue给我们提供了一个方法#nextTick
用nextTick就可以在模板更新后调用预设的方法:
edit(todo){
......
this.$nextTick(()=>{
this.$refs.autoFocus.focus();//自动对焦
})
}
Vue封装的过渡与动画
-
作用:在插入、更新、或移除DOM元素时,在核实的时候给元素添加样式类名
-
图示:
-
写法:
-
准备好样式:
- 元素进入的样式:
- v-enter:进入的起点
- v-enter-active:进入过程中
- v-rnter-to:进入的终点
- 元素离开的样式:
- v-leave:离开的起点
- v-leave-active:离开过程中
- v-leave-to:离开的终点
- 元素进入的样式:
-
样式写法示例:
/*vue能自动识别的样式*/ .kanokano-enter-active,.kanokano-leave-active{ transition: .5s linear; } /*进入的起点*//*离开的终点*/ .kanokano-enter,.kanokano-leave-to{ transform: translateX(-100%); } /*进入的终点*//*离开的起点*/ .kanokano-enter-to,.kanokano-leave{ transform: translateX(0); }
-
使用 包裹要过度的元素,并配置name属性:
<transition name="kanokano" appear> <h1>HELLO</h1> </transition></code></pre></li> <li><p>备注:若有多个元素需要过度,则需要使用:<transition-group> ,且每个元素都要指定key值: </transition-group></p> <pre><code class="language-html"> <transition-group name="kanokano" appear> <h1 v-show="isShow" class="gogogo" key="1">HELLO</h1> <h1 v-show="!isShow" class="gogogo" key="2">HELLO</h1> </transition-group></code></pre></li> <li><p>想让元素默认是显示,可以添加 appear 属性</p></li> <li><p>样式的名字默认是v开头,如果想自定义样式头,可以在标签上加上name属性即可</p> <pre><code class="language-html"> <transition name="kanokano" appear> <h1 v-show="isShow" class="gogogo">HELLO</h1> </transition>
使用插件控制过渡(animate.css):
首先需要安装插件:npm install animate.css
//由于是js,所以不需要from什么什么的
-
import 'animate.css';
<br />
<transition-group name="animate__animated animate__bounce" appear enter-active-class="animate__bounceInLeft" leave-active-class="animate__backOutLeft"> <h1 v-show="isShow" class="gogogo" key="1">HELLO</h1> <h1 v-show="!isShow" class="gogogo" key="2">HELLO</h1> </transition-group></code></pre></li>
-
Vue代理
当我们在脚手架中编写ajax请求的时候,与服务器通信,有时候会遇到跨域问题
一般的做法就是在后端进行处理,设置响应头,或者使用cors插件进行跨域处理
但我们也可以使用 代理服务器 的方式进行间接的请求
代理服务器请求原理:
因为ajax请求有跨域问题,但是代理服务器与后端服务器之间的通信并不是使用ajax进行传输的,所以自然不会存在跨域的问题
使用方法
配置
-
在vue.config.js中添加一个新属性:
// 开启代理服务器 devServer: { proxy: '后端服务器地址'//例如:http://api.kanokano.cn }
这会告诉开发服务器将任何未知请求 (没有匹配到静态文件的请求 ) 代理到
后端服务器地址
。 -
把需要ajax请求的地址统一换为脚手架的服务器地址:
axios.get('http://localhost:8080/student').then( response =>{ console.log('请求成功',response.data); }, error=>{ console.log('请求失败',error.message); } )
-
如果你想要更多的代理控制行为,也可以使用一个
path: options
成对的对象。devServer: { proxy: { '/api': {//匹配所有以'/api'开头的请求路径 target: '
<url>',//代理目标的基础路径 ws: true,//websocket changeOrigin: true //修改请求源 }, '/foo': { target: ' <other_url>' }
} }
</other_url> </url></code></pre> <p><strong>注意:有时设置代理的时候,需要去掉请求头 比如上面的<code>'/api'</code> 防止带入到后端服务器造成404</strong><br> 使用<code>pathRewrite</code> 重写请求地址,格式:<code>pathRewrite:{'正则表达式','替换字符串'}</code></p> <pre><code class="language-javascript">proxy: {
'/api': { target: '
<url>', pathRewrite: { '^/api': '' }
} },
</url></code></pre></li>
github搜索框案例
案例下载:本地下载
Vue-resource
注意,此插件已过时
如果不想用axios发送http请求,也可以使用一个Vue插件:Vue-resource
安装:
npm i vue-resource
引入:
//引入,使用资源文件
import vueResource from 'vue-resource';
Vue.use(vueResource);
使用:
//不使用axios 使用插件带的$http 是一样的效果,不过用的少
this.$http.get(`xxx`).then(
response => {
...
},
error => {
...
}
)
slot 插槽
当我们想要在同一个组件里面展示不同内容的时候,除了使用v-if之外,还可以使用插槽 来分发内容
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信方式,适用于 父组件 ===》子组件
分类: 默认插槽,具名插槽,作用域插槽
默认插槽
我们可以通过定义一个<slot></slot>
来作为一个插槽
使用方法:
App.vue
<Category title="美食" :ListData="foods">
<img src="xxx" alt="">
</Category>
<Category title="游戏" :ListData="games"/>
<Category title="电影" :ListData="films">
<video src="xxxxx" muted="muted" autoplay="autoplay" loop="loop">
您的浏览器不支持HTML5标签。
</video>
</Category>
Category.vue
<!-- 定义一个插槽(挖个坑等待父级填充,如果父级有填充,则使用父级的结构,没有就使用插槽内的结构) -->
<slot>
<ul>
<li v-for="(item,index) in ListData" :key="index">{{item}}</li>
</ul>
</slot>
具名插槽
有时我们需要多个插槽,默认插槽显然已经不能满足多插槽的使用情景,下面将引入具名插槽
使用方法:
App.vue
<Category title="美食">
<!-- 自定义插槽名 -->
<img slot="center" src="XXX" alt="">
<a slot="footer" href="#">更多美食</a>
</Category>
Category.vue
<!-- 定义一个插槽(挖个坑等待父级填充,如果父级有填充,则使用父级的结构,没有就使用插槽内的结构) -->
<slot name="center"></slot>
<slot name="footer"></slot>
作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定,(games数据在category组件中,但使用数据所遍历出来的结构由app组件决定)
2.具体编码:
App.vue
<Category title="游戏" >
<!-- 子组件传递的数据包装了一层对象 -->
<template scope="scop">
<ul>
<li v-for="(item,index) in scop.games" :key="index">{{item}}</li>
</ul>
<h3>{{scop.msg}}</h3>
</template>
</Category>
<Category title="游戏" >
<!-- 还可以支持解构赋值 -->
<template scope="{games}">
<ol>
<li v-for="(item,index) in games" :key="index">{{item}}</li>
</ol>
</template>
</Category>
<Category title="游戏" > <!-- scope的另一种写法,slot-scope --> <template slot-scope="{games}"> <h4 v-for="(item,index) in games" :key="index">{{item}}</h4> </template> </Category>
Category.vue:
<!-- 使用作用域插槽,可以在app里面访问到这个组件传过去的东西 -->
<slot :games="games" msg="hello">我是默认内容</slot>
<script>
...
data(){
return{
games:['GTA5','CSGO','原','MC'],
}
}
...
</script>
总结:这三种插槽可以按照实际情况配合使用
VueX
vueX是什么
- 概念:专门在Vue中实现集中式状态(数据)管理的一个Vue组件,对vue应用中多个组件的共享状态进行集中式管理(读/写),也是一种组件间通信的方式,且适用于任意组件间的通信。
什么时候使用VueX
- 多个组件依赖于同一个状态
- 来自不同组件的行为需要变更同一个状态
工作原理
上图是VueX的 "状态管理模式":
这个状态自管理应用包含以下几个部分:
- state,驱动应用的数据源(公共数据);
- view ,以声明方式将 state 映射到视图;
- actions ,响应在 view 上的用户输入导致的状态变化(类似中间件)。
其实就是类似于后端开发中的三层结构:视图层 业务层 持久层
工作流程
- 组件请求数据,使用
dispatch('name','kano')
,请求一个变量名(key)为name,值为'kano'的函数 - dispatch函数会到Actions中,这里类似中间件,可以在这里发送后端ajax请求,进行数据处理等操作,之后Actions会执行
commit('name','kano')
到Mutations - commit函数到达Mutations中,会进行查找key为'name'对应的函数 ,在这个函数内,存放了state 中的公共数据,进行Mutate操作,也就是 修改/处理 数据
- 在Mutate操作,修改数据成功时,Vue会渲染修改后的DOM到视图中,这样就完成了修改数据的流程
注意:Actions如果不进行特殊操作,仅仅只是传递数据,可以直接在Vue Component步骤时候直接调用commit('name','kano')
就可以跳过Actions阶段
搭建VueX环境
安装VueX:
注意:vue2中需要使用VueX的3版本,Vue3中需要使用VueX的4版本
Vue2:
npm i vuex@3
Vue3:
npm i vuex
使用VueX:
创建文件: src/store/index.js
//该文件用于创建Vue中最为核心的store
//引入Vue
import Vue from 'vue'
//在这引入可以避免在use Vuex之前就创建了store造成报错
//引入Vuex
import Vuex from 'vuex'
//使用Vuex
Vue.use(Vuex);
//准备actions 用于响应组件中的动作
const actions = {}
//准备mutations 用于操作数据()
const mutations = {}
//准备state 用于存储数据
const state = {}
//准备一个getters ,用于将state中的数据进行加工
const getters = {}
//准备store
const store = new Vuex.Store({
actions,
mutations,
state,
getters
})
//导出
export default store
在main.js
中创建vm时传入store
配置项
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
//引入插件
import VueResource from 'vue-resource';
//使用插件
Vue.use(VueResource);
//引入store
import store from './store';
new Vue({
render: h => h(App),
store, //挂载为了$store
beforeCreate() {
Vue.prototype.$bus = this
},
}).$mount('#app')
组件中读取Vuex中的数据:$store.state.sum
组件中修改Vuex中的数据:$store.dispatch('action中的方法名',数据)
或者 $store.commit('action中的方法名',数据)
getters的使用
在src/store/index.js
中添加getters对象即可
.......
//准备一个getters ,用于将state中的数据进行加工
const getters = {}
const store = new Vuex.Store({
......,
getters
})
使用getters:$store.getters.属性名
四个map方法的使用
**mapState方法:**用于帮助我们映射state中的数据为计算属性
computed:{
//使用mapState,,对象解构,因为mapState返回的是一个对象,用于映射状态对象属性
//借助mapState读取state中的属性
//..mapState({sum:'sum',nickname:'nickname',age:'age'}),
//数组写法,如果需要映射的名称是一样的话
...mapState(['sum','nickname','age']),
}
mapGetters 方法 :用于帮助我们映射getters
中的数据为计算属性
computed:{
//mapGetters也是一样的,借助它读取getters里面的属性
// ...mapGetters({bigSum:'bigSum'}),
//数组写法
...mapGetters(['bigSum'])
}
mapActions方法: 用于帮助我们生成actions
对话的方法,即:包含$store.dispatch(xxx)
函数
methods:{
//靠mapActions生成:incrementOdd,incrementWait(对象形式)
...mapActions({incrementOdd:'addOdd',incrementWait:'addWait'}),
//靠mapActions生成:incrementOdd,incrementWait(数组形式),前提是方法和属性名相同
...mapActions(['addOdd','addWait'])
}
mapMutations方法: 用于帮助我们生成mutations
对话的方法,即:包含$store.commit(xxx)
函数
methods:{
//对象展开写法,这样的写法无法写参数,所以在使用的时候需要手动传入参数
//方法会调用commit去联系mutation
...mapMutations({increment:'ADD',decrement:'DECR'}),
//数组写法,方法会调用commit去联系mutation,前提是名字需要一致
...mapMutations(['ADD','DECR'])
}
备注:mapActions与Mutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象
VueX模块化与namespace
目的:让代码更好维护,让多种数据分类更加明确
修改store.js
const countAbout = {
namespaced:true,//开启明明空间
state:{x:1},
mutations:{...},
actions:{...},
getters:{
bigSum(state){
return state.sum *10
}
}
}
const personAbout = {
namespaced:true,//开启明明空间
state:{...},
mutations:{...},
actions:{...}
}
const store = new Vuex.Store({
modules:{
countAbout,
personAbout
}
})
开启命名空间后,组件中读取state数据
//方式一,直接自己读取
this.$store.state.personAbout.list
//方式二,借助mapState读取
...mapState('countAbout',['sum','name','age']);
开启命名空间后,组件中读取getters数据
//方式一,直接自己读取
this.$store.getters['personAbout/firstPersonName']
//方式二,借助mapGetters读取
...mapGetters('countAbout',['bigSum'])
开启命名空间后,组件调用dispatch
//方式一,直接自己dispatch
this.$store.dispatch('personAbout/addPersonNotWang',personObj)
//方式二,借助mapActions
...mapActions('countAbout',{incrementOdd:'addOdd',incrementWait:'addWait'}),
开启命名空间后,组件调用commit
//方式一,直接自己commit
this.$store.commit('personAbout/ADD_PERSON',personObj)
//方式二,借助mapMutations
...mapMutations('countAbout',{increment:'ADD',decrement:'DECR'})
求和案例(VueX)
代码下载: 本地下载
Vue-router
相关理解
vue-router 的理解
是一个Vue的插件库,专门用来实现SPA应用
对SPA应用的理解
- 单页 Web 应用 (single page web application,SPA)
- 整个应用只有一个完整的页面。
- 点击页面中的导航链接不会刷新 页面,只会做页面的局部刷新。
- 数据需要通过ajax请求获取。
路由的理解
- 什么是路由?
- 一个路由就是一组映射关系(key - value)
- key为路径,value可能是function或component
- 路由的分类
- 后端路由:
- 理解:value 是 function,用于处理客户端提交的请求
- 工作过程:服务器接收到一个请求时,根据请求路径 找到匹配的函数来处理请求,返回响应数据
- 前端路由:
- 理解:value 是 component,用于展示页面内容
- 工作过程:当浏览器路径改变时,对应的组件就会显示。
- 后端路由:
路由的基本使用
-
安装vue-router,命令
npm i vue-router
(vue2安装vue-router@3
) -
引用并应用插件:
vue.use(VueRouter)
-
编写router配置项(
/src/router/index.js
):// 该文件专门用于创建整个应用的路由器 import VueRouter from 'vue-router'
import About from '../components/About' import Home from '../components/Home'
//创建路由器,并共享出去 export default new VueRouter({ routes: [{ path: '/about', component: About }, { path: '/home', component: Home } ] })
-
实现切换(active-class可配置高亮样式)
<router-link to="/about" class="list-group-item" active-class="active">About </router-link></code></pre></li>
-
指定展示位置
<router-view></router-view></code></pre></li>
-
注意:
- 路由vue组件一般不放到component文件夹中,而是单独放在一个叫pages的文件夹中
- 通过切换,'隐藏 '了路由组件,默认是被销毁掉的,需要的时候再去挂载
- 每个组件都有自己的
$route
属性,里面存储着自己的路由信息 - 整个应用只有一个router,可以通过组件的
$router
属性获取到
多级路由
配置路由规则,使用children配置项:
routes: [{ //一级路由
path: '/about',
component: About
},
{ //一级路由
path: '/home',
component: Home,
children: [{
path: 'news', //子路由,不加斜杠
component: News
},
{
path: 'message', //子路由
component: Message,
children: [{
path: 'detail',
component: Detail
}]
}
]
}
]
跳转(需写完整路径)
<!-- 二级路由 -->
<router-link class="list-group-item" active-class="active" to="/home/news">News</router-link>
路由的query参数
-
传递参数
<router-link :to="{
path:'/home/message/detail', query:{ id: L.id, title: L.title } }"> {{L.title}}
</router-link></code></pre></li>
-
接收参数
$route.query.id $route.query.title
-
命名路由
-
作用:可以简化路由的跳转。
-
如何使用:
-
给路由命名:
{ path: 'message', //子路由 component: Message, children: [{ name: 'xiangqiang', path: 'detail', component: Detail }] }
-
简化跳转:
<router-link :to="<code>/home/message/detail?id=${L.id}&amp;title=${L.title}</code>">{{L.title}} </router-link> <router-link :to="{
//path:'/home/message/detail', //不用path,用设置的name name:'xiangqing', query:{ id: L.id, title: L.title } }"> 跳转
</router-link></code></pre></li>
-
路由的params参数
-
配置路由,声明params参数
{ path: 'message', //子路由 component: Message, children: [{ name: 'xiangqing', // params参数,:表示占位符 path: 'detail/:id/:title', component: Detail }] }
-
传递参数
<router-link :to="<code>/home/message/detail/${L.id}/${L.title}</code>">{{L.title}} </router-link>&nbsp;&nbsp; <router-link :to="{
//path:'/home/message/detail', //使用params的时候不可以用path,只能用name name:'xiangqing', params:{ id: L.id, title: L.title } }"> {{L.title}}
</router-link></code></pre> <p>特别注意:<strong>路由携带params参数时,若使用to的对象写法,则不能使用path配置项,需要使用name写法</strong>!</p></li>
-
接收参数:
$route.params.id $route.params.title
-
路由的props配置
作用:让路由组件更方便的接收到参数
{
path: 'message', //子路由
component: Message,
children: [{
name: 'xiangqing',
// params参数,:表示占位符
path: 'detail/:id/:title',
component: Detail,
//props的第一种写法,值为对象,该对象的所有的key-value都会以props形式传给Detail组件
// props: {
// a: 1,
// b: 'hello'
// }
//props的第二种写法,值为布尔值,若为真,就会把该路由组件收到的所有的params参数,以
//props的形式传给Detail组件 (query不行)
// props: true
//props第三种写法,值为一个回调函数,就会把该路由组件收到的所有的参数传递回去,可query也可params
props($route) {
return {
id: $route.params.id,
title: $route.params.title
}
}
}]
}
接收端只需要添加相应的props参数即可 :props:['id','title']
<router-link>
的replace属性
- 作用:控制路由跳转时操作浏览器历史记录的模式
- 浏览器的历史纪录有两种写入方式:分别为
push
和replace
,push是追加历史记录 replace是替换当前历史记录,默认为push - 如何开启replace模式:
<route-link replace .......>
即可
编程式路由导航
-
作用:不借助
<router-link>
实现路由跳转,让路由跳转更加灵活 -
具体编码:
//$router的两个API pushShow(L){ // console.log(this.$router); //手动push this.$router.push({ name:'xiangqing', params:{ id:L.id, title:L.title } }) }, replaceShow(L){ //手动replace this.$router.replace({ name:'xiangqing', params:{ id:L.id, title:L.title } }) }
前进和后退:
//后退 back(){ this.$router.back(); }, //前进 forward(){ this.$router.forward(); }, //可前可后 testgo(){ this.$router.go(-2); }
缓存路由组件
-
作用:让不展示的路由组件保持挂载,不被销毁。
-
具体编码:
<keep-alive include="News"> <router-view></router-view> </keep-alive></code></pre> <p>如果想缓存多个组件,可以写成数组形式</p> <pre><code class="language-javascript"> <keep-alive :include="['News','Message']"> <router-view></router-view> </keep-alive></code></pre></li>
两个新的生命周期钩子
作用:路由组件独有的两个钩子,用于捕获路由组件的激活状态。
具体名字:
actived
路由组件被激活时触发。deactivate
路由组件失活时触发。
active
//激活
activated(){
console.log('我被激活了');
this.timer = setInterval(() => {
this.opacity -= 0.01
console.log('@');
if(this.opacity <= 0){
this.opacity = 1
}
}, 16);
},
deactivate
//失活
deactivated(){
console.log('我失活了');
clearInterval(this.timer);
}
路由守卫
-
作用:对路由进行权限控制
-
分类:全局守卫、独享守卫、组件内守卫
-
全局守卫:
//全局前置路由守卫 //在每一次路由切换之前都会调用这个函数,初始化的时候也会调用这个函数 router.beforeEach((to, from, next) => { console.log(to, from, next); //使用元信息来匹配需要授权的路由 if (to.meta.isAuth) { // if (to.name === 'xinwen' || to.name === 'xiaoxi') { // if (to.path === '/home/news' || to.path === '/home/message') { if (localStorage.getItem('name') !== 'kano') { alert('全局前置路由守卫不允许你进入该路径') } else { next(); } } else { //放行 next(); } }); `//后置全局路由守卫 初始化的时候被调用、每次路由切换之后被调用 router.afterEach( (to, from) => { console.log('后置路由守卫', to, from); //借助后置路由守卫,切换完成后执行,就不会出现标题bug document.title = to.meta.title || 'Vue' } ) export default router;`
-
独享路由守卫(只有前置没有后置)
///独享前置路由守卫(没有后置) beforeEnter: (to, from, next) => { if (to.meta.isAuth) { if (localStorage.getItem('name') !== 'kano') { alert('独享前置路由守卫不允许你进入该路径') } else { next(); } } else { //放行 next(); } }
-
组件内守卫(千万不要理解为前置或者后置守卫,没有这种说法)
//组件路由守卫 //通过路由规则,进入该组件时被调用 beforeRouteEnter (to, from, next) { //... } //通过路由规则,离开该组件时被调用 beforeRouteLeave (to, from, next) { // ... }
路由器的两种工作模式
- 对于一个url来说,什么是hash值? ---- #及其后面的内容就是hash值
- hash值不会包含在HTTP请求中,即:hash值不会带给服务器
- hash模式:
- 地址中永远带着# 号,不美观
- 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法
- 兼容性较好
- history模式:
- 地址干净美观
- 兼容性和hash模式相比略差
- 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题
切换方法:
//创建路由器,并共享出去
const router = new VueRouter({
//选择路由工作模式(不写默认是hash)
mode: 'history',
// mode: 'hash'
//两者的区别,hash模式兼容性好一点,但是会多一个#
//history不会有多余的字符,但是兼容性稍差
.....
ElementUI
引入 Element
你可以引入整个 Element,或是根据需要仅引入部分组件。我们先介绍如何引入完整的 Element。
完整引入
在 main.js 中写入以下内容:
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
new Vue({ el: '#app', render: h => h(App) });
以上代码便完成了 Element 的引入。需要注意的是,样式文件需要单独引入。
按需引入
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component:
npm install babel-plugin-component -D
然后,将 .babelrc 修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
-
Vue.use(Button)
-
Vue.use(Select)
*/
new Vue({ el: '#app', render: h => h(App) });
Vue3
使用vite创建工程
## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev
常用 Composition API
1.setup函数
- 理解:Vue3中的一个新配置项,值为一个函数
- setup是所有Composition API(组合API)'表演的舞台'
- 组件中所用到的:数据、方法等、均要配置再setup中
- setup函数的两种返回值:
- 若返回一个对象,则对象中的属性,方法,再模板中均可以直接使用(需要注意)
- 若返回一个渲染函数:则可以自定义渲染内容。
- 注意点:
- 尽量不要与Vue2.x配置混用
- 但setup中不能访问到Vue2.x中的配置(data、methods、computed...)
- 如有重名,setup优先
- setup不是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的
- 尽量不要与Vue2.x配置混用
属性
2.ref函数
- 作用:定义一个响应式的数据
- 语法:
const xxx = ref(initValue)
- 创建一个包含响应式数据的引用对象(reference对象)
- JS中操作数据:
xxx.value
- 模板中读取数据:不需要.value,直接
{{xxx}}
即可
- 备注:
- 接受的数据可以是:基本类型、也可以是对象类型
- 基本类型的数据:响应式依然是靠
Object.defineProperty()
的get
与set
完成的 - 对象类型的数据:内部**"求助"** 了Vue3中的一个新函数---
reactive
函数
3.reactive函数
- 作用:定义一个对象类型的响应式数据(基本类型别用他,用ref函数)
- 语法:
const 代理对象 = reactive(被代理对象)
接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象) - reactive定义的响应式数据是'深层次的'
- 内部基于ES6中的Proxy实现,通过代理对象操作源对象内部数据都是响应式的
4.Vue3.0中的响应式原理
vue2.x的响应式
-
实现原理:
-
对象类型:通过
Object.defineProperty()
对属性的读取,修改和拦截(数据劫持) -
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包装)
Object.defineProperty(data,'count',{ get(){}, set(){} })
-
-
存在的问题:
- 新增属性、删除属性,界面不会更新(没有响应式)
- 直接通过下标修改数组,界面不会自动更新
Vue3.0的响应式
- 实现原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化,包括属性值的读写、属性的添加与删除等
- 通过Reflect(反射):对被代理对象中的属性进行操作。
- MDN文档中有描述Proxy和Reflect的信息,可以去看看
//模拟Vue3中实现响应式
const p = new Proxy(person, {
//target是p propname是操作的p中的属性名字
//捕获读取的动作
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`);
return Reflect.get(target, propName)
},
//捕获修改、添加的动作
set(target, propName, value) {
console.log(`有人修改了p身上的${propName}属性`)
Reflect.set(target, propName, value)
},
//捕获删除的动作
deleteProperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性`);
//删除一个对象,返回删除的状态
return Reflect.deleteProperty(target, propName)
}
});
reactive对比ref
- 从定义数据角度对比
- ref用来定义:基本类型数据
- reactive用来定义:对象(或数组)类型的数据
- 备注:ref也可以用来定义对象(或数组)类型的数据 ,它内部会自动通过
reactive
转换为代理对象
- 从原理角度对比:
- ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持) - reactive通过使用 proxy 来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据
- ref通过
- 从使用角度来对比:
- ref定义的数据:操作数据需要.value 读取数据时模板中直接读取,不需要
·value
- reactive定义的数据:操作数据与读取数据:均不要
.value
- ref定义的数据:操作数据需要.value 读取数据时模板中直接读取,不需要
setup的两个注意点
- setup执行的时机
- 在beforeCreate之前执行一次,this是undefined
- setup的参数
- props:值为对象,包含:组件外部传递过来,且组件内声明接收了的属性
- context:上下文对象
- attrs:值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性,相当于
this.$attrs
- slots:收到的插槽内容,相当于
this.$slots
- emit:分发自定义事件的函数,相当于
this.$emit
计算属性与监视
-
computed函数
-
与vue2.x中的computed配置功能一致
-
写法
import {computed} from 'vue'; //Vue3中的计算属性-简写形式,没有考虑计算属性被修改的情况 // person.fullName = computed(()=>{ // return person.firstName + person.lastName // }) //完整写法 person.fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-'); person.firstName = nameArr[0]; person.lastName = nameArr[1] } })
-
-
watch函数
-
与vue2.x中的watch配置功能一致
-
两个小坑
- 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
- 监视reactive定义的响应式数据中某个属性时:deep配置有效
//情况一:监视ref所定义的响应式数据 watch(sum,(newValue,oldValue)=>{ console.log('sum变化',newValue,oldValue); },{immediate:true})
//情况二:监视ref定义的多个响应式数据(使用数组) //如果是reactive的对象设置的属性,默认会强制开启深度监视(deep配置无效),无需设置,也不能关掉深度监视 watch([sum,msg],(newValue,oldValue)=>{ console.log('sum变化',newValue,oldValue); },{immediate:true})
//情况三:监视reactive所定义的一个响应式数据,注意,此处无法正确获取oldValue watch(person,(newValue,oldValue)=>{ console.log('person变化了'); console.log(newValue,oldValue);//你会发现这两个的值是一样的 })
//情况四:监视reactive所定义的一个响应式数据中的某一个属性 watch(()=>person.job.j1.salary,(newValue,oldValue)=>{ console.log('salary变化了'); console.log(newValue,oldValue);//你会发现这两个的值是一样的 })
//情况五:监视reactive所定义的一个响应式数据中的某些属性 watch([()=>person.job.j1.salary,()=>person.name],(newValue,oldValue)=>{ console.log('salary或者name变化了'); console.log(newValue,oldValue);//你会发现这两个的值是一样的 })
//特殊情况,由于job没有地址上的变化,只是他里面属性发生了变化,所以监视不奏效 watch(()=>person.job,(newValue,oldValue)=>{ console.log('salary变化了'); console.log(newValue,oldValue); },{deep:true})//此处由于监视的是reactive定义的对象的某个属性,deep有效
-
说白了,监视的如果是个对象里面包裹着对象,那你直接用reactive就可以直接监视到数据,如果你用的是ref,那么就加上deep或者是在监视对象后面加上value 即可正常监测数据变化
watchEffect函数
- watch的套路是:既要指明监视的属性,又要指明监视的回调
- watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性
也就是,只会监视函数内所依赖的属性,并且默认immediate为true - watchEffect比较像compured:
- 但computed注重的是计算出来的值(回调函数的返回值),所以必须要写返回值
- 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值也行
//监视函数里面使用到的数据
watchEffect(()=>{
//监视sum name salary
const x1 = sum.value
const x2 = person.name
const x3 = person.job.j1.salary
console.log('watchEffect所指定的回调执行了');
})
5.Vue3中的生命周期
6.通过组合式API调用生命周期钩子
- 在Vue3中,setup会比beforeCreate和created的优先级要高 ,所以使用组合式API的时候,setup已经替代掉这两个生命周期钩子
代码演示
setup(){
//数据
let sum = ref(0)
//通过组合式API(setup内)的形式使用生命周期钩子
//组合式API中的生命周期钩子比配置项钩子先执行
console.log('----setup----');
onBeforeMount(()=&gt;{
console.log('----onBeforeMount----');
})
onMounted(()=&gt;{
console.log('----onMounted----');
})
onBeforeUpdate(()=&gt;{
console.log('----onBeforeUpdate----');
})
onUpdated(()=&gt;{
console.log('----onUpdated----');
})
onBeforeUnmount(()=&gt;{
console.log('----onBeforeUnmount----');
})
onUnmounted(()=&gt;{
console.log('----onUnmounted----');
})
//返回一个对象
return {sum}
},
还有一个注意点是 :组合式API中的生命周期钩子比配置项钩子先执行
7.自定义hook函数
- 什么是hook? ------本值是一个函数,把setup函数中使用的CompositionAPI进行封装
- 类似vue2.x中的mixin
- 自定义hook的优势:复用代码,让setup中的逻辑更清楚易懂
8.toRef
- 作用:创建一个ref对象,其value值指向另一个对象中的某个属性值
- 语法:
const name = toRef(person,'name')
- 应用:要将响应式对象中的某个属性单独提供给外部使用时
- 扩展:
toRefs
与toRef
功能一致,但可以批量创建多个ref对象,语法:toRefs(person)
import {reactive,toRef,toRefs} from 'vue';
setup(){
//数据
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
//返回一个对象
return {
//这样做是无效的,会失去响应式,这是值传递而不是引用传递
// name : person.name
// name:toRef(person,'name'),
// age:toRef(person,'age'),
// salary:toRef(person.job.j1,'salary')
//person.job本身就是引用类型,所以不需要toRef
// job:person.job
//torefs可以给里面每一个属性变成refimpl
...toRefs(person),
...toRefs(person.job.j1)
}
}
其他Composition API
1.shallowReactive 与 shallowRef
- shallowReactive:只处理对象最外层属性的响应式(浅响应式)
- shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理
- 什么时候使用?
- 如果有一个对象数据,结构比较深,但变化时只是外层属性变化==>shallowReactive
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换==>shallowRef
// 引用
import {ref,reactive,toRefs,shallowReactive,shallowRef} from 'vue';
//数据
//浅层reactive,只处理第一层的对象的响应式
let person = shallowReactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
//shallowRef只处理基本数据类型的响应式,不进行对象响应式的计算
let x = shallowRef({
y:0
})
2.readonly 与 shallowReadonly
- readonly:让一个响应式数据变为只读的(深只读)。
- shallowReadonly: 让一个响应式数据变为只读的(浅只读)
- 应用场景:不希望数据被修改时(特别是别人传给你的)。
//person现在是只读的,readonly接收响应式数据
person = readonly(person)
//person的第一层对象的属性是只读的,job内的属性不受影响
person = shallowReadonly(person)
x = readonly(x)
3.toRaw 与 markRaw
toRaw:
- 作用:将一个由
reactive
生成的响应式对象 转为普通对象 - 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的而所有操作,不会引起页面更新
markRaw:
- 作用: 标记一个对象,使其永远不会在成为响应式对象
- 应用场景:
- 有些值不应该被设置为响应式的,例如第三方的复杂类库
- 当渲染具有不可变的数据源的大列表时,跳过响应式转换可以提高性能
// 引用
import {ref,reactive,toRefs,toRaw,markRaw} from 'vue';
//toRaw只能用在reactive创建的响应式对象上
function showRawPerson(){
console.log(toRaw(person));
//console.log(toRaw(x));//无效
}
function addCar(){
let car = {name:'马自达',price:'1'}
//添加的car是响应式的,得益于proxy
// person.car = car
//标记为markRaw的数据,即使数据更改了,vue也不会更新数据到页面
person.car = markRaw(car)
} function replaceCar(){ person.car.name = 'nmsl' person.car.price++ console.log(person.car); }
4.customRef
-
作用:创建一个自定义的ref,对其依赖跟踪和更新触发进行显式控制
-
实现防抖效果
// 引用 import {ref,customRef} from 'vue'; //自定义ref,实现防抖效果 function myRef(value,delay){ //追踪器track和触发器trigger return customRef((track,trigger)=>{ let timer return { get(){ console.log('getter',value); track()//通知Vue追踪value的变化(提前和get商量一下,让他认为这个value是有用的) return value }, set(val){ console.log('setter',val); clearTimeout(timer); timer = setTimeout(()=>{ value = val trigger()//通知vue重新解析模板 },delay) } } }) } // let keyWord = ref('hello')//使用vue提供的ref let keyWord = myRef('hello',1000)//使用自定义的ref
5.provide 与 inject
-
作用:实现祖孙组件间的通信(中间隔一个父)
-
套路:父组件有一个
provide
选项来提供数据,子组件有一个inject
选项来开始使用这些数据 -
具体写法:
- 1.祖组件中:
import { reactive,toRefs,provide} from 'vue' setup(){ let car = reactive({ name:'马自达', price:114514, }) provide('car',car)//给自己的后代组件传递数据 return { ...toRefs(car) } }
- 2.孙组件中
import {inject} from 'vue' setup(){ let car = inject('car')//类似全局事件总线 return { car } }
6.响应式数据的判断
- isRef: 检查一个值是否为一个ref对象
- isReactive:检查一个对象是否是由
reactive
创建的响应式代理 - isReadonly:检查一个对象是否是由
readonly
创建的只读代理 - isProxy:检查一个对象是否是由
reactive
或者readonly
四、Compostion API的优势
Options API (Vue2)存在的问题
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
| | | |-------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
Composition API (Vue3)的优势
我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
| | | |-------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
五、新的组件
Fragment
- 在Vue2中: 组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
Teleport
- 什么是Teleport?
teleport
是一种能够将我们的 组件html结构 移动到指定位置的技术
<teleport to="移动位置">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
Suspense
-
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
-
使用步骤:
-
异步引入组件
import {defineAsyncComponent} from 'vue' const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
-
使用
Suspense
包裹组件,并配置好default
与fallback
<template> <div class="app"> <h3>我是App组件</h3> <Suspense> <template v-slot:default> <Child/> </template> <template v-slot:fallback> <h3>加载中.....</h3> </template> </Suspense> </div> </template>
六、其他
1.全局API的转移
-
-
Vue 2.x 有许多全局 API 和配置。
-
例如:注册全局组件、注册全局指令等。
//注册全局组件 Vue.component('MyButton', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>' }) //注册全局指令 Vue.directive('focus', { inserted: el => el.focus() }
-
-
Vue3.0中对这些API做出了调整:
- 将全局的API,即:
Vue.xxx`````````调整到应用实例(`````````app
)上
| 2.x 全局 API(``````Vue``) | 3.x 实例 API (
app
) | |----------------------------------------------|-----------------------------| | Vue.config.xxxx | app.config.xxxx | | Vue.config.productionTip | 移除 | | Vue.component | app.component | | Vue.directive | app.directive | | Vue.mixin | app.mixin | | Vue.use | app.use | | Vue.prototype | app.config.globalProperties | - 将全局的API,即:
2.其他改变
-
data选项应始终被声明为一个函数。
-
过度类名的更改:
- Vue2.x写法
.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }
- Vue3.x写法
.v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; }
-
移除 keyCode作为 v-on 的修饰符,同时也不再支持
config.keyCodes
-
移除
v-on.native
修饰符-
父组件中绑定事件
<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" />
-
子组件中声明自定义事件
<script> export default { emits: ['close'] } </script>
-
-
移除过滤器(filter)
过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 "只是 JavaScript" 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。