vuedemo20230801
Project setup
npm install
Compiles and hot-reloads for developments
npm run serve
Compiles and minifies for production
npm run build
Lints and fixes files
npm run lint
Customize configuration
See Configuration Reference.
Vue 是用来构建用户界面的渐进式框架
渐进式:如果应用很简单,就引用一个轻量小巧的核心库
如果需要更多,则可以引用很多复杂的Vue第三方库
允许用户从简单向复杂按需引用
Vue 特点
组件化:提高代码复用率,跟容易维护
申明式编码:让编码人员无需直接操作DOM,提高开发效率
Vue 文档
API,相当于一个字典,有问题查询API
Awesome Vue 万能的Vue,官方给出的好用的第三方库
网站:awesomejs.dev/for/vue
引入vue
<script type="text/javascript" src="../js/vue.js"></script>
引入Jquery,代码里可以通过$或者jQuery调用封装方法
引入Vue,代码里就多了一个Vue 构造函数
开发建议浏览器安装DEV tools: vue.js.devtools
VSCode 安装 LiveServer 辅助开发
Vue3 代码插件推荐
Vue 3 Snippets
=======================================================
1.想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
2.root 容器里的代码依然符合html规范,只不过混入了一些特殊的vue语法
3.root 容器里的代码被称为 Vue模板
4.Vue实例和容器是一一对应的关系
5.真是开发中只有一个Vue实例,并且会配合着组件一起使用
6.{{XXX}} 插值模板中的XXX 是js 表达式,XXX 会自动读取到data中对应的属性
7.一旦data的数据发生变化,那么模板中用到该数据的地方也会自动更新
注意区分 js 表达式 和 js 语句
- 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
(1)变量a
(2)运算表达式,a + b
(3)方法表达式,demo(1)
(4)三元表达式
... - js 代码(语句)
(1)if() {}
(2)for() {}
code 示例
<div id="root"></div>
new Vue({
el: "#root",
data: {
name: 'tom'
}
})
===============================================================
<a v-bind:href="url">链接地址</a>
把Vue对象data里的url值绑定到 a 标签上,v-bind简写方式为:
<a :href="url">链接地址</a>
<a v-bind:href="url">链接地址</a>
插值语法一般用于标签体里面的内容的绑定
指令用于解析标签属性
v-bind 一般用于标签属性的绑定,简写为:
v-model 双向数据绑定,但是v-model只能应用于输入元素(元素有value标签)
简写为v-model
=================================================================
const v = new Vue({
el: "#root",
data: {
name: "tome"
}
cosnole.log(v); 查看v对象
})
v对象原型有个mount('#root'),此写法跟new vue 是传入el: "#root" 效果一样
但是v.$mount 更灵活,比如设置定时器,在页面加载一秒钟以后再绑定模板
mount 是挂载的意思
data绑定还可以用函数时
data: function() {
return{
name: "tom"
}
}
data() {
return{
name: "tom"
}
}
data函数必须是普通函数,不支持箭头函数,如果使用箭头函数,调用this不再是Vue实例,而是window
箭头函数没有自己的this,只能往外找,找到window对象
==================================================================================
MVVM模型
M: 对象Vue对象data里的数据,
V: 页面模板
VM: Vue实例对象
Vue实例对象通过Dom Listeners 监听页面数据变化绑定到模型
通过Data Bindings 把data 数据绑定到页面模板
data 中所有的属性最终都会出现在vm身上
vm 身上的所有属性以及vm原型的所有属性,都可以在模板里直接使用
=================================================================================
Object.defineproperty方法
let person = {
name: "tom"
}
给person 添加age属性
Object.defineproperty(person, 'age', {
value: 18
})
通过Object.keys(person) 遍历属性名只能拿到 name 属性,通过 Object.defineproperty 添加的属性age遍历不到
想要age 可遍历(或者叫可枚举),用下面这种写法
Object.defineproperty(person, 'age', {
value: 18,
enumerable: true, // 控制属性是否可枚举,默认false
writable: true, // 控制属性是否可以被修改,默认false
configurable: true, // 控制属性是否可以被删除(通过 delete person.age 来删除属性),默认false
get() { // 当有人读取age 属性的值的时候,get 属性就会被调用,且返回值就是age 的值
},
set(value) { // 当有人修改age 值的时候会调用set方法,且回收厚道修改的value值
}
})
数据代理技术
通过一个对象代理另一个对象的读写操作
let obj1 = {x: 100}
let obj2 = {y: 200}
让obj2 代理obj1
Object.defineproperty(obj2, 'x', {
get() {
return obj1.x;
},
set(value) {
obj1.x = value;
}
})
vm._data == vue options 里传的data
==============================================================
- Vue中的数据代理
通过vm对象来代理data对象中的属性操作 - Vue中的数据代理好处
更加方便的操作data中的数据 - 基本原理
通过Object.defineproperty() 把data对象中的属性加到vm上
为每一个添加到vm上的属性都指定 一个 getter/setter
在getter/setter内部读写 data对应的属性
============================================================
v-on:click 点击事件 简写 @click
如果方法想要得到event, 方法调用必须加上$event 占位符
<button v-on:click="showInfo1"></button> // 不传参
<button v-on:click="showInfo2(66, $event)"></button> // 传参
showInfo方法 不能用箭头函数,否则this对象就会执行window
const vm = new Vue({
el: "#root",
data: {
name: "tom"
},
methods: {
showInfo1(event){
// console.log(event.target.innertext);
// console.log(this); // 此处的this是vm
// alert('同学你好');
},
showInfo2(number, event){
// console.log(event.target.innertext);
// console.log(this); // 此处的this是vm
// alert('同学你好');
}
}
})
=======================================================
a标签, 接收event对象,用 e.preventDefault() 阻止原有事件
<a href="http://www.baidu.com" @click="showInfo">点击</a>
showInfo(e) {
e.preventDefault();
alert("Hello");
}
也可以这么写
<a href="http://www.baidu.com" @click.prevent="showInfo">点击</a>
showInfo 方法里就不用写了
还有其他事件修饰符
@Click.prevent 阻止默认事件,点击a标签,链接就不会跳走
@Click.stop 阻止事件冒泡,事件不会被父元素捕获
@Click.once 事件只出发一次,点第一次弹窗,再点无效
@Click.capture 在之间捕获的时候处理,先捕获,再冒泡
@Click.self 只有当e.event.target 是当前元素的时候才触发事件,也能用于实现阻止冒泡事件
@Click.passive 事件的默认行为立即执行,无需等待回调执行
比如@Wheel 出发回调,回调执行很复杂允许,如果不加paasive,则也买你会出现卡顿(等回调执行完,滚动条才会滚动),加了passive 即使回调有复杂运算,也会立即滚动滚动条,同时复杂计算慢慢算去
小技巧:既阻止冒泡,又阻止默认事件,可以这么写
@Click.stop.prevent
滚动事件有两种写法
@Scroll
滚动条滚动事件,当滚动条滚到头就不再出发事件
@Wheel
即使滚动条滚到头,只要滚轮继续滚动,还能出发事件
==================================================================
键盘事件:
@keydown 按下去立即出发回调
@keyup 按下去松手再出发回调
<input type="text" @keyup="showInfo">
showInfo(e) {
// 通过e.keyCode 可以获取键盘编码
if(e.keyCode == "13") {
console.log("回车了");
}
}
上面捕获回车事件还可以这么些,不用再回调里判断keyCode
<input type="text" @keyup.enter="showInfo">
showInfo(e) {
console.log("回车了");
}
常用按键别名有enter delete(退格和删除) esc space tab up down left ritht
监听tab键更适合用keydown去监听,因为tab键会切换焦点,当keyup 的时候焦点已经切换到了别的元素
还有几个系统按键也应当使用keydown,ctrl,alt, shift, meta
举例:如果ctrl 使用keyup,如果按了Contrl C 那么等C键抬起来才会触发事件
不推荐使用按键编号 (如:@keyup.13) 去监听回车事件,因为有的键盘事件编号不统一,将来web协议会废弃,不支持
Vue 支持自己定义按键别名,比如定义回车:
Vue.config.keyCodes.huiche = 13;
小技巧:监听Contrl + y 事件可以这么写 ====> @keydown.ctrl.y
Vue 计算属性:用data里的属性去计算加工,产生一个新的属性叫做计算属性
插值语法调用方法必须要带小括号,表示调用方法,否则就会把方法本身打印到界面
事件语法里可以省略小括号
data 里的数据发生变化,模板要重新解析
所以某个方法A使用了data里的值,并且该方法A在插值语法里有调用,每次data数据发生变化,方法A必然会被调用
computed: {
fullName {
// 当有人读取fullname 时,get属性就会被调用,底层实现用的就是 Object.defineproperty里的getter
// get 方法调用时机
// 1. 初次读取fullname
// 2. 所依赖属性发生变化时
get() {
return this.FirstName + '-' + this.LastName; // 此处 this指向vm
},
// 修改fullname时候会被调用,如果fullname 确定不会被修改,就不用写set
set(value) {
...
}
}
// 简写形式
fullName() {
return this.FirstName + '-' + this.LastName;
}
}
fullName 也会绑定到vm对象上
假如界面上同时读取四次 fullName,fullName getter 方法也只会执行一次,fullname 值Vue 会帮我们缓存
计算属性比Method实现的优势
- 计算属性多次使用fullName 之会调用一次get方法,而method 执行多少次就调用多少次
======================================================================================
@XXX绑定事件的时候,事件后面可以写简单语句去执行
@XXX="YYY" YYY 可以是简单语句比如: isHot = !isHot; i ++
监视属性
watch: {
// 计算属性也可以被监视
isHot: {
// 初始化时rang handler调用一些
immediate: true,
// 当被监听属性isHot发生变化时,handler会被调用
handler(newvalue, oldvalue) {
}
}
}
也可以在Vue 对象外面监视属性
vm.$watch('isHot', {
immediate: true,
handler(newvalue, oldvalue) {
}
});
// 深度监测
data: {
numbers: {
a: 1,
b: 2
}
},
// 监测多级属性 numbers 下面的a属性
watch: {
'numbers.a': {
// 初始化时rang handler调用一些
immediate: true,
// 当被监听属性isHot发生变化时,handler会被调用
handler(newvalue, oldvalue) {
}
}
}
假如想监测整个numbers的变化,直接再watch写numbers: {...}是不起作用的,
因为这种情况下vue监测的是numbers对象的地址,除非numbers 重新被复制
正确的写法是开启deep属性
watch: {
numbers: {
// 当numbers下面任何属性发生变化都能监测到
deep: true,
// 当被监听属性isHot发生变化时,handler会被调用
handler(newvalue, oldvalue) {
}
}
}
如果不考虑deep 和 immediate 配置,watch 属性可以简写为
watch: {
isHot(newvalue, oldvalue) {
console.log(newvalue, oldvalue);
}
}
vm.watch('isHot', function(newvalue, oldvalue) {
console.log(newvalue, oldvalue);
})
计算属性靠返回值完成任务,没法使用setTimeout,因为setTimeout 是把结果返回给了异步方法,而不是返回给了计算属性
Watch 可以完美的实现异步任务
setTimeout(() => {
console.log('123');
}, 1000)
定时器所指定的回调是由浏览器定时器管理模块控制的,到点了由JS引擎调用的,不是由Vue管理的
computed能完成的watch 一定能完成,
watch 能完成的computed不一定能完成,例如:watch 可以进行异步操作
所有被Vue管理的函数最好写成普通函数,这样的话this的指向才是vm
所有不被Vue管理的函数(定时器回调函数,ajax函数,promise的回调函数),最好写成箭头函数
这样this的指向才是vm,可以访问vue属性
比如定时器是由window 调用的,如果申明成普通函数,this一定指向window
如果是箭头函数,则没有指定this,this向外寻找,找到了vm对象
=============================================================
vue style绑定class样式
适用于样式的类名不确定,需要动态绑定
不变的样式还是跟在html里面一样
变化的样式用:class来绑定,vue 会把变化和不变的样式组合到一起生效
绑定字符串,场景:类名不确定,需要动态指定
<div class="basic" :class="mod" @Click="changeMod"></div>
绑定数组, 多个类组合到一起生效,场景:类名不确定,样式也不确定
<div class="basic" :class="arr" @Click="changeMod"></div>
绑定对象, 应用样式atguigu1, atguigu3,atguigu2 是false,所以不生效
场景:类名确定,应用哪个需要动态决定
<div class="basic" :class="classObj" @Click="changeMod"></div>
直接绑定内联样式
<div class="basic" :style="styleObj" @Click="changeMod"></div>
数组形式绑定内联样式
<div class="basic" :style="[styleObj, styleObj2]" @Click="changeMod"></div>
new Vue({
data: {
mod: "normal",
arr: ["atguigu1", "atguigu2", "atguigu3"],
classObj: {
atguigu1: true,
atguigu2: false,
atguigu3: true
},
styleObj: {
fontSize: '40px' // 所有有中划线的css属性名都要去掉中划线,使用驼峰命名
},
styleObj2: {
color: 'red';
}
},
methods: {
changeMod(){
this.mod = "happy"
}
}
})
================================================================
v-show 是通过display:none 来隐藏元素
后面的表达式不一定是true或false,能够解析为布尔值的表达式都可以
<div v-show="true">hello</div>
v-if 值为false直接会删除对应的dom节点,
同时还有v-else-if 和 v-else,跟js 里的if else 逻辑一模一样
要使用v-else-if 和 v-else,所有dom元素必须是连续的,中间不能打断插入别的元素
<div v-if="true">hello</div>
如果dom切换频率很高,则使用v-show,动态控制隐藏和显示,效率更高
想要同时控制多个元素的隐藏和显示可以使用 template 元素
template 元素渲染的时候会消失,不影响原来dom,但是template只能配合v-if 使用
<template v-if="true">
<h1>Angular</h1>
<h1>React</h1>
<h1>Vue</h1>
</template>
=======================================================
v-for
// 遍历数组
<ul>
// key 也可以绑定index,只要在ul里不重复就行
<li v-for="(person, index) in persons" :key="person.id">
{{p.name}} - {{p.age}}
</li>
</ul>
// 遍历对象
<ul>
// v 是属性值,p是属性名称
<li v-for="(v, p) in car" :key="p">
{{v}} - {{p}}
</li>
</ul>
// 遍历字符串
<ul>
// c是字符,index是下标
<li v-for="(c, index) in car" :key="index">
{{c}} - {{index}}
</li>
</ul>
// 遍历指定次数
<ul>
// i 是1,2,3,4,5 index 是下标,0,1,2,3,4
<li v-for="(i, index) of 5" :key="index">
{{i}} - {{index}}
</li>
</ul>
new Vue({
data: {
persons: [
{id: '001', name: '张三', age: 18}
{id: '002', name: '张五', age: 19}
{id: '003', name: '张六', age: 20}
],
car: {
name: "Audi A8",
price: "18 W",
color: "黑色"
},
str: "hello"
},
methods: {
}
})
=======================================================================
v-for key 的原理
// 遍历数组
<ul>
// key 也可以绑定index,只要在ul里不重复就行
<li v-for="(person, index) in persons" :key="person.id">
{{p.name}} - {{p.age}}
</li>
</ul>
new Vue({
data: {
persons: [
{id: '001', name: '张三', age: 18},
{id: '002', name: '张五', age: 19},
{id: '003', name: '张六', age: 20}
]
}
})
react-vue 循环渲染,key的作用
虚拟dom的作用,key是dom的标识,当数据发生变化时,vue 会根据新数据生成新的虚拟dom
新的虚拟dom和旧的虚拟dom的差异对比,比较规则如下:
- 如果旧的虚拟dom中找到了与新的虚拟dom相同的key
如果虚拟dom内容没有变,则直接使用之前的真实dom
如果虚拟dom内容变了,则生成新的真实dom,随后替换掉页面上之前的真实dom - 如果旧的dom中没有找到与新的虚拟dom相同的key
根据新的虚拟dom直接创建真实dom - 如果旧的虚拟dom中的key在新的虚拟dom中不存在
删除旧的虚拟dom对应的真实dom
用index下标作为key可能引发的问题:
- 如果对数据进行逆序添加,逆序删除等操作
会产生没有必要的真实dom更新,界面没有问题,效率低下
一旦结构中有输入类dom,会产生错误的dom更新,界面会有问题
如果使用v-for没有指定key,vue默认会把下标作为key
开发中选择key时,最好使用真实数据的唯一标识:比如数据主键
如果紧紧是对数据的渲染,不会逆序删除,添加数据,则使用 index 没有问题
=====================================================================
列表的过滤:
<ul>
<li v-for="(person, index) in filPersons" :key="person.id">
{{p.name}} - {{p.age}} - {{p.gender}}
</li>
</ul>
new Vue({
data: {
keyWord: ''
persons: [
{id: '001', name: '马冬梅', age: 18, gender: "女"},
{id: '002', name: '周冬雨', age: 19, gender: "女"},
{id: '003', name: '周杰伦', age: 20, gender: "男"},
{id: '003', name: '温兆伦', age: 20, gender: "男"}
]
},
computed: {
filPersons() {
return this.persons.filter(p => {
return p.name.indexOf(this.keyword) != -1;
});
}
}
})
=====================================================================
Vue 修改数据信息但是不会更新到界面的Demo
<ul>
<li v-for="(person, index) in persons" :key="person.id">
{{p.name}} - {{p.age}} - {{p.gender}}
</li>
</ul>
<button @Click="updateMei">更新第一条数据</button>
const vm = new Vue({
data: {
keyWord: ''
persons: [
{id: '001', name: '马冬梅', age: 18, gender: "女"},
{id: '002', name: '周冬雨', age: 19, gender: "女"},
{id: '003', name: '周杰伦', age: 20, gender: "男"},
{id: '003', name: '温兆伦', age: 20, gender: "男"}
]
},
methods: {
updateMei() {
// 下面修改方法会更新到页面,没有问题
this.persons[0].Name = "马老师";
this.persons[0].age = "34";
this.persons[0].gender = "男";
// 下面修改方法不生效,有问题
this.persons[0] = { name = "马老师", age = 34, gender = "男" }
}
}
})
Vue 监测数据改变原理分析
- 我们传入的data 会保存到 vm._data属性下面,具体流程如下:
加工传入的data,给每个属性添加 gettter/setter
加工后的data 赋值给vm._data
加工之后有啥用?
比如修改data 的name属性,就会引起set_name方法的调用
set_name 调用就会重新解析模板生成新的虚拟dom,虚拟dom对比,更新真实dom
==========================================================
简单模拟Vue监测data对象改变:
let data = {
name: "马老师"
}
// 创建一个监视的实例对象, 用于监视data中属性的变化
// Vue其实就是在setter方法里添加了动态更新页面的操作
const obs = new Observer(data);
let vm = {}
vm._data = data = obs
function Observer(obj) {
const keys = Object.keys(obj);
keys.foreach((k) => {
Object.defineproperty(this, k, {
get(){
return obj[key]
},
set(val) {
// 此处可以添加动态更新页面的操作
console.log(k + "被修改了");
obj[k] = val
}
})
})
}
明白了上面代码,假如在代码运行中新添加一个属性,那么这个属性是不是就没有被加工,没有添加getter/setter?答案是肯定的,
既然新添加属性没有被添加getter/setter,那么新添加属性改变就不会被更新到Dom上
那如果我就像在代码运行中给data 添加属性并且动态绑定到页面要咋整呢?
别急,Vue提供了专门的方法来干这事儿
Vue.set(vm._data.student, 'sex', '男') //
还可以写成vm.student,因为vm.student 这个方法就是通过数据代理绑定了vm._data.Student
这么写页面就能监测到属性变化了
还有另一个方法,vm对象的set(vm._data.student, 'sex', '男')
注意!!!
在vue里面调用set 可以直接写
Vue.set(this.student, 'sex', '男')
或 Vue. $set(this.student, 'sex', '男')
注意2!!!
Vue 的set方法只能给data下面的某一个对象添加属性,不能直接给data添加属性
==================================================
Vue 的数组中的元素是没有添加getter/setter 来监视数组元素变化的
但是Vue 会监测以下方法调用,当以下方法调用了,vue认为你的数组就变了,并更新到页面
push最后面新增,
pop删除最后一个元素,
shift删除第一个,
unshift在前面加一个,
splice指定位置插入/删除/替换元素,
sort,
reverse反转
Vue是如何监测这些方法的呢,很简单,Vue 重写了这些方法,你在vue里面调用的push等这些方法其实是vue自己实现的
既然是自己实现的,那还不是想咋写就咋写
另一种实现方式是使用Vue.set, Vue 的set方法也可以操作数组
Vue.set(this.student.hobby, 1, '捣蛋')
Vue set方法操作了student 的hobby数组中下标为1的元素
直接替换整个数组也是可以的,因为数组属性也是普通属性,有getter/setter
!!! 数据劫持啥意思?
就是修改data中的某个属性,修改的动作会被setter 劫持到
================================================================
from 表单
假如想让点击label的时候,label后面的输入组件获得标签,
那就要通过for属性把labe和输入组件做绑定,操作如下
<label for="demo">账号:</label>
<input type="text" id="demo">
双向绑定radio 单选按钮的时候,radio 元素一定要设置value值,不然属性拿不到值
双向绑定CheckBox的时候默认绑定checked属性拿到true/false,
如果要绑定checkbox具体的值,必须指定value属性value
!!!绑定元素的初始值能够影响绑定model
假如绑定model一开始是bool值,后面只能接收check属性
如果绑定model一开始是数组,那么就会把勾选的value值加到数组里
表单提交绑定事件可以用 @submit来绑定,用prevent阻止默认事件
<form @submit.prevent="submit">
...
</form>
双向绑定接收数据默认是字符串,想要接收进来的数据是数字可以加.number,这么写,type=number 和 v-model.number 一般一起使用
年龄:<input type="number" v-model.number="age">
v-model.trim 可以接收输入时去掉前后空格
v-model.lazy 不是实时收集输入,等输入控件失去焦点的时候,才会更新绑定model
vue 过滤器
日期处理第三方包:moment.js, day.js
var fromatRes = dayjs(new Date()).format('yyyy-mm-dd');
1690780406197
// 无参过滤器
<h3>现在是:{{time | timeFormater}}</h3>
// 有参过滤器
<h3>现在是:{{time | timeFormater2('YYYY-MM-DD')}}</h3>
// 多个过滤器写法,先格式化日期,再截取前四位
<h3>现在是:{{time | timeFormater2('YYYY-MM-DD') | mySlice}}</h3>
// 动态绑定属性也可使使用过滤器
<h3 :x="msg | mySlice"></h3>
const vm = new Vue({
data: {
time: 1690780406197,
msg: '大哥你好, 哈哈哈'
},
filters: {
timeFormater(val) {
return dayjs(val).format('YYYY-MM-DD HH:mm:ss')
},
timeFormater2(val, str){
return dayjs(val).format(str)
},
mySlice(val) {
return val.slice(0, 4);
}
}
})
过滤器的功能也可以通过计算属性computed和methods来实现
全局过滤器实现,直接把过滤器注册到Vue全局,所有Vue组件都能调用
Vue.filter('mySlice', function(value){
return value.slice(0, 4);
})
过滤器只支持插值语法和属性绑定
v-text指令,向其所在标签插入text文本
v-html指令,支持html解析
<h3 v-text="title"></h3> // 输入任何值以文本形式展示到节点
<h3 v-html="htmlStr"></h3> // 会把html 解析到页面
const vm = new Vue({
data: {
title: '大哥你好, 哈哈哈',
htmlStr: '<h1>哈哈</h1>'
}
})
cookie 加上http only 属性,那么这个cookie 只能被http协议拿到,没法通过document.getCookie() 方法拿到
v-html 在页面直接渲染html 有可能会导致XSS攻击
一定在可信的内容渲染上使用v-html
用户输入的内容一定不要直接使用v-html 来渲染
=============================================================
js 阻塞
js 头文件如果加载延迟,那么后面的内容渲染,js代码指向都会被阻塞
直到js 头文件加载完成
如果把引入的js脚本放在body最后面去加载,则不会阻塞
v-cloak 标签用途
当网速过慢的时候,未经过解析的Vue模板会被展示到页面上
比如直接展示:{{name}} 这种插值语法
如果不想让页面闪一下,那么就使用v-clock
<style>
[v-cloak] {
display: none;
}
</style>
<h1 v-cloak>{{name}}</h1>
vue 执行之前由于插值元素加了v-cloak,所以会被display:none隐藏掉元素,
啥也不展示,当vue创建实例接管容器后,Vue会删除所有v-clock属性,一切有Vue接管,恢复正常
=================================================
v-once 指令
v-once所在的节点在初次动态渲染之后,就会被视为静态内容了
以后绑定之变后,不会再引起v-once所在节点的更新
<h2 v-once>N的初始值:{{n}}</h2>
<h2 >N的当前值:{{n}}</h2>
========================================================
v-pre指令
跳过所在节点的编译过程,
可以利用它跳过没有指令语法,没有使用插值语法的节点,加快编译
<button v-pre @Click="n++">点我n+1</button>
<h2 v-pre>{{name}}</h2>
=====================================================
Vue自定义属性
自己写一个v-big指令,
会把绑定的数值放大10 倍
自己写一个v-fbind,
和v-bind功能类似,到那时可以让所绑定的输入控件获得焦点
<div id="root">
<h2>当前值为:<span v-text="n"></span></h2>
<h2>放大十倍的值为:<span v-big="n"></span></h2>
</div>
const vm = new Vue({
el: "#root",
data: {
n: 10
},
directives: {
// big 函数调用时机:
// 指令与元素被成功绑定时调用一次(一上来就调用);
// 指令所在的模板被重新解析时调用
// element是真实dom元素
// binding是绑定对象
big(element, binding) {
console.log(this); // 此处的this是window
element.innerText = binding.value * 10;
},
// fbind 错误实现方式
fbindError(element, binding) {
element.value = binding.value;
element.focus();
// 上面的focus 会在页面打开时自动把焦点放到绑定元素上面吗???
// 答案是不会的,因为element是属于Vue模板,Vue模板经过解析后才会被真正渲染到页面上
// 当第一次打开页面,元素成功绑定意味着再内存里建立了指令与元素的绑定关系,整个模板还未被渲染,此时调用focus当然无效
},
// fbind 正确实现方式,不能简写,只能采用完整的指令申明方法
fbind:{
// 调用顺序:bind>inserted>update
// bind就是上面简写形式的方法
bind(element, binding){
element.value = binding.value;
},
// inserted 所在元素在被插入页面时调用
inserted(element, binding){
element.focus();
},
//指令所在模板在被重新解析时调用
update(element, binding){
}
}
}
})
总结:指令语法就是用于解析标签(包括:标签属性,标签内容,绑定事件...)
简写语法相当于同时写了:bind和update,没有写inserted
指令名称的坑,指令名称多个单词用中划线分隔,不能使用驼峰命名
指令定义则么写
'bind-number': function(element, binding){
}
定义指令的方法里的this都是指向window
定义全局指令:
Vue.directive('fbind', {
// 调用顺序:bind>inserted>update
// bind就是上面简写形式的方法
bind(element, binding){
element.value = binding.value;
},
// inserted 所在元素在被插入页面时调用
inserted(element, binding){
element.focus();
},
//指令所在模板在被重新解析时调用
update(element, binding){
}
});
===============================================================
vue 生命周期
- 生命周期回调函数是Vue在关键的时刻帮我们调用的一些特殊名称的函数
- 生命周期函数的名字不可更改,但是函数内容可以根据具体需求编写
- 生命周期函数中的this指向是vm,或 组件实例对象
--veu对象创建,先会去初始化生命周期,事件等,但是数据代理还没开始
===beforeCreate() {} 钩子函数
此时还不能访问methods 和 data,因为数据代理还没开始
--此阶段进行数据监测,和数据代理
===created() {} 钩子函数
此时可以通过vm访问到methods 和 data,因为数据代理已经完成
--此阶段Vue开始解析模板,生成虚拟DOM,页面还不能显示解析好的内容
===beforeMount() {}
页面呈现的是未经Vue编译的DOM结构 所有对DOM的操作都不奏效!!!(不奏效的原因是因为下一步插入真实DOM 会覆盖掉现在的所有操作)
--此阶段将内存中的虚拟DOM转为真实DOM插入页面,Vue会把虚拟DOM往Vue对象$el 属性上存一份
===mounted() {} 钩子函数
此时,页面中呈现的都是经过Vue编译的DOM
对DOM的操作均有效,直至此初始化过程结束,一般在此时进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件等初始化操作
===beforeUpdate() {} 钩子函数
此时,数据是新的,但是页面是旧的,即:页面尚未和数据保持同步
--此阶段会根据新数据生成新的虚拟DOM,并且比较新旧虚拟DOM,并更新真实DOM,完成了Model -> View 的更新
===updated() {} 钩子函数
此时数据是新的,页面也是新的,页面和数据保持同步
===beforeDestory() {} 钩子函数
此时vm中的data,methods,指令等等都处于可用状态,即将要执行销毁操作
一般在这时候:关闭定时器,取消订阅消息,解绑自定义事件等等
在beforeDestory 再去操作数据,数据不会被更新到界面上,因为到了这个阶段,不会再走到 beforeUpdate 和 updated 方法里面去了
===destoryed() {} 钩子函数
没啥屌用
=====================================================================
Vue中清理定时器,定时器的Id不知道往哪儿放???放到外面有点不合适,其实可以放在vm对象上
比如在mounted上开启一个定时器:
mounted(){
console.log('mounted');
this.timerId = setInterval(() => {
this.n ++;
}, 16)
}
销毁的是就可以这么清理
beforeDestory() {
clearInterval(this.timerId);
}
======================================================================
Vue 组件
定义Vue组件不能写 el 配置项,因为最终所有的组件都要被一个vm管理,由VM决定服务于哪个模板
定义Vue组件时,data一定要写成函数式,返回一个data对象
为什么一定要这么写?
直接定义成data对象,那么多个地方引用组件时他们就共享同一份data,互相影响
写成函数式则不一样,每次被引用都是返回一个新的data对象,互相直接没有关联
使用组件拢共分三步:
- 创建组件
- 注册组件
- 调用组件
创建一个非单文件schoo组件
const school = Vue.extend({
template: <div> <h3>学习名称:{{schoolName}}</h3> <h3>地址:{{address}}</h3> </div>
,
data() {
return {
schoolName: "英才",
address: "定西市安定区"
}
}
})
创建非单文件student组件
const student = Vue.extend({
template: <div> <h3>学生名称:{{name}}</h3> <h3>年龄:{{age}}</h3> </div>
,
data() {
return {
name: "张三",
age: 18
}
}
})
<body>
<div id="root">
<h1>{{message}}</h1>
// 使用组件
<xuexiao></xuexiao>
<xuesheng></xuesheng>
</div>
</body>
new Vue({
el: "#root",
data: {
message: "hello"
},
// 注册组件
components: {
xuexiao: school,
xuesheng: student
}
})
全局注册组件,所有vm都可以使用
Vue.component("xuexiao", school)
Vue.component("xuesheng", student)
=======================================================
组件名称命名:多个单词,用中划线'my-school'
不要用html已经使用的元素名,如h2,div
可以在name属性指定组件在开发者工具中显示的名字
const student = Vue.extend({
name: '[name-in-dev-tools]'
})
简写 可以省略Vue.extend()
const student = {
name: '[name-in-dev-tools]'
}
Vue 自己会调用Vue.extend()
====================================================
嵌套组件
// 定义school的子组件student
const student = Vue.extend({
template: <div> <h3>学生名称:{{name}}</h3> <h3>年龄:{{age}}</h3> </div>
,
data() {
return {
name: "张三",
age: 18
}
}
})
const school = Vue.extend({
template: <div> <h3>学习名称:{{schoolName}}</h3> <h3>地址:{{address}}</h3> <student></student> </div>
,
data() {
return {
schoolName: "英才",
address: "定西市安定区"
}
},
components: {
student: student // 把student注册给school,成为school组件的子组件
}
})
创建非单文件student组件
<body>
<div id="root">
<h1>{{message}}</h1>
// 使用schoo组件,组件内部还有个student组件
<xuexiao></xuexiao>
</div>
</body>
new Vue({
el: "#root",
data: {
message: "hello"
},
// 注册组件
components: {
xuexiao: school
}
})
=========================================================
VueComponent
- school 组件本质是一个VueComponent的构造函数,不是程序员定义的,是由Vue.extend 生成的
- 我们只要写<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的实例对象,简称cm
vm 可以传入el,vc不行
function demo(){
this.a = 1
this.b = 2
}
const d = new Demo()
demo身上有个属性叫 prototype 显式原型属性
demo.prototype
d 身上有个属性叫 proto (隐式原型属性)
d.proto
demo.prototype 和 d.proto 都指向同一个原型对象
// 程序员通过显示原型属性,给demo追加一个x属性
demo.prototype.x = 99
d.proto.x 就能拿到值,这么写也能拿到d.x
=================================================
一个重要的内置关系,VueComponent.prototype.proto === Vue.prototype
为什么有这个关系:让组件实例对象vc可以访问Vue原型上的属性和方法
比如:Vue.prototype.x = 99
Vue下的组件school 也可以访问到x,vc.x
===================================================
单文件组件 以.vue结尾
.vue 文件可以被vue脚手架编译成js文件
文件结构
定义School组件
<template>
<div>
<h2>学习名称:{{schoolName}}</h2>
</div>
</template>
<script>
// 把school对象暴露出去,让别的地方可以调用
export default Vue.extend({
name: "School"
});
// 简写形式
export default {
name: "School"
};
</script>
<style>
.demo {
background-color: red;
}
</style>
定义App.Vue 组件领导所有组件
<template>
<div>
<school></school>
</div>
</template>
<script>
import School from './School'
export default {
name: 'App',
components: {
School: School
}
}
</script>
创建main.js
import App from './App.vue'
new Vue({
el: '#root',
template: '<app></app>' // 把root div用APP替换掉
components: {
App: App
}
})
创建index.html
<body>
<div id="root"></div>
<script type="text/javascript" src="./js/vue.js"></script>
<script type="text/javascript" src="./main.js"></script>
</body>
以上代码放到vue脚手架里就可以运行
Vue 脚手架是Vue官方提供的开发平台
babel es6 => es5
eslint 语法检查
npm run serve 运行项目
脚手架结构:
new Vue({
render: h => h(App)
}).destory销毁一个组件,再通过$amount 挂载一个新的组件
vue 主要包含两个部分
vue.js 与vue.runtime.xxx.js 的区别
vue.js 是完整版vue,包含: 核心功能+模板解析器
vue.runtime.xxx.js 是运行版本vue,只包含 核心功能
因为vue.runtime.xxx.js 没有模板解析器,所以不能使用tempplate配置项
需要使用render函数接收createElement函数去指定具体内容
=============================================================
vue 脚手架依赖webpack,所有配置都在webpack.config.js 中
想要查看配置,运行以下命令:
vue inspect > output.js,所有配置都会被生成到output.js 中
入口文件就在entry 下面配置
想要自定义配置,可以在根目录添加vue.config.js,可以调整脚手架工作模式
此配置最终有webpack接手,采用common.js 的暴露方式,因为webapck就用的是common.js
可以在vue.config.js 设置关闭语法检查
lintOnSave: false
================================================================
vue 获取dom元素
ref 用于给元素或者子组件注册引用信息
应用在html标签上获取的是真实的DOM元素,应用在组件标签上获取的是组件实例对象 VC
<template>
<h1 v-text="msg" ref="title"></h1>
<School ref="school"/>
</template>
<script>
export default {
name: "App",
components: {School},
data() {
return {
msg: "hello"
}
},
methods: {
showDom() {
// 获取h1 dom 元素
console.log(this.refs.school // 获取组件实例对象
}
}
}
</script>
============================================================
props 用于向子组件传参
父组件调用:
<Student name="马保国" :age="45" gender="男"></Student>
子组件:
<template>
<div>
<h1>{{name}}</h1>
<h1>{{age}}</h1>
<h1>{{gender}}</h1>
</div>
</template>
<script>
export default({
name: "Student",
data(){
return {
msg: "哈喽啊,大家好",
// name: "张三",
// age: 18,
// gender: "女"
}
},
// 外部调用传递进来
// props: ['name', 'age', 'gender']
// 对接收参数作类型限制
// props: {
// name: String,
// age: Number,
// gender: String
// }
// 更完善的写法如下,显示类型和是否必填
props: {
name: {
type: String,
required: true
},
age: {
type: Number,
required: false,
default: 20
},
gender: {
type: String,
required: false
}
}
})
</script>
====================================================================
props 是只读的
如果业务一定要改,先把传进来的属性赋值给,data属性,然后随便改
如下,myname 随便改
data(){
return {
msg: "哈喽啊,大家好",
myName: this.name
}
}
props: {
name: {
type: String,
required: true
}
}
=====================================================================
假如两个vue组件用了相同的配置 methods,可以通过以下方法实现代码复用
mixins 属性,就是用来复用配置的
假如混合传入一个x属性,组件本身也有一个x属性,那么就以组件本身为准
但是注意!!!!!
混合引入的生命周期钩子是 【都要!!!】两个都生效
写一个mixin.js
export const hunhe = {
methods: {
showName() {
alert(this.name)
}
}
}
使用的时候 引入
import {mixin} from '../mixin'
export default {
name: "school",
data(){
return {
name: "zhangsan"
}
},
// 此处的hunhe是外部引入的,下面的节点可以直接被当前组件使用
// 你在引入的hunhe里配置的data,methods,mounted 等任何节点都可以共用
mixins: [hunhe]
}
===============================================
全局引入混合:
在main.js 引入mixin.js,并注册
import main from './main.js'
Vue.mixin(hunhe)
以后项目下的所有子组件都会引入hunhe 里的配置
=================================================
插件就是一个包含instal方法的一个对象,install的第一个参数是Vue,
第二个及以后的参数是插件使用者传递的数据
新建一个plugins.js
export default {
install(Vue) {
// 注册全局过滤器
Vue.filter('mySlice', function(value) {
return value.slice(0, 4);
})
// 注册全局指令
Vue.directive('fbind': {
bind(element, binding) {
element.value = binding.value
},
inserted(element, binding) {
element.focus();
},
update(element, binding) {
element.value = binding.value
element.focus()
}
});
// 定义全局混入
Vue.mixin({
data() {
return {
x: 99
}
}
})
// 给Vue全局上添加方法
Vue.prototype.hello = () => {
alert("你好啊");
}
}
}
在main.js里引入并注册
import plugins from './plugins.js'
Vue.use(plugins)
=======================================================
组件样式类名如果冲突,后来的就会覆盖前面的
比如:先引入School, 再引入Student,那么Student的同名样式会覆盖School的
想要 样式不冲突,给style添加scoped属性,style 的样式之在当前组件生效
<style scoped>
...
</style>
实现原理:Vue给样式加了scoped属性的组件生成一个随机属性
用的时候会通过类名 + 随机属性来匹配样式
<div data-v-256434>...</div>
.demo-style[data-v-256434] {
...
}
!!!!!!App组件不应该加scoped
App里写的样式一般是项目里通用的,加上的话子组件就用不了了
========================================================
// 这么配置写样式可以用less, 不屑默认是css
less 可以嵌套着写,css不允许
<style lang="less">
.demo{
background-color: pink;
.qwe {
font-size: 40px;
}
}
</style>
=======================================================
平级组件之间传递参数
最初级方式:子组件 把数据传给父组件,父组件再把数据传给 另一个子组件
父组件App把addToDo方法传给子组件header
header 里的输入事件调用addToDo方法,往父组件添加数据
这里有个原则:数据在哪里,操作数据的方法就在哪里
toDoList 数据又传给+了子组件list
这样 子组件header 就把数据通过父组件header传给了自己的兄弟组件 list
父组件 App
<template>
<header :addToDo="addToDo"></header>
<list :toDoList=toDoList></list>
</template>
<script>
import header from './header'
import list from './list'
export default ({
data() {
return {
toDoList: []
}
},
methods: {
addToDo(toDoObj){
// 给数组前面加一个元素
this.toDoList.unshift(toDoObj);
}
}
})
</script>
子组件header
<template>
<input type="text" @keyup.enter="add"/>
</template>
<script>
export default ({
data() {
return {
toDoList: []
}
},
props: [addToDo]
methods: {
add(e) {
const toDoObj = {id: '001', title='e.target.value', done: false };
this.addToDo(toDoObj);
}
}
})
</script>
子组件list
<template>
<ul>
<li v-for="let toDoObj in toDoList" :key="toDoObj.id">{{toDoObj.title}}</li>
</ul>
</template>
<script>
export default ({
data() {
return {
toDoList: []
}
},
props: [toDoList]
methods: {
addToDo(toDoObj) {
// 给数组前面加一个元素
this.toDoList.unshift(toDoObj);
}
}
})
</script>
使用v-model时候切记,v-model 绑定的值不能是props 传递过来的,因为props是不可以修改的
如果props 传过来的是对象属性,v-model绑定其中的某一个属性是可以修改的,但是不推荐
=================================================
统计数据函数,reduce
pre 初始值为0,对象的done为true,统计+1
list.reduce((pre, current) => pre + (current.done ? 1 : 0), 0)
计算属性允许套娃,也就是说计算属性 可以用别的计算属性来计算
浏览器本地存储,能存储5M左右的字符串
localStorage: 浏览器关了再打开,存储的数据还在
localStorage.setItem("", "")
localStorage.getItem("")
localStorage.removeItem("")
localStorage.clear()
sessionStorage: 浏览器已关闭,存储数据就没了
sessionStorage.setItem("", "")
sessionStorage.getItem("")
sessionStorage.removeItem("")
sessionStorage.clear()
=============================================================
组件的自定义事件:
父组件实现
父组件使用Student组件,往Student组件身上绑定一个myevent事件,事件触发就调用testMethod方法
<Student v-on:myevent="testMethod"></Student>
// 只触发一次
<Student @myevent.once="testMethod"></Student>
methods: {
testMethod(name) {
console.log("收到name:" + name);
}
}
子组件
点击按钮调用emit('myevent', this.name);
}
}
父组件还可以通过ref属性给子组件绑定事件
这种事件灵活性强,可以异步实现
<Student ref="student"></Student>
mounted: {
setTimeout(() => {
this.on('myEvent', this.testMethod)
// 只触发一次
// this.once('myEvent', this.testMethod)
}, 3000)
},
methods: {
testMethod(name) {
console.log(name);
}
}
================================================
解绑自定义事件:
Student 组件定义方法
methods: {
unbindEvent(){
this.off(["myEvent", "myEvent2"]); // 解绑多个
this.$off(); // 解绑所有
}
}
this.$destory() 方法调用后,组件和子组件会被销毁,Student的自定义事件全都不奏效,原生事件不受影响
=============================================
给属性绑定原生事件:
Student组件所在的div一点击就能触发click事件
不写.netive Vue默认click是组件自定义事件,点点点是没有作用的
这也是vue模板根元素只能由一个的原因之一,不然写了多个根元素,组件的原生click事件不知道绑给谁
<Student ref="student" @click.native="show"></Student>
==============================================
全局事件总线:可以实现任意组件间的通信
全局事件总线又以下要求
1. 全局访问
2. 可以调用 emit, once方法
所以,可以使用Vue原型对象来作为载体
实现方式如下:
- 在Main.js 安装全局事件总线
new Vue({
el: "#app",
render: h => h(App),
// 在Vue启动之之前在Vue的原型对象上 添加事件总线
// bus = this
}
})
组件School里面注册一个事件
mounted: {
this.on('hello', (data) => {
console.log('我是school组件,收到了数据', data);
});
},
// 组件销毁之前,关闭hello事件监听
beforeDestory() {
this.off('hello');
}
组件Student里触发事件,发送数据
methods: {
sendStudentName() {
this.emit('hello', this.name);
}
}
====================================================
消息订阅与发布:
- 订阅消息:消息名
- 发布消息:消息内容
订报纸:
- 订阅报纸:住址
- 邮递员送报纸:报纸
安装pubsub.js库(publish,subscribe)
npm i pubsub-js
在School中订阅消息
import pubsub from 'pubsub-js'
mounted:{
this.pubId = pubsub.subscribe('hello', function(msgName, data) {
console.log('有人发布了hello消息', data);
})
}
// 销毁前取消订阅
beforeDestory() {
pubsub.unsubscribe(this.pubId);
}
在Student中发布消息
methods: {
publishStudentName() {
pubsub.publish('hello', 666)
}
}
==============================
判断tudo对象是否具有isEdit属性,用以下方法
todo.hasOwnProperty('isEdit')
有这样一个功能
点击编辑按钮,数据显示到一个输入框里,并且自动获取焦点
e.target.focus() 这句话不会生效!!!
为什么??? 因为showEdit改为true之后,界面上不会立即显示<input>框
要等到handelEdit方法执行完采取更新界面,那么e.target.focus() 在input显示出来之前就去获取焦点,肯定是失败的
可以使用timeout函数,正确的做法是用 nextTick 在下一次DOM更新结束后执行其指定的回调函数
什么时候用?当改变数据后,要基于更新后的DOM进行某些操作时,就要在netTick指定的回调函数中执行
<input v-show="showEdit" type="text" :value="todo.title" ref="inputTitle"/>
handelEdit(e){
this.showEdit = true;
// this.nextTick(function() {
this.$refs.inputTitle.focus();
})
}
======================================
Animate.js // 第三方动画库
npm install animate.css
import 'animate.css'
使用:
<transition-group
appear
name="animate__animated animate_bounce"
enter-active-class="animate__swing"
leave-active-class="animate__backOutUp"
>
<h1 v-show-"!isShow" key = "1">你好啊!</h1>
<h1 v-show="isShow" key="2">尚硅谷!</h1>
</transition-group>
Vue 封装的过渡与动画
- 作用:在插入,更新,或移除DOM元素时,在合适的时候给元素添加各种样式
- 进入:v-enter-active,退出:v-leave-active
- 写法:
- 准备好样式
- v-enter: 进入的起点
- v-enter-active: 进入的过程
- v-enter-to:进入的终点
- 元素离开的样式
- v-leave:退出的七点
- v-leave-active:离开的过程
- v-leave-to:离开的终点
- 使用
<transition name="hello">
<h1 v-show="isShow">你好啊</h1>
</transition> - 如果又多个元素需要过度,则需要使用<transition-group>,且每个元素都要指定key
=======================================================================
解决跨域问题,配置代理服务器
- 准备好样式
配置方式1:
在vue.config.js 中添加
devServer: {
// 代理服务器地址
proxy: 'http://localhost:5000'
}
使用的时候调用请求8080,代理服务器会把请求转发给5000
配置方式2:
请求8080/api/[path]
会转发给5000/[path]
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5000',
// 转发的时候把API替换成空
pathRewrite: {'/api':''},
// 支持websocket
ws: true,
// 修改HttpRequest中的Host为被代理服务器Host
// 为true host是5000,为false,host为8080
changeOrigin: true
},
// 可以配置多个代理
// '/api2': {
// ...
// }
}
}
=======================================================================
引入静态资源
在src下创建assets/css文件夹
在assets/css下添加bootstrap.css
在App.vue中引入
// 用import方式引入css文件,假如css使用了不存在的资源,脚手架会做严格检查
import './assets/css/bootstrap.css'
// 想要不报错,可以换一种引入方式
在public创建css文件夹,在index.html中引入
<link res="stylesheet" href="<% BASE_URL %>css/bootstrap.css">
================================================================
在组件Category内部定义一个插槽,挖个坑,等待使用者填充
默认插槽
<div>
<h1>标题</h1>
<slot>不给插槽传递东西,这句话就会显示</slot>
</div>
调用者给插槽传入一个图片,图片就会展示在组件插槽的位置上
<category>
<img src="XXX" alt="">
</category>具名插槽
有多个插槽,需要指定插槽名称
<div>
<h1>标题</h1>
<slot name="center"></slot>
<slot name="bottom"></slot>
</div>
调用者需要需要传插槽名称
<category>
<img slot="center" src="XXX" alt="">
<a slot="bottom" href="http://www.baidu.com">我是超链接</a>
</category>
调用插槽还有一种写法
通过template包裹需要传入的东西,给template添加标签 v-slot:bottm
<category>
<img slot="center" src="XXX" alt="">
<template v-slot:bottm>
<div>
<a href="http://www.baidu.com">我是超链接</a>
</div>
<h4>欢迎光临</h4>
</template>
</category>
- 作用域插槽
在组件定义一个插槽,不一样的地方在于插槽有个绑定参数,插槽会把参数传给调用者
<div>
<slot :games="games"></slot>
</div>
调用者使用scope(名字随便起)变量接收插槽传过来的数据,然后渲染
<category>
<template scope="scope">
<ul>
<li v-for="(g, index) in scope.games" :key="index">{{g}}</li>
</ul>
</template>
</category>
也可以通过解构赋值获取games,这么写
<category>
<template scope="{games}">
<ul>
<li v-for="(g, index) in games" :key="index">{{g}}</li>
</ul>
</template>
</category>
!!!!!!!!作用域插槽也可以有名字,传入时指定名字即可
============================================================
vuex 是什么
用来对Vue中多个组件中共享的数据进行集中式的状态管理,也是一种组件间的通信方式
- 什么时候使用vuex?
- 当多个组件依赖同一个数据时候
- 来自不同组件的行为需要变更同一数据的时候
107 需要回看
安装vuex
npm -i vuex@3,Vue2 只能使用版本3 的vuex,Vue3 只能使用4版本的veux
创建在根目录store/index.js
// 该文件用于创建Vuex中最为核心的store
import Vue from 'vue'
import Vuex from 'vuex'
// 使用vuex插件
Vue.use(Vuex);
// 准备actions---用于响应组件中的动作,通过this.$store.dispatch()调用
const actions = {}
// 准备mutations---用于操作数据(state),通过this.$store.commit()调用
const mutations = {}
// 准备state---用于存储数据
const state = {}
// 用于将state中的值加工输出
const getters = {}
// 创建store
export default new Vuex.Store({
actions,
mutations,
state,
getters
})
在main.js 中引入vuex
import store from './store/index'
new Vue({
el: "#app",
render: h => h(App),
store: store // 安装store
})
vuex使用流程
- 数据保存在state下
const state = {
sum: 0
}
- 在组件中通过this.$store.dispatch('addWait', 2) 调用action中的方法
actions 中的方法可以写一些通用的业务逻辑,操作state数据的操作通过调用mutations 中的方法来完成
actions 中的方法还可以继续通过dispatch来调用其他的actions方法
const actions = {
addWait(context, value) {
settimeOut(() => {
context.commit('add', value);
}, 500);
}
}
- 在actions中通过context.commit调用mutations 中的方法
mutations 中的方法用来直接修改state中的数据,一般不写业务逻辑
Vue开发者工具直接监听mutations,如果在其他地方修改state中的数据,开发者工具检测不到
const mutations = {
add(state, value) {
state.sum += value;
}
}
- mutations修改完数据,模板会重新渲染
- getters 使用方法
getter 接收一个state参数,可以把state中的数据做加工,然后输出
const getters = {
bigSum(state) {
return state.sum * 10;
}
}
组件使用bigSum可以通过this.$store.getters.bigSum获取
mapState
获取store下的数据必须要写成 this.store.state...
其中this.$store.state.可以通过mapState简化
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
export default{
// ...
computed: {
// mapState把state中的sum返回给计算属性he,使用的时候直接取this.he (对象写法)
//...mapState({he: 'sum', xuexiao: 'school'})
// 如果映射后的计算属性名跟state字段名称一样,可以用数组写法,更简便
...mapState(['sum', 'school'])
// mapGetters同理,跟mapState一样一样的,自己实现
},
methods: {
// 通过mapMutations生成mutations方法,
// 注意,加入有参数要传递,调用生成方法jia的时候可以传参
...mapMutations({jia: 'sum', jian: 'sub'})
// mapMutations 简写形式如mapSate
// mapActions 同理,自己实现
}
}
===========================================================================
vuex模块化编程
假如很多个业务的 actions,state,mutations,getters全部混在一起放在store里,代码将变得难以维护,vuex模块化编程支持把actions等以模块化分类,放到不同模块地下,这样编码和调用都将变得更有条理
- store 里面采用模块化编程
const personOptions = {
namespaced: true,
actions: {},
mutations: {
add(state, value) {
state.sum += value;
},
sub(state, value) {
state.sum -= value;
}
},
state: {
school: 'YC',
},
getters: {}
}
const countOptions = {
namespaced: true,
actions: {},
mutations: {},
state: {
sum: 0,
},
getters: {
mutiTen(state) {
return state.sum * 10;
}
}
}
export default new Vuex.Store({
modules: {
person: personOptions,
count: countOptions
}
})
- 组件使用时:
<h1>{{count.sum}}</h1>
<h1>{{count.add(2)}}</h1>
computed: {
...mapState(['person', 'count']),
// 想要直接取到count下state里面的sum,这么写
// 同时要在store 里面countOptions 下面加上namespeaced: true !!!!!!!!!!!!
...mapState('count', ['sum']),
// 用map获取getters里面的数据同state
// 如果不想用map,可以这么写
sum() {
return this.$store.state.count.sum;
}
// 不用map获取getters里的数据,注意跟获取state不一样,要这么写
bigSum() {
return this.$store.getters['count/mutiTen']
}
}
// ...
methods: {
...mapActions(['person', 'count'])
// 想要直接取count下mutations里面的add,sub方法可以这么写
// 同时要在store 里面countOptions 下面加上namespeaced: true !!!!!!!!!!!!
...mapMutations('count', {jia: 'add', jian: 'sub'})
// 如果获取mutations里的方法不用重命名,直接这么写
...mapMutations('count', ['add', 'sub'])
// 如果不想用map,可以这么写
add() {
this.$store.commit('count/add', 2)
}
}
模块化编程可以把 上面例子中的count和person写到不同的js文件里面,在store/index.js 中引入再注册
=======================================================================================
vue 路由
- 安装
vue3 对应 vue-router@4, vue2对应 vue-router@3
npm i vue-router@3
- 创建路由
在根目录新建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
}
]
})
- 引入
在main.js
import router from './router'
Vue.use(VueRouter)
new Vue({
el: '#app',
render: h => h(App),
router: router
})
- 使用
- 在页面中通过router-link 跳转路由
- 在页面中通过router-view 指定组件呈现位置
<router-link class="..." active-class="路由激活时候的高亮样式" to="/about"></router-link>
<router-link class="..." active-class="路由激活时候的高亮样式" to="/home"></router-link>
<div class="panel-body">
// 指定组件的呈现位置
<router-view></router-view>
</div>
============================================
- 组件一般分为路由组件和普通组件,路由组件一般放在pages文件夹下,普通组件一般放在components文件夹下
- 通过切换,隐藏了的路由组件默认是被销毁了,需要的是再重新挂载
- 每个组件都有自己的$route属性,里面存储着自己的路由信息
- 整个应用只有一个router,可以通过组建的$router属性获得
=========================================================
子路由
- 子路由配置在父路由节点下的children下
- 子路由path前面不要加/
- router-link使用子路由的时候要带上父路由,比如:
<router-link active="active-class" to="/home/news">news</router-link>
export default new VueRouter({
routes: [
{
path: '/about',
component: About
},
{
path: '/home',
component: Home,
children: [
{
path: 'message',
component: Message
},
{
path: 'news',
component: News
}
]
}
]
})
=======================================================
路由传参
- 在路由组件里过query传参
// 在router-link里传参
<router-link to="/home/new/message?id=666&title=ddd">message</router-link>
// 动态传参,to 必须要用bind取绑定,to里面用模板字符串
<router-link :to="`/home/new/message?id=${m.id}&title=${m.title}`">message</router-link>
// 在message组件里接收参数
this.$route.query.id
this.$toute.query.title
- 对象传参
<router-link
:to={
path:'/home/message/detail',
query: {
id: m.id,
title: m.title
}
}>
{{m.title}}
</router-link>
==========================================================================
命名路由
- 命名路由的作用就是简化路由跳转
- 使用
// 给路由命名
{
path: '/demo',
component: Demo,
children: [
{
path: 'test',
component: Test,
children: [
{
name: 'hello',
path: 'welcome',
component: Hello
}
]
}
]
}
// 简化跳转
完整路径写法
<router-link to="/demo/test/welcome">跳转</router-link>
简化写法
<router-link :to="{name: 'hello'}">跳转</router-link>
==============================================================
params 传参
- 在路由里配置参数
{
path: 'message',
component: Message,
children: [
{
name: 'detail',
path: 'detail/:id/:title',
component: Detail
}
]
}
- 跳转时携带参数
// 方式1
<router-link :to="`/message/detail/${m.id}/${m.title}`">详情<router-link>
// 方式2
<router-link :to="{
name: 'detail', // 对象写法此处只能支持name!!!!!,不支持path
params: {
id: m.id,
title: m.title
}
}">
</router-link>
- 使用时获取参数
this.$route.params.id
this.$route.params.title
=================================================================
路由的props
- 第一种写法,传固定值(此写法用处极少,几乎没用。。。)
props 值为对象,对象中的key-value都会以props的形式传递给Detail组件
// 在route中配置
{
path: 'message',
component: Message,
children: [
{
path: 'detail',
component: Detail,
props: {
a: 1,
b: 'hello'
}
}
]
}
// 在detail中通过props配置项接收参数
<div>
<h1>a: {{this.a}}</h1>
<h1>b: {{this.b}}</h1>
</div>
name: 'Detail',
computed: {
// ...
},
props: ['a', 'b']
- 第二种写法,路由配置props为true
props 值为布尔值,若布尔值为真,就会把路由组件收到的所有params参数,以props的形式传递给Detail组件
// 在route中配置
{
path: 'message',
component: Message,
children: [
{
path: 'detail/:id/:title',
component: Detail,
props: true
}
]
}
// 在detail中通过props配置项接收参数
<div>
<h1>a: {{this.a}}</h1>
<h1>b: {{this.b}}</h1>
</div>
name: 'Detail',
computed: {
// ...
},
props: ['a', 'b']
- 第三种写法,路由配置props为一个函数
函数里返回值返回的key-value会传递给detail组件的props
// 在route中配置
{
path: 'message',
component: Message,
children: [
{
path: 'detail/:id/:title',
component: Detail,
props($route) {
return {id: $route.query.id, title: $route.query.title}
}
// 可以用解构赋值简化为下面这种写法
props({query: {id, title}}) {
return {id, title}
}
}
]
}
// 在detail中通过props配置项接收参数
<div>
<h1>a: {{this.a}}</h1>
<h1>b: {{this.b}}</h1>
</div>
name: 'Detail',
computed: {
// ...
},
props: ['a', 'b']
===========================================================
router-link 处理浏览器历史记录
- 模式1:push模式(默认)
每次新的浏览记录会被追加到历史记录里面
- 模式2:replace模式(需要手动开启)
每次产生的新的历史记录会替换上一条历史记录
<router-link replace></router-link>
啥时候用push,啥时候用replace???
比如有这么一个页面A,A里面有几个子路由B,C,D
假如想要A 进入时候保留历史记录,而B,C,D三个子路由不保留历史记录,后退直接到A,则可以这么设置
A 为push 模式,B,C,D为replace 模式
先点A,然后点B,C,D,点一下浏览器后退按钮,页面直接退回A,
因为B,C,D为replace模式,虽然点了三下,但是只保留了一条历史记录,一后退直接就回到A了
=====================================================================
编程式路由导航
methods:{
// 用push模式跳转路由
pushShow(m) {
this.$router.push({
path: '/home/message',
query: {
id: m.id,
title: m.title
}
})
},
// 用replace模式跳转路由
replaceShow(m) {
this.$router.replace({
path: '/home/message',
query: {
id: m.id,
title: m.title
}
})
},
// 路由回退
back(){
this.$router.back();
}
// 路由前进
forward(){
this.$router.forward();
},
// go test
goTest(){
// 后退两步
this.$router.go(-2);
// 前进三步
this.$router.go(3);
}
}
===============================================================
缓存路由组件
- 路由组件切换默认是先销毁,再重新挂载新的路由组件
- 想要前一个组件不被销毁,而是隐藏起来,那就要设置<router-view>的缓存
// 当前router-view切换的所有组件都缓存起来,不销毁
<keep-alive>
<router-view></router-view>
</keep-alive>
// keep-alive还可以指定特定组件缓存,其他销毁,此处只指定News组件缓存
<keep-alive include="News">
<router-view></router-view>
</keep-alive>
// 指定缓存多个组件
<keep-alive :include="['News', 'Messages']">
<router-view></router-view>
</keep-alive>
======================================================
路由组件独有的生命周期钩子
- activated: 路由组件激活会被调用
- deactivated: 路由组件失活会被调用
对于路由组件来说,如果被指定缓存了(keep-alive),那么组件的mounted 和 beforeDestory两个钩子组件就失效不会被调用了,这时候可以用activated和deactivated两个钩子函数替代
===========================================================
路由守卫
- 配置全局前置路由守卫
const router = new VueRouter({
routes: [
{
path: '/about',
component: About
},
{
path: '/home',
component: Home,
children: [
{
path: 'message',
component: Message
},
{
path: 'news',
component: News
}
]
}
]
})
// 全局前置路由守卫,初始化被调用,每次路由切换之前调用
router.beforeEach((to, from, next) => {
// to: 要去的路由
// from: 来的路由
// next: 执行跳转,不调用next则停止跳转
if(to.path === 'home/news' || to.path === 'home/messages'){
if (localStorage.getItem('role') === 'admin'){
next();
} else {
alert('没有权限!!!');
}
} else {
next();
}
})
export default router
- 配置route的元信息
路由配置里有个meta配置项,接收一个对象,配置好之后可以在this.$route 中拿到
// 比如配置当前路由是否需要授权
{
path: '/userinfo',
component: UserInfo,
meta: {
isAuth: true
}
}
在路由守卫中可以用meta元信息作判断
router.beforeEach((to, from, next) => {
// 判断当前路由是否需要授权,如果需要授权,再验证权限
if(to.meta.isAuth) {
if (localStorage.getItem('role') === 'admin'){
next();
} else {
alert('没有权限!!!');
}
} else {
next();
}
})
- 后置路由守卫: 初始化时和每次路由切换之后被调用
router.afterEach((to, from) => {
// to: 要去的路由
// from: 来的路由
// 后置路由守卫没有next,因为路由已经切换完成,没有接下来的操作了
// 比如路由切换完成后去修改页面title信息 (title 配置在路由元信息中)
document.title = to.meta.title;
})
- 独享路由守卫:某一个路由单独使用的路由守卫
{
path: '/userinfo',
component: UserInfo,
meta: {
isAuth: true
},
beforeEnter(to, from, next){
// 内部逻辑只对userinfo这个页面产生作用
// 独享路由守卫只有前置,没有后置
}
}
- 组件内路由守卫
组件内部实现的路由守卫,在路由进入和离开时会调用
mounted() {
// ...
},
// 通过路由规则,进入该组件时被调用
// 假如当前组件作为子组件直接加载到页面上,不是通过路由规则进入到组件,beforeRouteEnter 不会被调用
// afterRouteLeave同理,必须是通过路由规则进入组件,在离开时才会被调用
beforeRouteEnter(to, from, next) {
// 可以写鉴权逻辑
// 此处的to时当前组件的route对象
},
// 通过路由规则,离开该组件时被调用
afterRouteLeave(to, from, next) {
// 此处的to是离开当前页面要去的路由对象
// from是当前组件的路由对象
}
beforeRouteEnter 和 afterRouteLeave 不与前置和后置路由守卫对应
前置后置路由守卫是在页面切换之前和之后调用,一旦页面切换,必然会先调用前置路由守卫,再调用后置路由守卫
beforeRouteEnter 是在进入页面时被调用,如果停留在页面上,afterRouteLeave 不会被调用,只有在跳走之前才会调用 afterRouteLeave
===============================================================================
vue 路由的hash工作模式和history工作模式
- hash 哈希模式路劲里带有#, 对于浏览器来说往后端发送路劲只会发送#之前的,比如:
浏览器发送路由:http://www.test.com/userinfo/test1/#/sdfs/sdfsd/sdf
后端收到的只是#前面的部分:http://www.test.com/userinfo/test1/,#后面的部分不会发送给后端 - history模式
vue路由模式默认是hash模式,history模式需要手动开启
const router = new VueRouter({
mode: history,
routes: [
{
path: '/about',
component: About
},
// ...
]
})
- hash模式和history模式的区别
- hash模式
- 路由中永远带着#,不美观
- 若以后将地址通过手机第三方app分享,若app严格校验,则地址会被标记为不合法
- 兼容性好
- history模式
- 路由中没有#,显得美观
- 兼容性跟hash模式相比略差
- 应用部署上线时,需要后台服务器支持,比如IIS,nginx另外作配置,否则页面刷新会出现404问题
- ??? 为啥刷新会出现404,因为vue是单页面应用,路劲只是用于vue app内部作路由跳转
- history模式路由中没有#,页面刷新浏览器会把整个路由发送给后台,后台找不到对应的路劲,自然会被404
- 后台服务器可以作相关配置,把该地址的路由都认为是静态路由,不返回404
==========================================================================
vue UI组件库
- 移动端
- Vant
- Cube UI
- Mint UI
- PC 端
- element UI
- Iview UI
组件库可以按需引入,也可以完整引入,具体怎么引入,自己研究官方文档!!!
===============================================================================
Vue3
- Vue3 带来了什么
- 性能提升
- 打包速度,内存减少,初次渲染速度
- 源码升级
- 使用Proxy代替defineProperty实现响应式
- 重写虚拟dom的实现和Tree-Shanking
- 拥抱TypeScript
- 新特性
- 性能提升
- vue 3 不在通过Vue对象创建APP
// 引入工厂方法,用于创建应用实例对象
import { createApp } from 'vue'
// 根模块
import App from './App.vue'
// 先创建 app实例对象,然后把对象挂载到index.html的 app容器上
createApp(App).mount('#app')
- vue 3 的模板结构可以没有根标签
- vue 3 的setup
setup 是vue3 的一个新的配置项,数据,方法,生命周期钩子等都要写在setup里
export default {
name: 'App',
setup() {
let name = 'zhangsan';
let age = 9;
function sayHello() {
alert(`I'm ${name} and ${age} years old`)
}
// 返回一个正常对象,对象里的属性和方法可以被模板直接使用
return {
name,
age,
sayHello
}
// 2. 返回一个函数,函数会渲染一个h1 替换模板的内容
return () => h('h1', 'hunter')
}
}
=================================================================
vue 3 ref 函数
- vue 3 setup中申明的变量默认不是响应式的,变量变化不会引起页面的变化,想要让变量成为响应式变量,就要加上ref
setup() {
// 加了ref的变量就变成了一个 引用对象(refrence implement)
let name = ref('张三')
let age = ref(18)
let job = ref({
type: '前端工程师',
salary: '18k'
})
// 修改引用变量
function changeInfo(){
name.value = '李四'
age.value = 19
job.value.type = '后端工程师'、
job.value.salary = '20k'
}
// 在模板里面读取变量不用.value,vue3 解析发现对象是ref对象的时候会自动加上.value
}
- ref 函数处理基本数据类型,使用的是Object.defineProperty实现数据劫持,最后实现相响应式
- ref 函数处理对象或数组数据,使用的是Proxy来实现响应式,Proxy 操作封装在了一个名字叫reactive的函数中;所以处理对象或者数组建议直接使用reactive函数
- 用ref处理基本类型,修改数据永远都要用.value来访问,比较麻烦,推荐用下面写法
setup() {
// 推荐写法!!!
let person = reactive({
name: '张三',
age: 18,
job: {
type: '前端工程师',
salary: '18K'
}
})
// 在模板里面读取变量不用.value,vue3 解析发现对象是ref对象的时候会自动加上.value
return {
person
}
}
- reactive 总结
- 作用:定义一个对象类型的响应式数据(基本类型还是使用ref)
- 语法:const 代理对象 = reactive(源对象),接收一个对象,返回一个代理对象(Proxy的实例对象)
- reactive 代理的响应式数据是深层次的
- 内部基于es6的proxy实现,通过代理对象操作源对象内部数据进行操作
===========================================================================
vue 2 响应式原理总结
-
实现原理
- 对象类型:通过Object.defineProperty()对属性的读写进行劫持
- 数组类型:通过对数组的一系列方法进行拦截(对数组的操作方法进行了包裹)
-
存在的问题
- 新增删除属性界面不会更新,需要用Vue.set方法绑定新属性,Vue.delete删除一个存在的属性
- 直接通过下标修改数组,界面不会自动更新
=================================================================================
vue 3 的响应式
可以直接新增、删除对象属性,可以直接修改数组元素,不需要使用vue 的set,delete 方法
代码实现
let person = {
name: '张三',
age: 18
}
// 任何对p的操作都会修改person对象,p是对person的代理
cosnt p = new Proxy(person, {
// 读取属性时会被调用
// target 是person对象
// propName 是读取的属性名称
get(target, propName) {
return target[propName]
},
// 增加或者修改属性时会调用
// target 是person对象
// propName 是读取的属性名称
// value 是传入的修改值
set(target, propName, value) {
target[propName] = value
},
// 删除属性的时候会调用
delete(target, propName) {
return delete target[propName]
},
})
- es6 反射
let obj = {a: 1, b: 2}
// 反射读取属性
Reflect.get(obj, 'a')
// 反射修改obj属性
Reflect.set(obj, 'a', 110)
// 反射删除obj属性
Reflect.deleteProperty(obj, a)
// Reflect 也有defineProperty
// res 时布尔值,返回是否成功
const res = Reflect.defineProperty(obj, 'c', {
get(){
return 3
}
})
- vue 3操作Proxy对象其实用的是Reflect
let person = {
name: '张三',
age: 18
}
// 任何对p的操作都会修改person对象,p是对person的代理
cosnt p = new Proxy(person, {
// 读取属性时会被调用
// target 是person对象
// propName 是读取的属性名称
get(target, propName) {
// return target[propName]
return Reflect.get(target, propName)
},
// 增加或者修改属性时会调用
// target 是person对象
// propName 是读取的属性名称
// value 是传入的修改值
set(target, propName, value) {
// target[propName] = value
Reflect.set(target, propName, value)
},
// 删除属性的时候会调用
delete(target, propName) {
// return delete target[propName]
return Reflect.deleteProperty(propName)
},
})
对比reactive和ref
- 从数据角度对比
- ref 用来定义基本类型数据
- reactive 用来定义对象类型数据
- 备注:ref也可以用来定义对象类型数据,它内部会求助reactive转换为代理对象
- 从原理角度对比
- ref 通过 Object.defineProperty() 的get、set来实现响应式
- reactive内部通过Proxy来实现响应式,通过Reflect来对对象进行操作
- 从使用角度对比
- ref 定义的数据:操作时需要用.value,读取数据时候,模板中直接读取,不需要.value
- reactive 定义的数据:操作于读取数据都不需要.value
Vue3 setup 注意点
- setup执行时机
- 在beforeCreate之前执行一次,this时undifined
- set的参数
- props:值为对象,包含:组件外部传递过来,且组件内部申明接收
- context上下文:
- attrs: 值为对象,包含组件外部传递过来,但是没有在props中申明接收的属性,相当于this.$attrs
- slots: 收到的插槽内容,相当于 this.$slots
- emit: 分发自定义事件的函数,相当于 this.$emit
- setup接收参数
export default{
name: 'Demo',
props: ['msg', 'school'],
// 申明自定义事件,申明后父组件才可以绑定
emits: ['sayHello']
// props 接收父组件传递的参数,
// context 上下文对象,包含:attrs, emit, slots
setup(props, context){
}
}
Vue3 计算属性
import {reactive, computed} from 'vue'
export default{
name: 'Demo',
// vue2 写法
//computed: {
// fullName() {
// return `${this.person.firstName} - ${this.person.lastName}`;
// }
//},
setup(props, context){
let person = reactive({
firstName: '张',
lastName: '三'
})
//vue3 计算属性
person.fullName = computed(() => {
return `${this.person.firstName} - ${this.person.lastName}`
})
//vue3 计算属性完整写法,考虑读和取
person.fullName = computed({
get() {
return `${this.person.firstName} - ${this.person.lastName}`
},
set() {
const nameArr = value.split('-')[0]
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
return {
person
}
}
}
vue3 watch属性
import {ref, watch, reactive} from 'vue'
export default{
name: 'Demo',
// vue2 写法
//watch: {
// sum: {
// // 立即监听
// immediate: true,
// // 深度监视
// deep: true,
// handler(newValue, oldValue) {
// }
// }
//},
setup() {
let sum = ref(0)
let msg = ref(msg)
let person = reactive({
name: '张三',
age: '18',
salary: {
j1: '30K'
}
})
// 1. 监视ref定义的响应式数据
watch(sum, (newValue, oldValue) => {
console.log('sum变了', newValue, oldValue);
})
// 2. 监视ref定义的多个响应式数据
// newValue 和 oldValue都是数组
watch([sum, msg], (newValue, oldValue) => {
console.log('监视数据变了', newValue, oldValue)
}, {immediate: true})
// 3. 监听reactive对象定义的响应式数据,此处无法正确的获得newvalue和oldvalue
// 强制开启了深度监视,deep配置无效
watch(person, (newValue, oldValue) => {
})
// 4. 监听reactive对象的某个属性
// 必须通过函数返回一个属性,直接写person.age无效
watch(() => person.age, (newValue, oldValue) => {
console.log('age 变了', newValue, oldValue)
})
// 5. 监听reactive对象的多个属性
// 必须通过函数返回一个属性,直接写person.age无效
watch([() => person.age, () => person.name], (newValue, oldValue) => {
console.log('属性 变了', newValue, oldValue)
})
// 特殊情况:监听reactive对象中的某个属性,这个属性是一个对象,这时候deep属性会生效
watch(() => person.salary, (newValue, oldValue) => {
console.log('属性 变了', newValue, oldValue)
}, {deep: true})
return {
sum,
msg,
person
}
}
}
vue3 watchEffect
import {ref, watch, reactive, watchEffect} from 'vue'
export default{
name: 'Demo',
setup() {
let sum = ref(0)
let msg = ref(msg)
let person = reactive({
name: '张三',
age: '18',
salary: {
j1: '30K'
}
})
watch(sum, (newValue, oldValue) => {
console.log('sum变了', newValue, oldValue);
})
// watchEffect 不用指明监视属性
// 用谁就监视谁,如果sum或者person.salary.j1的值变了,watchEffect就会被调用
// 与computed有点像,computed必须有返回值,watchEffect更注重逻辑
watchEffect(() => {
const x1 = sum.value;
const x2 = person.salary.j1
})
return {
sum,
msg,
person
}
}
}
vue3 生命周期、
- 有两个钩子函数改名了
- beforeDestory => beforeUnmount
- destoryed => Unmounted
- vue2 中的钩子函数在vue3 中还可以以配置的方式使用,也支持在setup中通过组合式函数使用钩子函数
- 组合式钩子函数名称都是在原来函数名称前面加个on
- beforeCreate 和 created没有对应的组合式钩子函数
- vue3 认为setup方法就相当于 beforeCreate + created
- 如果同时写了组合式API的钩子函数 和 配置项钩子函数,那么组合式API的先调用
- 比如:setup > beforeCreate > created > onBeforeMounte > beforeMounte > onMounted > mounted ...
vue3 自定义hook函数
- 有如下需求:鼠标点击时输出鼠标的坐标
import {ref, reactive, onMounted} from 'vue'
export default {
name: 'Demo',
setup() {
let point = reactive({
x: 0,
y: 0
})
function savePoint(event) {
point.x = event.pageX
point.y = event.pageY
}
// 添加window窗体监听事件
onMounted(() => {
window.addEventListener('click', savePoint)
})
// 组件卸载前移除 window窗体监听事件
onBeforeUnmount(() => {
window.removeEventListener('click', savePoint)
})
return {point}
}
}
- 假如挂载函数、卸载函数代码想要提取出来供大家调用,可以这么写
- 在根目录下创建/hooks/usePoint.js文件
- 把代码放进去
import {reactive, onMounte, onBeforeUnmounted}
export default function() {
let point = reactive({
x: 0,
y: 0
})
function savePoint(event) {
point.x = event.pageX
point.y = event.pageY
}
// 添加window窗体监听事件
onMounted(() => {
window.addEventListener('click', savePoint)
})
// 组件卸载前移除 window窗体监听事件
onBeforeUnmount(() => {
window.removeEventListener('click', savePoint)
})
return point
}
- 其他地方引入上面定义的文件
import usePoint from '../hooks/usePoint'
export default {
name: 'Demo',
setup() {
let point = usePoint()
return {point}
}
}
- 总结:
- 什么是hook:本质是一个函数,把setup函数中的CompositionAPI进行了封装
- 类似于vue2中的mixin
- 自定义hook的优势:复用代码,让setup可读性更好
toRef 和 toRefs
- 作用:创建一个ref对象,其value指向另一个对象
- 语法:const name = toRef(person, 'name')
- 应用:要将响应式中的某个属性单独提供给外部使用
- 扩展:toRefs 与 toRef功能一致,但是可以批量创建ref对象,语法toRefs(person)
import {reactive, toRef, toRefs} from 'vue'
export default{
name: 'Demo',
setup() {
let person = reactive({
name: '',
age: 18,
job: {
j1: {
salary: 20
}
}
})
// toRef让name2变量指向 person.name,name2修改会影响 person.name
const name2 = toRef(person, 'name')
const salary = toRef(person.job.j1, 'salary')
// 批量处理person的所有属性,功能根ref一样
const x = toRefs(person)
return {
name2,
...x
}
}
}
其他Composition API
- shallowReactive 浅层次响应式
- 何时使用:当对象只有最外层的属性的时候使用
// shallowReactive 只考虑第一层属性的响应式,下面的salary 不支持响应式
let person = shallowReactive({
name: '',
age: 18,
job: {
j1: {
salary: 20
}
}
})
- shallowRef 传入基本类型的话根ref没有区别,但是shallowRef 不会处理对象类型的响应式
- 何时使用:如果后续功能不会修改对象的属性,而是用新生对象来代替,就使用shallowRef
readonly 和 shallowReadonly
- 作用:让对象只读
- 语法:person = readonly(person)
- 场景:不希望数据被修改时
return default{
name: 'Demo',
setup(){
let person = reactive({
name: '',
age: 18,
job: {
j1: {
salary: 20
}
}
})
// 限制一个对象的所有层次的属性只读
person = readonly(person)
// 只对第一层对象限制只读
person2 = shallowReadonly(person)
}
}
toRaw & markRaw
- toRaw
- 作用:把一个reactive生成的响应式对象变成普通对象
- 场景:用于读取响应式对象的普通对象,对这个普通对象的所有操作都不会引起页面更新
- 语法:
- markRaw
- 作用:用于标记一个对象, 使其用于不会再成为响应式对象
- 场景:
- 有些值不应该被设置为响应式的,例如复杂的第三方类库
- 当渲染具有不可变数据的大列表时,跳过响应式转换,可以提高性能
export default {
name: 'Demo',
setup() {
let person = reactive({
name: '张三'
})
// 把响应式对象转换成普通对象
const p = toRaw(person)
function addCar(){
let car = {name: 'benchi', price: 40}
// 如果直接写 person.car = car,那么car就是一个响应式对象
// 用markRaw把car变成普通对象
person.car = markRaw(car)
}
}
}
customRef
- 作用: 创建一个自定义Ref, 并对其依赖项和更新触发进行显示控制
- 案例:实现防抖效果
// ref 相当于精装房,customRef相当于毛坯房,需要自己实现一些东西
import {ref, customRef} from 'vue'
export default {
name: 'App',
setup() {
function myRef(value, delay) {
let timer = null
return customRef((track, trigger) => {
return {
get() {
// 通知vue追踪value的变化
// 也就是提前和getter商量一下,让他认为这个value是有用的,当值变化时读取新值
track()
return value
},
set(nweValue) {
clearTimeout(timer)
value = newValue // 修改value
timer = setTimeout(() => {
trigger() // 通知Vue重写解析模板,模板会调用get方法读取新值
}, delay)
}
}
})
}
let keyWord = myRef('hello', 500)
return {keyWord}
}
}
provide 与 inject
- 作用:适用于祖孙组件之间通信
- 套路:父组件有一个provide选项来提供数据,子组件
- 用法:
// 祖先组件
import {provide} from 'vue'
export default {
name: 'app',
setup() {
let car = reactive({name: 'BYD', price: 40})
// 通过provide把数据提供给后代组件
provide('car', car)
}
}
// 后代组件
import {inject} from 'vue'
export default {
name: 'Child',
setup() {
// 后代组件通过inject获取父组件提供的对象
let x = inject('car')
}
}
响应式数据判断方法
- isRef:检查一个值是否为一个ref对象
- isReactive:检查一个对象是否是由reactive创建的响应式对象
- isReadonly:检查一个对象是否是由readonly创建的只读对象
- isProxy:检查一个对象是否是由reactive 或者 readonly方法创建
Vue2 中的API为配置式(Options API),Vue3中的API为组合式(Composition API)
- Options API:
- 新增或者修改需求,需要分别在data,methods,computed中修改功能对应的代码
- 同一个功能的代码时分别放到data,methods,computed等模块中,代码结构不合理
- composition API:
- 同一个功能相关的data,methods,computed方法都写在一起,代码结构更合理
- hook函数功能强大,提高代码复用
Fragment
- Vue2 中必须要有根标签
- Vue3 中可以没有根标签,内部多个标签会包裹在一个Fragment虚拟元素中
- 好处:减少元素层级