一、Vue简介
1.vue
vue是一个动态构建用户界面的渐进式JS框架,渐进式是指按需加载相应的功能模块。
2. vue的特点
- 遵循MVVM模式
- 采用组件化模式,提高代码复用率
- 双向数据绑定
- 本身只关注用户界面,可以轻松引入第三方插件或其他库
3. 单页面与多页面
多页面 | 单页面 |
---|---|
由多个完整页面组成 | 由一个页面和多个页面片段组成 |
页面之间的跳转是从一个页面跳转到另一个页面 | 页面的跳转是将当前页面片段删除或隐藏,将另一个页面片段加载显示,以此模拟页面的跳转 |
跳转后重新加载公共资源 | 跳转后不重新加载公共资源 |
URL:http://xxx/index1.html和http://xxx/index2.html 两个页面index1.html和index2.html | URL:http://xxx/index.html#page1和http://xxx//index.html#page2 同一个页面index.html的两个页面片段page1和page2 |
页面之间切换加载慢,用户体验差 | 首页加载慢,但页面片段之间切换加载快,用户体验好 |
对搜索引擎优化友好 | 不利于搜索引擎的抓取 |
在web页面初始化时就加载相应的HTML、CSS、JS等,页面加载完成后不会因为用户的操作进行页面的重新加载,而是动态切换页面片段,实现用户与页面的交互 | |
优化方案:根据当前页面的显示按需加载;根据路由拆分减少初始加载体积,(路由懒加载)在需要进入对应路由时再进行加载 |
单页面首页加载慢的优化方案:
- 资源加载优化:①减小资源大小,压缩代码;②优化资源加载时机,根据当前页面的显示按需加载,或根据路由拆分减少初始加载体积,(路由懒加载)在需要进入对应路由时再进行加载。
- 页面渲染优化:
4. MVVM和MVC
- MVC模式:
- Model-View-Control:Model用于存储数据,View用于处理数据显示,Control负责从视图读取数据,控制用户输入,并向模型发送数据。
- MVVM模式
- Model代表数据模型,可以定义数据修改和操作的业务逻辑;View代表视图层,负责将数据模型转换成用户界面展现出来;ViewModel是一个同步View和Model的对象。
- Model,实现一个数据监听器observer:对数据对象进行递归遍历,利用Object.defineProperty()的getter和setter属性监听数据,数据变动时通知订阅者
- View,实现一个指令编译器compiler:根据指令模板替换数据,初始化渲染页面,并对每个指令对应的节点绑定相应的更新函数
- VM,watcher订阅者是observer和compiler的桥梁,能够订阅并收到每个属性变化的通知,执行指令绑定的回调函数,从而达到更新视图的目的
MVVM整合observer、compiler和watcher三者,通过observer来监听model数据的变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通信桥路,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果。
- MVVM和MVC的区别
- MVVM实现了View和Model的自动同步,也就是当Model的数据改变时不需要操作Dom就可以自动更新View层,解决了MVC中使用大量Dom操作导致的加载速度慢、页面渲染性能低的问题。
5. 双向数据绑定原理
双向数据绑定就是说数据发生变化后会同步到视图中,用户在视图上的修改也会同步到数据中。
原理:采用数据劫持和发布订阅者模式实现的。主要是利用Object.defineProperty()的getter和setter属性对数据进行劫持,达到监听数据的目的,在数据发生变化时发布消息给订阅者,触发相应的监听回调。
6. v-model => 实现双向数据绑定
v-model
是v-bind
和v-on
的语法糖,在表单<input>
、<textarea>
、<select>
元素上创建双向数据绑定,监听输入事件以更新数据。
- 一方面绑定数据,在绑定数据变化时,更新页面输入控件里的值
- 另一方面监听input输入事件,将页面上输入控件里的值更新到绑定数据中。
// v-model
<input type="text" v-model="myboy">
<div> {{ myboy }} </div>
data: {
myboy: []
}
// v-bind和v-on
<input type="text" v-bind:value = 'mylove' v-on:input="change">
<p> {{ mylove }} </p>
data: {
mylove: ''
}
methods: {
change: function(){
this.mylove = $event.target.value
}
}
7. 响应式原理
(改变数据data时视图view会随之改变就是响应式) vue上的data属性都被添加getter和setter,当执行渲染函数时,读取data并调用getter,此时vue会记录所依赖的data(依赖收集);当data发生变动时会调用setter,此时vue会通知所有依赖该data的组件去更新视图。
8. vue从初始化-修改数据-更新页面的过程
当vue进入初始化阶段,一方面Observer会遍历data的所有属性,利用Object.defineProperty()设置getter和setter属性,实现数据劫持,在读取属性时会触发dep.depend()收集依赖,在设置属性时会触发dep.notify()触发依赖;另一方面Compiler指令编译器对元素节点的各个指令进行编译,初始化渲染页面,并绑定Watcher订阅者更新页面,Watcher将自己加入消息订阅队列Dep,此时初始化完成。
当数据发生变化时,触发Observer中的setter,调用dep.notify()方法,遍历订阅者队列,并调用其update()方法,生成新的虚拟dom,通过diff算法,更新视图。
9. Object.defineProperty()
- Object.defineProperty()会直接在一个对象上定义一个新的属性,或修改一个对象的现有属性。有三个参数,obj是需要定义属性的对象,prop是需要定义的属性,descriptor是属性描述符。返回值是该对象。
Object.defineProperty(obj, 'name', {
get: (name) => {
return this.name
},
set: (newName) => {
this.name = newName
return this.name
}
})
10. Proxy
Proxy对象用于创建一个对象的代理,访问用Proxy包装的对象都会被Proxy拦截。Proxy有两个参数,target是要访问的对象,handle是一个以函数作为属性的匿名对象,可以在这个对象中定义get和set函数
let p = new Proxy(target,handle)
11. Object.defineProperty()和Proxy
- Object.defineProperty()不能监听数组的变化,必须遍历对象的所有属性,必须深层遍历嵌套的对象。
- 不能监听数组的变化:数组的push、pop、shift、unshift、splise、sort、reverse方法不能触发set,vue中对这些方法进行了重写
- 必须遍历对象的所有属性:利用Object.keys()遍历对象的属性并通过Object.defineProperty()给对象设置getter和setter属性
- 深层遍历嵌套多层的对象
- Proxy劫持的是整个对象,不用遍历对象的属性;并且支持监听数组的变化;对于嵌套多层的对象,可以在get属性中递归调用Proxy
12. 计算属性computed
-
计算属性:当其依赖的属性值发生变化时,该属性也会发生相应的变化。
-
在计算属性中可以设置getter和setter属性,getter属性可以用来获取依赖的属性值,setter属性可以用来设置计算属性值并更新依赖的属性值。
-
计算属性具有缓存特性,只有在依赖的属性发生变化时才会重新执行getter;缓存特性存在的问题在于当依赖的属性具有实时性时,每次访问该实例时都要获取最新的属性值,此时只需要将该计算属性的cache改为false即可。(cache是用来控制计算属性的缓存与否)
-
通过调用方法也可以实现同样的效果,但是计算属性具有缓存特性,当其依赖的属性值没有发生变化时访问该计算属性会立即返回之前计算出来的值,不需要重新计算。
-
适用于一个数据受多个数据影响的场景。
{{ first }} + {{ last }} = {{ total }}
data() {
return {
first: 'xu',
last: 'caojun'
}
}
computed: {
total: function() {
cache: false, // 关闭计算属性的缓存特性
get: function() {
return this.first + this.last
},
set: function(newvalue) {
var names = newvalue.split(' ');
this.first = names[0]
this.last = names[1]
}
}
}
13. 监视属性watch
- **监视属性:**在属性值发生变化时自动触发相应的监听事件;watch属性的键是要监听的属性,值是监听事件
- watch属性的值可以是一个函数function(){},也可以是methods里的函数名’change’,或者是一个对象,这个对象有三个属性:handle(回调函数),immediate,deep(是否深入监听)
- watch有一个特点是最初绑定的时候不会执行,只有在数据变化时才会执行监听事件并更新,若想一开始的时候就执行监听事件,可以将immediate设为true
- 当deep为false时,在视图中的输入框中改变对象的属性值时监听事件是无效的。默认情况下,watch只能监听到将对象重新赋值,对象属性的变化监听不到。
data() {
return {
age:21,
height: 175,
student:{
id:1,
studentName:'xcj'
}
}
}
methods: {
change: function() {
console.log('22')
}
}
watch: {
age: 'change',
height: function() {
console.log(165)
}
student: {
handle: function() {
return this.id + "=" + this.studentName
},
deep: true,
immediate: true,//代表立即先去执行handle方法
}
}
14. methods与watch、computed的区别
- methods方法:定义函数,在需要时进行调用(v-on/@绑定事件),不依赖属性值的变化。
- watch:监听属性的变化,属性变化时自动调用methods或其他函数,watch还可以进行异步操作(setTimeout(()=> { },1000))
- computed:当某个属性依赖的多个属性发生变化,该属性也会随之发生相应的变化;同时也可以通过setter属性设置该属性,其依赖的属性随之变化。
15. v-bind:绑定class
- 对象语法:
- 直接绑定class属性:
<div v-bind:class="{active: isActive, red: isRed}"></div>
data() {
return {
isActive: true,
isRed: false
}
}// isActive为true表示使用active这个类名,isRed为false表示不使用red这个类名
- 将class作为对象处理:
<div :class="classObj"></div>
data() {
return {
classObj:{
red: true,
blue: true,
pink: blue
}
}
}
- 将class与computed结合起来:
<div :class="computeClass"></div>
data:{
red:true,
yellow:true
},
computed:{
computeClass(){
return {
red:this.red&&this.yellow
}
}
}
- 数组语法:
-
直接绑定class属性:
<div v-bind:class="[activeClass, errorClass]"></div>
-
三元运算符:
<div :class="[isActive? red: green]"></div>
-
数组和对象:
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
16. v-bind:绑定style
- 对象语法:
- 直接绑定style:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
,activeColor和fontSize的值都保存在data中 - 用对象绑定:
<div v-bind:style="styleObj"></div>
,styleObj是保存属性的对象 - 和计算属性结合:
<div :style="computeStyle">你好</div>
computed:{
computeStyle(){
return{
color:this.bl?'red':'blue',
fontSize:this.bl?'20px':'50px',
}
}
}
- 数组语法:
- 直接绑定style:
<div v-bind:class=['red','pink']></div>
- 绑定多层值:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
17. v-if/v-show(条件渲染)
- v-if:
- 用于条件渲染,当指令表达式的值为
true
时才会渲染,可以与v-else-if
、v-else
连起来使用。 - 若想同时切换多个元素,可以在这些元素外面包裹上
<template>
,在<template>
标签内使用v-if
。 - 用
key
管理可复用的元素
- 用于条件渲染,当指令表达式的值为
- v-show:
- 用于条件渲染,当
v-show
后面的值为true
时才会渲染,不能与v-else
一起用。 v-show
只是切换元素的display
属性
- 用于条件渲染,当
v-if
和v-show
的区别:v-if
是通过添加或删除dom
元素控制;v-show
是通过设置元素的display
属性控制v-if
切换有局部编译或卸载过程,在切换过程中重建或销毁事件监听和子组件;v-show
只是简单的css
切换v-if
只有在第一次为真时才开始编译,若初始页面为false
,则一直等到变成true
时才渲染;v-show
不管初始是否为真,都进行编译并缓存v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗
18. v-for(列表渲染)
v-for
:
<li v-for="item in items" :key="item.message"> {{ item.message }} </li>
<div v-for="item of items"> {{ item }} </div>
<div v-for="(value, name, index) in object"> {{ index }}-{{ name }}-{{ value }} </div>
<li v-for="n in evenNumbers">{{ n }}</li>
<-- evenNumbers可以是一个计算属性或方法,用于获取过滤值 -->
key
的作用和原理:
1. 虚拟DOM中key的作用:
key是虚拟DOM对象的标识,当数据发送发生变化时,vue会根据新数据生成新的虚拟DOM,然后vue进行新旧虚拟 DOM的差异比较:
若在旧虚拟DOM中找到与新虚拟DOM中相同的key:若虚拟DOM没有变化则依旧使用之前的真实DOM,若虚拟DOM发
生变化则生成新的真实DOM,替换掉之前的真实DOM。
若在旧虚拟DOM中未找到与新虚拟DOM相同的key,就创建新的真实DOM,随后渲染到页面。
2. 用index做key可能会引发的问题:
①对数据进行逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实DOM更新
②若结构中包含输入类DOM元素,则会产生错误的DOM更新
3. 如何选择key?
①使用每条数据的唯一标识作为key,比如id、证件号码等
②若仅用于渲染列表,可以使用index作为key
19. v-for和v-if
- 当
v-for
和v-if
用于同一个节点时,v-for
的优先级高于v-if
,这意味着v-if
作用在v-for
循环的每一个元素。 - 若是想条件式使用
v-for
循环,则可以在v-for
外层嵌套一个<template>
,在<template>
标签内加上v-if
判断语句。
20. v-on(事件处理)
v-on
监听DOM事件,触发相应的函数。@click="find"
、@keyup="count + 1"
、@mouseover="set(value)"
为什么在HTML中绑定监听事件?: ①便于在HTML代码中快速定位JS中的方法;②不需要在JS代码中手动绑定事件,和DOM完全解耦;③当ViewModel实例被销毁时,所有事件处理器都被清理。
21. Vue实例的生命周期
当一个Vue实例被创建时,他的data
对象中的所有属性都会被加入响应式系统,当属性值发生变化时视图也会随之更新。注意:只有在实例被创建时就存在的属性才是响应式的。可利用Object.freeze()使属性不能变化。
生命周期: vue实例从开始创建到销毁的一系列过程就是vue实例的生命周期,也就是实例的创建、挂载、更新和销毁的过程。作用:在这个过程中会运行生命周期钩子函数,可以在不同阶段添加不同的函数。
- 创建
vue
实例,初始化生命周期和事件,实例创建完的钩子函数是beforeCreate()
; - 初始化
data
和methods
,之后钩子函数是created()
,此时可以获取data
中的数据和调用methods
中的方法;接下来将data
对象中的数据和vue语法中的模板生成HTML
; - 在
beforeMount()
钩子函数之后,开始挂载生成的HTML
,然后将之前生成好的HTML
内容替换el
属性指定的dom
元素,但此时页面还没有内容; - 挂载完成后的钩子函数为
mounted()
,此时生成的HTML
已经渲染到页面; - 实时监控
data
,在data
被修改之前的钩子函数为beforeUpdate()
,此时页面还没有更新; - 比较新旧虚拟
dom
,找出真正需要更新的元素; - 钩子函数
updated()
之后页面已经完成更新,继续实时监控data
; beforeDestroy()
钩子函数是实例销毁前执行,此时还可以操作实例,此后会进行一系列实例销毁操作,接触数据引用,移除事件监听等;- 销毁完成后,执行
destroyed()
钩子函数,之后就不可以操作实例了。
22. 父子组件生命周期函数顺序
-
第一次加载页面触发的钩子函数
beforeCreate --> created --> beforeMount --> mounted
-
加载渲染过程
父beforeCreate --> 父created --> 父beforeMount --> 子beforeCreate --> 子created --> 子beforeMount --> 子mounted --> 父mounted -
更新过程
父beforeUpdate --> 子beforeUpdate --> 子updated --> 父updated -
销毁过程
父beforeDestory --> 子beforeDestroy --> 子destroyed --> 父destroyed
23. vue实例挂载
首先进行初始化,定义方法、事件等;然后调用$mount进行页面的挂载;定义udateComponent更新函数,执行render函数生成虚拟dom,然后将虚拟dom生成真实的dom,渲染到页面中。
24. vue中的nexttick
- nexttick:在修改数据后立即调用这个方法,获取更新后的DOM。
25. 虚拟DOM与diff算法
虚拟DOM:
- 真实DOM更新页面时只能通过遍历查询DOM树的方式找到需要修改的DOM然后修改样式行为或结构来达到更新页面的作用。每次查询都要遍历整个DOM树,非常消耗计算资源。
- 虚拟DOM是建立一个与DOM树对应的虚拟DOM对象,以对象嵌套的方式来表示DOM树,利用数据中对象的属性来描述节点,每次DOM的更改就变成了JS对象的属性更改,这样查找JS对象的属性变化比遍历DOM树的消耗性能小。
- 虚拟DOM的作用:提高效率,提高渲染性能。
- 虚拟DOM的实现:根据当前数据生成一个虚拟dom,数据发生变动时又会生成一个新的虚拟dom,通过diff算法,计算出新旧虚拟dom的差异,找出真正需要更新的节点,从而避免操作其他无需更改的节点。
diff算法:
- 对同层的树节点进行比较
- 特点:只在同级进行比较,不会跨级;在比较过程中,循环地从两边向中间靠拢。
流程:
- 对新旧节点的起始和结束位置进行标记
- 分情况进行虚拟节点的比较并移动标记(while循环)(patch函数)
- 若新旧节点的起始节点相同,则执行patchVnode函数,比较虚拟节点的属性等;同时新旧节点的起始索引都加一
- 若新旧节点的结束节点相同,则执行patchVnode函数,比较虚拟节点的属性等;同时新旧节点的结束索引都减一
- 若旧节点的起始与新节点的结束相同,执行完patchVnode函数后,将当前节点移到旧结束节点后面;同时旧节点起始索引加一,新节点结束索引减一
- 若旧节点的结束与新节点的起始相同,执行完patchVnode函数后,将当前节点移到旧起始节点前面;同时旧节点的结束索引减一,新节点起始索引加一
- 若上述四种情况都不存在,在事先创建的以旧vnode为key、以index为vlaue的哈希表中查找与新节点起始相同的节点,若能找到则放到相应位置,若查找不到则创建一个新的dom节点放到相应位置
- (while)循环结束后根据新旧节点的数目不同,对节点进行添加或删除
26. 自定义指令
Vue.directive(‘指令名字’, {对象数据或指令函数})
Vue.directive('focus',{
inserted:function(el){
el.focus()
}
})
二、vue组件化
1. 组件
组件化:把页面拆分成多个组件,每个组件实现自己的功能,组件之间的资源相互独立,实现组件的复用。
组件: 将UI界面拆分成一个个小的功能块,每个功能块实现自己的功能。是可复用的vue实例,可以在父组件中把子组件当成是自定义元素来用。
- 降低系统的耦合度(依赖程度),可以通过替换不同的组件完成需求
- 实现代码复用,提高开发效率和系统的可维护性
插件: 为vue添加全局功能,对vue功能的增强和补充
- vue-lazyload 图片懒加载
- vue-router
- 使用Vue.use()安装插件
**组件和插件的区别:**组件注册方式有全局注册和局部注册;插件注册使用Vue.use()
。组件是构成业务模块,插件是对vue功能的增强或补充。
2. 组件中的data
① 组件中的data必须是一个函数:对象是引用数据类型,若组件中的data是一个对象,复用的同一组件中的data都指向同一个地址,其中一个组件中的数据变化就会引起其他组件的数据都会变化;当data是一个函数时,每个组件的data都会有一个作用域,不会受到其他组件数据变化的影响。
② 组件中的data函数返回一个对象,用来存储数据。返回一个对象是因为要使用Object.defineProperty()
进行数据劫持。
-
vue中给data添加新属性,只会更改数据,不会更新视图
-
原因:新增的属性没有通过Object.defineProperty()设置成响应式数据。
-
解决方法
- Object.assign(target, source1, source3…):合并对象,将源对象的可枚举属性复制到目标对象
- Vue.set(obj,key,value):向响应式对象添加响应式属性,并更新视图
- this.$set(obj,key,value):同上
3. 组件通信
-
父子组件:
-
props和$emit:
- 父=>子:
props
,通过在子组件中定义props
,在子组件的props
中定义传递的数据的类型、默认值等,在父组件中的子组件标签内通过v-bind
绑定数据;**单向数据流:**防止子组件意外改变父组件中属性的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mX4OjFfp-1630299526884)(D:\00Front_Web\myKnowledge\images\父传子.png)] - 子=>父:
$emit
,在子组件中通过$emit
触发相应事件并传递数据,在父组件中的子组件标签内通过v-on
监听相应事件并接收数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1llgxy9y-1630299526886)(D:\00Front_Web\myKnowledge\images\子传父.png)]
- 父=>子:
-
$parent和$children:
- 子操作父:在子组件中通过this.$parent.data = "传递的数据"改变父组件中的data
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dr7tnDQI-1630299526887)(D:\00Front_Web\myKnowledge\images\子操作父.png)]
- 父操作子:在父组件中通过this.$children.data = "传递的数据"改变子组件中的data
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDAOLkte-1630299526888)(D:\00Front_Web\myKnowledge\images\父操作子.png)]
-
ref:
ref
属性是用来给子组件注册引用信息的,使用this.$refs.xxx
获取;ref
用在html
标签上是获取真实的DOM
元素,用在组件标签上是获取组件实例对象。ref
特性就是为元素或子组件赋予一个ID
引用,通过this.$refs.refName
来访问元素或子组件的实例。this.$refs
是一个对象,持有当前组件中注册过ref
特性的所有DOM
元素和子组件实例- 在父组件的子组件标签内,通过
ref="child"
绑定子组件,在父组件中通过this.\$refs.child.data
调取子组件的数据,通过this.\$refs.child.methods
调用子组件的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3iwcb4O-1630299526888)(D:\00Front_Web\myKnowledge\images\ref属性.png)]
- 在父组件的子组件标签内,通过
-
-
跨级通信
-
$attrs和$listeners:两个对象
$attrs
存放父组件中非props
中的属性,当一个组件没有props
属性时,$attrs
包含父组件所有的属性,并通过v-bind="$attrs"
传入子组件。$listeners
存放父组件中v-on
事件监听,通过v-on="$listeners"
传入子组件。
-
**子获取父组件属性:**在父组件的子组件标签中通过
v-bind
绑定需要传递的属性,在子组件props
中的属性不能再往下传递,通过this.\$attrs
获取到除props
中以外的属性 -
**孙获取父组件属性:**在子组件的孙组件标签中通过
v-bind='\$attrs'
绑定父组件传过来的除子组件props
中以外的属性,在孙组件props
中的属性不能再往下传递。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lwqVdREb-1630299526889)(D:\00Front_Web\myKnowledge\images\attrs.png)]
- **子调用父组件事件:**在父组件的子组件标签中通过
v-on
绑定父组件事件,在子组件中通过this.\$listeners.父组件事件
来调用 - **孙调用父组件事件:**在子组件的孙组件标签中通过
v-on="\$listeners"
绑定父组件事件,在孙组件中通过this.\$listeners.父组件事件
来调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G3DswhGy-1630299526890)(D:\00Front_Web\myKnowledge\images\父listeners子.png)]
- **孙传递给父组件:**在孙组件中通过
this.\$emit('相应事件','数据')
触发相应事件,在子组件的孙组件标签中通过v-on='\$listeners'
来监听触发事件,在父组件的子组件标签中通过v-on:相应事件="父组件事件"
来接收数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vWapCHqx-1630299526890)(D:\00Front_Web\myKnowledge\images\孙emit父.png)]
-
-
兄弟组件的通信(A=>B)
- 借助父组件,A传给父,父再传给B:先通过
$emit
将数据传给父并在父中保存,然后父通过props
传给B - bus总线传值:bus总线作用的两个组件应该在同一页面中,也可以在vuex中保存这个值。
- 若两个组件不在同一个页面,即使bus总线改变了值,当通过路由去往另一个组件后
data()
函数已经初始化。
在main.js中创建bus总线:
Vue.prototype.bus = new Vue(); // bus属性值为vue实例
在组件A中通过bus总线抛出信息和值,
this.bus
是vue实例,$emit
是vue实例的方法<div @click="handle"> hello </div> methods: { handle(e) { this.bus.$emit('message',e.target.innerText) } }
在组件B中的
created
、mounted
、data
函数中使用$on
监听事件和获取数据data() { this.bus.$on('busEvent',res=>{ this.busEvent = res; }) return { message: '' } } created() { this.bus.$on('busEvent',res=>{ this.busEvent = res; }) }
- 借助父组件,A传给父,父再传给B:先通过
**全局事件总线:**任意组件间通信。
4. 插槽
插槽<slot></slot>
就是子组件中留给父组件的占位符,在父组件中使用这个子组件时可以在占位符中填充任何模板代码,填充的内容会代替<slot></slot>
。
- 匿名插槽
<slot></slot>
:父组件的子组件标签内若有多个DOM,都填充到这个插槽。 - 具名插槽
<slot name="xxx"></slot>
:在父组件的子组件标签内,用v-slot:xxx
/#xxx
属性指明要填充到name="xxx"
的插槽。v-slot
只能添加在<template>
上。 - 作用域插槽:使父组件可以访问子组件的数据。将子组件上的数据用
v-bind
绑定到slot
标签的属性上,在父组件使用时,将slot
标签上绑定的属性命名为slotProps
,是一个对象。
// 匿名插槽
// 子
<h1>我是子组件</h1>
<slot></slot>
// 父
<children>
<span>我是魔鬼</span>
</children>
// 具名插槽
// 子
<slot name="header"></slot>
<h1>我是子组件</h1>
<slot name="footer"></slot>
// 父
<Child>
<span slot="header">我是header</span>
<span slot="footer">我是footer</span>
</Child>
// 作用域插槽
// 子
<slot v-bind:user="user">
// 父
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
5. 组件的注册
- 组件的名字:注册组件时可以使用连字符命名,也可以使用驼峰命名;引用组件时用连字符命名。
- 全局注册:①
Vue.extend()
②Vue.component("name",{})
;可以在任意vue实例中使用。 - 局部注册:使用vue实例的components属性,只能在这个vue实例下使用。
6. 自定义事件
//自定义事件的定义,也可以写在vue实例的methods里
vm.$on(自定义事件的名称,自定义事件的处理程序)
//自定义事件的触发
vm.$emit(自定义事件的名称,自定义事件处理程序需要的参数)
//父组件中定义自定义事件
methods:{
getChildData: function(val){
this.name = val;
}
}
//子组件中触发自定义事件
methods:{
sendParentData: function(){
this.$emit('receive',this.name)
}
}
//子组件中触发发送事件
<button @click="sendParentData">发送</button>
//父组件中接收数据
<p>{{ name }}</p>
<child @receive="getChildData"></child>
7. 动态组件&异步组件
-
**动态组件: **不同组件之间可以进行动态切换。
-
在组件标签内使用
is
属性来实现组件的动态切换:<component v-bind:is="currentTabComponent"></component>
。 -
DOM模板解析问题:在HTML中有些标签只能包含或包含于对应的标签,比如
<table>
只能包含<tr>
等:<table><tr is="component"></tr></table>
。 -
当动态组件之间进行切换时可以用
keep-alive
把组件包裹起来,保持组件的状态,以避免反复渲染导致的性能问题。但是用keep-alive
包裹的组件再一次进入时,就不会触发created
和mounted
,无法获取新数据。以上问题有两种解决方式:
①增加
activated()
生命周期钩子函数,在activated()
中添加请求数据的函数,可以在每次进入缓存的页面时再获取一次数据,退出这个组件时执行deactived()
失活函数。②在
keep-alive
中增加一个过滤exclude
,<keep-alive :exclude="name"><router-view></router-view></keep-alive>
,name
为不缓存的组件路由名字。- keep-alive的相关属性;
- include=‘字符串或正则表达式’:包含该名字的组件都被缓存
- exclude=‘字符串或正则表达式’:包含该名字的组件都不缓存
- max=数字:最多可以缓存的数量
- keep-alive的相关属性;
-
-
异步组件: 只在组件第一次需要渲染的时候进行加载渲染并缓存,缓存是以备下次渲染。
三、axios
1. axios是什么?
axios是基于promise和XMLHttpRequest的异步AJAX请求库,实现AJAX的异步通信功能。
为什么要二次封装?
- 每次http请求都要把设置超时时间、请求头、请求地址等操作重复一遍,会很繁琐,所以需要二次封装。
封装过程:
- 创建请求函数
- 在函数内,创建axios实例,设置请求地址前缀等
- 设置请求拦截器instance.interceptors.request.use()、响应拦截器instance.interceptors.response.use()
- 在函数内返回instance(config),真正的请求
2. axios在项目中的封装
- 项目的前期配置:下载
axios
,npm i axios -S
;在require.js
中引入,import axios from "axios"
。 - 创建一个
axios
实例:
const instance = axios.create({
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
baseURL: 'http://ttapi.research.itcast.cn/',
timeout: 5000, // 如果请求花费了超过 `timeout` 的时间,请求将被中断
method: 'GET'
})
- 设置请求拦截:
require.interceptors.request.use(
config => {
// 对请求参数进行配置
return config;
},
errer=>{
// 对错误进行处理
}
})
- 设置响应拦截:
instance.interceptors.response.use(result => {
return result.data;//返回处理后的data
}, error => {
console.log(error);
});
- 导出这个实例:
export default require
export function require(config) {
const instance = axios.create({})
//响应拦截
instance.interceptors.response.use();
// 请求拦截
instance.interceptors.request.use();
return instance(config);
}
- 先在
home.js
中引入require
函数并使用
import { require } from './require.js';
export function getData(data) {
return require({
url: '/home/data', // 相对路径
data: {
type,
page
}
})
}
3. 解决axios的跨域问题
跨域本质是浏览器基于同源策略的一种安全手段。同源就是协议、主机和端口全都一致。跨域是浏览器的限制。
- Proxy: proxy是网络代理,是一种特殊的网络服务,允许客户端通过网络代理与服务端进行非直接的连接。代理服务有利于保障网络终端的隐私或安全,防止攻击。在
vue.config.js
文件中配置devServer
实现代理转发,从而实现跨域。
// 在本地会创建一个虚拟服务端,然后发送请求的数据,并同时接收请求的数据,这样服务端和服务端进行数据的交互就不会有跨域问题
devServer: {
proxy: {
'/proxyApi': {
target: 'http://dev.xxx.com', // 请求的第三方接口
changeOrigin: true, // 是否跨域
pathRewrite: { // 路径重写
'/proxyApi': ''// 替换target中的请求地址,也就是说/api=/target,请求target这个地址的时候直接写成/api即可。
}
}
}
}
- CORS: 跨域资源共享是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端的JS代码获取跨域请求的响应。整个CORS通信过程都是浏览器自动完成,浏览器一旦发现跨域请求,便会自动添加一些附加的头信息,只要服务器实现了CORS接口,就可以跨域通信。
四、vuex
vuex相当于一个公共的仓库,用于保存所有组件都能使用的公共数据。Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式管理,也是组件间进行通信的一种方式。vuex不属于任何一个组件。
1、什么时候使用vuex?
- 多个组件依赖同一个状态
- 来自不同组件的行为需要变更同一状态
2、vuex工作原理:
- Actions:对象类型,异步操作
- Mutations:操作
state
数据的方法的集合,比如对该数据的修改、增加、删除等。 - State:对象类型,存放状态
3、vuex的工作流程:
首先,Vue
组件如果调用某个VueX
的方法过程中需要向后端请求时或者说出现异步操作时,需要dispatch
VueX中actions
的方法,以保证数据的同步。可以说,action
的存在就是为了让mutations
中的方法能在异步操作中起作用。如果没有异步操作,那么我们就可以直接在组件内提交状态中的Mutations
中自己编写的方法来达成对state
成员的操作。注意,不建议在组件中直接对state
中的成员进行操作,这是因为直接修改(例如:this.$store.state.name = 'hello'
)的话不能被VueDevtools
所监控到。最后被修改后的state成员会被渲染到组件的原位置当中去。
4、Mutations
mutations
中的方法都有默认的形参:[state,传递参数]
。
- 在store的index.js中
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.store({
state:{
name:'helloVueX'
},
mutations:{
//es6语法,等同edit:funcion(){...}
edit(state){
state.name = 'jack'
}
}
})
export default store
- 在组件中调用mutations的方法,同步操作数据的,
this.$store.commit('方法','参数')
// 单个值传值
this.$store.comit('edit', 'xcj')
// 多个值传值
this.$store.comut('edit', {'name':'xcj','age':25})
// 另一种方式
edit(state,payload){
state.name = 'jack'
console.log(payload) // 15或{age:15,sex:'男'}
}
// 在组件中提交并传值
this.$store.commit({
type: 'edit',
payload: {
age: 25,
sex: 'mail'
}
})
// 添加state
Vue.set(state,"age",15)
// 删除state
Vue.delete(state,'age')
- Getters方法,获取数据的,
this.$store.getters.xxx
Getters
可以对state
中的状态加工后传递给外界。getters
方法中有两个参数,state
和getters
。
getters:{
nameInfo(state){
return "姓名:"+state.name
},
fullInfo(state,getters){
return getters.nameInfo+'年龄:'+state.age
}
}
// 在组件中调用
this.$store.getters.fullInfo
- Actions:异步操作数据的,
this.$store.dispatch('方法','参数')
// context:上下文对象,payload:挂载参数
actions:{
aEdit(context,payload){
setTimeout(()=>{ // 异步操作mutations中的方法
context.commit('edit',payload)
},2000)
}
}
// 在组件中调用
this.$store.dispatch('aEdit',{age:25})
五、路由
1. 前端路由和后端路由
路由:根据不同的url地址展示不同的页面或内容,一个路由就是一组键值的映射关系(key => value)。
前端路由:key
为路径,value
为组件,用于展示页面内容。当浏览器的路径发生变化时,对应的组件就会显示。
后端路由:通过用户请求的url导航到具体的HTML页面,每次跳转到不同的URL都是重新访问服务器,服务器再返回页面。
2. 路由的基本使用
<router-link>
:使用<router-link>
来导航,通过传入to
属性指定要跳转的页面,会被默认渲染成<a>
链接。
<router-view>
:路由出口,路由匹配的组件在这里渲染。
3. $route 和 $router
**
r
o
u
t
e
:
∗
∗
每
个
组
件
都
有
自
己
的
‘
route:** 每个组件都有自己的`
route:∗∗每个组件都有自己的‘route属性,里面存储着自己的路由信息。
this.$route是当前跳转的路由对象,每个路由都会有一个
route对象,包含
path(当前路由对象的路径)、
name(当前路由对象的名字)、
query`(当前路由中查询参数的键值对)等。
$router: 整个应用只有一个router
,可以通过this.\$router
获取。this.\$router
是用来进行路由跳转的,通过VueRouter
和Vue
构造函数得到的一个全局实例对象,包含了所有的路由,包括很多对象和属性。比如history
对象,this.\$router.push({path:home})
就是向history
中添加路由记录。还有options
对象,包含mode
、routes
等。
4. 嵌套路由
在路由配置中,添加children
属性,children
是一个数组,在<router-link>
的to
属性中要设置为完全路径。
const routes = [
{ path: '/',
component: Home
},
{ path: '/foo',
component: Foo ,
children:[
{path:'foo1',component:Foo1},
{path:'foo2',component:Foo2},
{path:'foo3',component:Foo3},
]}
];
<p>
<router-link to="/foo/foo1">to Foo1</router-link>
<router-link to="/foo/foo2">to Foo2</router-link>
<router-link to="/foo/foo3">to Foo3</router-link>
</p>
5. 路由传参query
传递参数:
<!-- 第一种方式 -->
<router-link :to="`/home?id=${m.id}&title=${m.title}`"></router-link>
<!-- 第二种方式 -->
<router-link
:to="{
path: '/home',
query: {
id: m.id,
title: m.title
}
}"
></router-link>
接收参数:
$route.query.id
$route.query.title
6. 命名路由
命名路由可以简化路由的跳转。
给路由命名:
{
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>
<!-- 简化后传递参数 -->
<router-link
:to="{
name: 'hello',
query:{
id: m.id,
title: m.title
}
}"
>跳转</router-link>
7. 路由传参params(动态路由)
配置路由,声明接收params参数:
{
path: '/demo',
component: Demo,
children: {
{
path: 'news',
components:News
},
{
components: Message,
children: [
{
name: 'hello', // 给路由命名
path: 'detail/:id/:title', // 使用占位符声明接收params参数
component: Detail
}
]
}
}
}
传递参数: 使用params
传递参数时,若使用to
的对象写法,则不能使用path
的配置项,必须使用name
配置。
<!-- 跳转并携带params参数(字符串写法) -->
<router-link to="/home/message/detail/123/你好">跳转</router-link>
<!-- 跳转并携带params参数(对象写法) -->
<router-link
to="{
name: 'hello',
params: {
id: 123,
title: '你好'
}
}"
>跳转</router-link>
接收参数:
$route.params.id
params与query的区别: query
在页面跳转时,地址栏有请求的参数,刷新页面数据不会丢失。params
参数不会在地址栏显示出来,刷新页面数据会丢失。 params
传参,只能是 name:‘xxxx’
,注意配对使用,否则接收参数为undefined
。
8. 路由传参props
{ // 路由解耦
path: '/demo',
component: Demo,
children: {
{
path: 'news',
components:News
},
{
components: Message,
children: [
{
name: 'hello', // 给路由命名
path: 'detail/:id/:title', // 使用占位符声明接收params参数
component: Detail,
// props第一种写法:值为对象,该对象中的所有键值对都会以props的形式传给Detail
props: {a: 1, b: 'hello'}
// props第二种写法:值为布尔值,若为true,就会把该路由组件的params参数以props的形式传给Detail
props: true
// props第三种写法:值为函数,返回值是对象
props($route){
return {id: $route.query.id, title: $route.query.title}
}
}
]
}
}
}
9. 编程式路由导航
有时候不能使用<router-link>
实现跳转,因为<router-link>
会被默认渲染成<a></a>
标签,比如想使用<button>
来实现跳转就不能再使用<router-link>
。
编程式路由就是使用JS来实现路由的跳转:
$router.push(); // 向history栈添加一个新记录,当点击浏览器后退按钮时,可以返回到之前的URL。
$router.replace(); // 替换路由,没有历史记录,点击返回,会跳转到上上一个页面
$router.go(n); // 向前或者向后跳转n个页面,n可为正整数或负整数
$router.back(); // 后退
$router.forward(); // 前进
10. 路由守卫
-
路由跳转过程中的钩子函数,路由跳转的过程可以分为跳转前中后的细小过程,每个过程都有对应的钩子函数,可以在此期间触发事件。
-
**当点击切换路由时: ** beforeRouterLeave–>beforeEach–>beforeEnter–>beforeRouteEnter–>beforeResolve–>afterEach–>beforeCreate–>created–>beforeMount–>mounted–>beforeRouteEnter的next回调
-
全局守卫: 路由实例上直接操作的钩子函数,特点是所有路由配置的组件都会触发。
[beforeEach]:在路由跳转前触发,主要用于登录验证。
[beforeResolve]:在独享路由守卫和组件路由守卫之后调用,也是路由跳转前触发。
[afterEach]:路由跳转完成后触发。
- 独享路由守卫: 在单个路由配置的钩子函数,写在路由配置映射关系的
routes
中。
[beforeEnter]:路由跳转前触发。
- 组件路由守卫: 在组件内执行的钩子函数,相当于为配置路由的组件添加的声明周期钩子函数。
[beforeRouterEnter]:路由跳转前触发,不能获取组件实例的this,在next中通过vm访问组件实例,next中的函数在mounted之后调用(为了保证对组件实例的完整访问)。
[beforeRouterUpdate]:在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例;对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,组件实例会被复用,该守卫会被调用;当前路由query变更时,该守卫会被调用。
[beforeRouterLeave]:导航离开该组件的对应路由时调用,可以访问组件实例this
。
参数:
to
:目标路由对象from
:即将要离开的路由对象next
:next()
进行下一个钩子函数;next(false)
中断当前的导航,若URL改变,URL会重置到from的路由对象;next('/')
或next({path:'/home'})
跳转到另一个地址,不是to的目标路由;next(error)
中传入的参数是一个error实例,则导航会被终止且会将错误传递给route.onError()
注册过的回调。
11. 路由模式
浏览器对页面的访问是无状态的,导致切换不同的页面时都要重新发送请求,在vue
路由中不重新发送请求就能更新页面。有两种路由模式:hash模式和history模式。
hash
模式:hash
模式在URL中有#
,#
在英文中读作hashtag,所以简称hash
模式,#
后面的值称为hash
值。- 特点:
hash
值虽然出现在URL中,但是不会被包括在HTTP请求中,因此改变hash
值不会重新加载页面。 - 原理:
hash
模式的原理是onhashchange()
事件,可以利用window.onhashchange()
为hash
值的改变添加页面切换处理函数。 hash
值发生的变化都会在浏览器的访问历史中添加记录。
- 特点:
history
模式:利用了history pushState
和replaceState()
API,可以分为两大部分:切换和修改。- 切换历史状态:包括
go()
、back()
、forward()
三个方法,对应前进、后退、跳转操作。 - 修改历史状态:
history.pushState()
用于在浏览历史中添加历史记录,但是并不触发跳转;history.replaceState()
用于在浏览历史中修改当前记录,不触发跳转。 - 通过
pushState
把页面的状态保存在state
对象中,当页面的url再变回这个url时,可以通过event.state
取到这个state
对象,从而可以对页面状态进行还原
- 切换历史状态:包括
hash
模式下前端路由改变的是#
后面的内容,页面不会重新加载,即使服务端没有覆盖所以路由,也不会返回404错误;而在history
模式下,如果服务端没有相应的资源则会返回404错误(NOT FOUND),需要在服务端增加一个覆盖所有情况的候选资源,在URL匹配不到资源时应该返回同一个index.html
页面,就是app
依赖的页面。
为什么history模式下前端的url必须和后端一致? 因为vue是单页面应用,一个url对应一个页面。
12. 路由懒加载
在单页面应用中,如果没有应用懒加载,会导致在首次进入页面时需要加载的资源过多,延时过长,造成用户体验不好;运用路由懒加载可以将页面进行划分,需要的时候加载页面,可以有效的分担首页的加载压力,减少首页的加载时间。
-
vue异步组件
-
webpack的require,ensure()
-
ES6的import:
const 组件名=() => import('组件路径');
页面一加载就会执行
import Login from '@/views/login'
, 相当于编译执行了加载组件;使用路由懒加载的写法,只会在进入当前这个路由时才会走component
,然后在运行import
编译加载相应的组件
六、vue项目遇到的问题及解决
1. vue项目加载的顺序
①index.html项目的入口文件 ②main.js项目的核心文件,在main.js中创建一个新的vue实例,将根组件App.vue放在这个实例的components属性中,并将这个实例的app与index.html联系起来 ③App.vue根组件,引入子组件和<router-view>
(用来显示路由相关页面) ④index.js配置路由的文件 ⑤子组件