一、指令
1.1 小胡子语法
在vue中使用{{}}mustache语法的文本插值,用于数据绑定更新。它里面不仅仅可以直接写变量,还可以写表达式。如下所示:
<div id="app">
<!-- 小胡子里面可以是变量,表达式,对象,数组,或者是函数返回值 -->
<div>{{msg}}</div>
<div> {{[1,2,3].map(item=>item*2)}}</div>
<div>{{arr}}</div>
<div>{{obj.age}}</div>
<div>{{isShow?"yes":"no"}}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'message',
obj: { age: 20 },
arr: [1, 2, 3],
isShow: 1
}
})
</script>
1.2 常用指令
1、v-once:它表示元素或者组件只渲染一次,不会随着数据改变而改变,后面不需要跟任何表达式。
2、v-html:双大括号方式会将数据解析为纯文本,而不是html。如果是真正要输出html标签要用到这个v-html指令,它等价于js中的innerHTML。
3、v-text:等价于js中的innerText。作用和mustache相似都是用于将数据显示在界面中。
4、v-pre: 有这个指令的标签及其后代元素,vue都不在编译,主要用来跳过这个元素和它的子元素编译过程,用来提升编译效率。
5、v-cloak:保持在元素上直到关联实例结束时进行编译。它是Vue专门用来提供解决小胡子显示问题的指令。它需要配合css来使用,当vue编译完成后,会把这个属性删掉,这时css属性就不起作用。
6、v-bind(:):用来动态绑定一个或者多个属性。一般在class或者style的动态绑定上频繁使用。
7、v-on(@): 绑定事件监听器。它可以绑定函数,对象或者是表达式。如果后面绑定的方法不需要额外参数那方法后面()可以不添加;在定义事件写方法时如果方法本身是需要一个参数那vue会默认将event事件对象做为参数传入到方法中,方法后面不用写上(),如果参数为其它则需要在方法后面加上()并且有形参;如果需要传入某个参数并且同时需要event事件对象,可以通过$event作为参数传入函数。它里面还有.stop、.prevent、.{keyCode}等修饰符。
8、v-if/v-else:Vue会根据表达式真假条件来渲染元素。它可以实现条件渲染,用来控制元素或者组件是否进行加载,它有比较大的切换开销。
9、v-show:用来控制元素或者组件是否显示。和v-if不同的是如果v-if值是false那么这个元素将销毁不会在DOM中存在,当v-show元素会始终被渲染保存在DOM中当v-show为false它只是给元素增加了一个display:none属性将其隐藏。它有比较大的初始化加载开销。
10、v-for:用来循环展示标签。可以循环数组,数字,字符串,对象(基本上都是数组)。一般要带一个:key属性。对于Vue来说key有极其重要的作用。它是元素的身份证明是Vue中vnode的唯一标记,通过key,diff操作可以更准确、更快速,带key就不是就地复用并且利用key的唯一性生成map对象来获取对应节点比遍历方式更快,有利于Vue中的DOM DIFF计算,总之key的作用主要是为了高效更新虚拟DOM。v-if和v-for相比v-for的优先级高。
11、v-model:一般用于表单元素上,创建双向数据绑定。v-model它其实是语法糖,在内部为不同输入元素使用不同属性和不同事件。一般可以用.lazy、.number、.trim等修饰符进行修饰限制。
- text和textarea元素使用value属性和input事件
- checkbox和radio使用checked属性和change事件
- select将value作为prop和change事件 以input框为例:
<input v-model="message" />
相当于:
<input v-bind:value="message" v-on:input="message = $event.target.value" />
1.3 自定义指令
Vue除了提供默认内置指令以外,还可以让开发者根据实际情况来自定义指令。通过它我们可以对DOM元素进行更多底层操作。它存在着全局注册和局部注册两种方式。
我们可以通过Vue.directive(id,[definintion])方式来进行全局注册,第一个参数为自定义指令名称,第二个参数可以是对象数据也可以是一个函数。需要注意的是指令名称不需要加v-,默认是自动加上,但在使用时要加上v-前缀。也可以通过在Vue实例中添加directives对象来进行注册。在工作中如果有多个自己定义的指令,一般会单独新建一个文件,用import或者script进行引入。先执行局部再用全局指令。
二、computed vs methods vs watch
computed:计算属性。依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算。它其实就一个函数,里面包含set和get,但set一般是只读可以删除。所以它使用时后面可以不用加括号。
computed: {
// fullName:function(){
// return this.firstName+' '+this.lastName;
// }
// 也可以写成以下形式:
fullName: {
set: function (newValue) {
const names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[1];
},
get: function () {
return this.firstName + ' ' + this.lastName;
}
},
}
methods:它里面的this自动绑定为Vue的实例。不存在缓存,每次调用都编译执行函数代码,不能使用get,set。在表达式中可以通过调用方法达到和调用computed属性一样的显示效果,一般是用于回调函数。
watch:主要用于监听vue实例已有属性的变化(不管是data中的属性还是computed中的属性),对于不存在的属性无法起作用。它更多是观察,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用computed,可以利用computed缓存特性避免每次获取值都要重新计算;
- 如果每次渲染时都希望重新执行函数,可以用methods;
- 如果需要在数据变化时执行异步或开销较大的操作,应该使用 watch。使用 watch 选项允许我们执行异步操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。
三、生命周期
Vue生命周期就是Vue实例从开始创建、初始化数据、编译模板、挂载更新渲染、卸载从出生到消亡的这一系列过程。vue生命周期总共分为8大阶段: 创建前/后,载入前/后,更新前/后, 销毁前/后,在这8大阶段分别有不同的钩子函数来进行相应操作:
- beforeCreate阶段:vue实例的挂载元素el和数据对象data都是undefined,还没有初始化。
- created阶段:vue实例的数据对象data有了,可以访问里面的数据和方法,未挂载到DOM,el还没有。异步请求可以写在这个阶段,能更快获取到服务端数据,减少页面 loading 时间。
- beforeMount阶段:vue实例的el和data都初始化了,但是挂载之前为虚拟的dom节点。
- mounted阶段:vue实例挂载到真实DOM上,在这一阶段可以访问操作DOM。
- beforeUpdate阶段:响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器
- updated阶段:虚拟DOM重新渲染和打补丁之后调用,组成新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环。
- beforeDestroy阶段:实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件
- destroyed阶段:实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。
四、什么是MVVM和MVC?
Model-View-ViewModel(MVVM)它是一个软件架构设计模式,来源于Model-View-Controller(MVC)模式。MVVM出现极大促进了前后端分离。
- Model可以看成是代表数据层。对于前端来说它就是后端提供的api接口;
- View代表视图层,负责将数据转化成ui进行展示。对于前端来说主要是html和css;
- ViewMode用来连接Model和View,它将从后端获取的Model数据进行转化,转化成view需要的数据,处理view层的具体业务逻辑。它是MVVM的思想核心,是M和V的调用者,提供了数据的双向绑定。view层它其实展示的是ViewModel层的数据,由ViewModel负责和Model交互,分离了View和Model。降低代码耦合,利用双向绑定,数据更新后视图自动更新,来自动更新DOM。
- 它的不足之处在于bug很难被调试,因为使用了双向数据绑定,从而出现bug时,有可能是view的代码有问题,也有可能是model上的代码出故障,对于大型的图形应用程序来说,视图较多,维护成本偏高。
可以简单的理解View接收用户交互请求,将这个请求转交给ViewModel而ViewModel操作Model进行数据更新,Model更数完数据通知ViewModel数据发生变化后ViewModel更新View数据。
vue的父组件和子组件生命周期钩子函数执行的顺序:
- 加载渲染 父 beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted
- 子组件更新 父beforeUpdate => 子beforeUpdate => 子updated => 父updated
- 父组件更新 父beforeUpdate => 父updated
- 销毁过程 父beforeDestory => 子beforeDestory => 子destoryed => 父 destoryed
五、Vue数据双向绑定
Vue的数据双向绑定即数据变化视图自动更新,视图变化更新数据。它采用数据劫持结合发布者-订阅者模式来实现数据的双向绑定。通过Object.defineProperty劫持data属性的setter/getter。在数据变动时发布消息给订阅者,触发相应的监听回调。
5.1 Object.defineProperty & proxy
vue2.0版本中我们采用Object.defineProperty()方法进行数据劫持。Object.defineProperty()方法会直接在一个对象上定义一个新属性或者是修改一个对象的现在属性并返回这个对象。主要是利用Object.defineProperty中的访问器属性getter和setter方法。当把一个普通对象传入Vue实例作为data选项时,Vue将遍历对象中所有属性,为其添加上访问器属性。当读取data中数据时自动调用getter方法,getter进行依赖收集,当修改data中的数据自动调用setter方法,通知视图更新,利用对象属性中的getter/setter方法来监听数据变化。
let obj={
name:'hello Vue',
category:{
list:'分类一',
list2:'分类二'
}
};
setobj(obj);
function defineProperty(obj,k,val){
if(typeof val =="object"){
setobj(val);
}
Object.defineProperty(obj,k,{
get(){//依赖收集
return val;
},
set(val){ //通知视图更新
val = v;
}
})
}
function setobj(obj){
Object.keys(obj).forEach(k=>{
defineProperty(obj,k,obj[k])
})
}
使用Object.defineProperty()时,它是将对象的key转换成getter/setter形式来跟踪变化,getter/setter它只能跟踪一个数据是否被修改,不能跟踪属性的新增与删除。这时删除属性我们可以用到vm.$delete实现,新增使用Vue.$set来实现。
Object.defineProperty()还无法监听到数组的方法,对于数组而言,在Vue中以下方法push,pop,shift,unshift,splice,sort,reverse是经过了内部处理对数组进行重写来保证响应式的。并且Object.defineProperty()它只能劫持对象的属性,如果属性值也是对象,则需要进行深度遍历。通过查看Vue部分源码可以看到Vue它是通过遍历重写数组和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。 这样非常麻烦,所以vue3.0就开始采用proxy代理来实现数据劫持。Proxy它可以在目标对象之前就回调“拦截”,外界对该对象的访问,都必须先通过这层拦截,它提供了一种机制,可以对外界的访问进行过滤和改写,Proxy是Object.defineProperty的加强版。
Proxy可以实现对对象直接监听(而不是监听对象属性),并且还可以直接监听数组变化。如下所示:
let obj = {
like: 'swam',
age: { age: 20 },
arr: [1, 2, 3, 4]
};
function render() {
console.log('render')
}
let handler = {
get(target, key) {
//判断取的值是否为对象
if (typeof target[key] == 'object' && target[key] !== null) {
return new Proxy(target[key], handler);
}
return Reflect.get(target, key);
},
set(target, key, value) {
if (key === 'length') return true
render();
return Reflect.set(target, key, value)
}
}
let proxy = new Proxy(obj, handler)
proxy.age.name = 'davina' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 "davina"
proxy.arr[0] = '100' //支持数组的内容发生变化
console.log(proxy.arr) //Proxy {0: "100", 1: 2, 2: 3, 3: 4}
从上我们可以看出proxy可以直接监听对象而非属性并且它可以直接监听数组的变化,返回的是一个新对象,我们可以只操作新对象达到目的。而Object.defineProperty只能遍历对象属性直接修改,兼容性很好支持ie9而proxy存在浏览器兼容问题。
当数据属性发生变化时,可以通知那些曾经使用过这个数据的地方数据变化了,那么我们怎么知道曾经使用过这个数据的地方是哪些地方?我们要怎么进行通知?
这时我们就要收集相应的依赖才能知道哪此地方依赖数据,以及数据更新时进行相应更新。这时我们就要用到"发布订阅模式"。
5.2 发布订阅模式
要想了解什么是发布订阅模式我们先要了解什么是观察者模式。观察者模式可以看成定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变,所有依赖于它的对象都会得到通知,并进行相应更新。如下图所示,观察者(observer)它可以直接订阅(subscribe)主题(subject),当主题被激活时,会触发(fire event)观察者里的事件。观察者它一般是提供一个更新的接口,用于当被观察者状态发生变化时,能够及时得到通知,从而作出反应,而对于被观察者而言,它要做到的就是状态发生改变要及时的通知给观察者。
其实观察者模式在之前还有一个别名叫发布订阅模式,但随着项目复杂度代码可维护性提高,它们两个虽然大体方向一致,但还是有很多不同地方,二者并不相等。在实际的开发中我们更多是用到发布-订阅模式。
在现在的发布-订阅模式中,发布者发送的消息不会直接发送给订阅者,而是会通过一个第三方组件中间件来进行联系。发布者和订阅者之间不知道对方的存在,通过中间件来进行联系。能过下图我们可以看到,订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到中间件(event channel),发布者(Publisher)发布事件(event)到中间件,由中间件统一进行调度(fire event)订阅者注册到调度中心的处理代码。
它们二者最大的区别在于发布-订阅模式中有一个事件调用中心即中间件。观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,这样做简单高效,但是会造成代码的冗余。可维护性较差。而关于发布-订阅模式它是由调度中心中间件统一进行处理,订阅者发布者互不干扰,消除了二者的依赖性,实现了解耦。但发布者订阅者对彼此之间存在没有感知有时也会带来相应麻烦。
5.3 vue中的双向绑定
Vue数据的双向绑定通过以下步骤来现实:
1、实现一个监听器Observer
Vue中用Observer来实现监听,用来劫持并监听所有属性,如果属性发生变化就通知给订阅者Watcher。vue2.0中主要用到Object.defineProperty方法对数据对象进行遍历,而vue3.0中用到proxy代理。
/* 监听者*/
function observe(data) {
//简单判断类型
if (typeof data !== 'object') {
return;
}
let keys = Object.keys(data);//key是所有属性名组成的数组
keys.forEach(key => {
defineReactive(data, key, data[key])
})
}
//封装一个defineReactive函数专门调用defineProperty,实现数据劫持
function defineReactive(obj, key, value) {
observe(value);//实现深层劫持
let dep = new Dep;//每一个key都有一个私有变量dep
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);//Dep.target就是watcher实例
}
return value;
},
set(newval) {
if (value !== newval) {
value = newval;
observe(value);
dep.notify();
}
}
})
}
2、实现一个订阅器Dep
订阅器Dep采用发布订阅模式用来收集订阅者Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。简单来说,Dep是存储依赖的地方,它可以用来收集依赖,删除依赖,向依赖发送消息等等。它的主要作用是用来存放watcher观察者对象,我们可以把它看成是一个中介的角色,数据发生变化时会通知它,然后再由它通知到其它地方。当需求收集依赖时,我们可以调用addSub方法,当需求派发更新时我们调用notify方法。
//订阅者
class Dep {
constructor() {
//提供一个事件池
this.subs = [];
}
//增加事件的操作,向事件池里放事件
addSub(sub) {
this.subs.push(sub);
}
//通知更新
notify() {
this.subs.forEach(item => {
// 让对应的事件做更新操作
item.update();
})
}
}
3、实现一个订阅者Watcher
Watcher订阅者是Observer 和 Compile 之间通信桥梁 ,主要任务是订阅 Observer 中属性值变化的消息,当收到属性值变化时,触发解析器 Compile中对应的更新函数。属性发生变化后,我们要通知所有用到这个数据的地方,在一个项目或者是文件中用到这个数据的地方有很多,而且类型不一定相同,这时就需求我们抽象出一个类集中进行处理。我们在收集依赖的阶段只是收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其它地方,这样速度和效率会快很多。依赖收集的目的是将watcher订阅者对象存放到当前的闭包中Dep订阅者的subs下。
/*watcher的简单实现*/
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
// 最后将 Dep.target 置空
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}
4、实现一个解析器Complie
compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
一个完整的双向绑定有以下几点:
1、在new Vue()后利用Proxy或Object.defineProperty方法对对象/对象属性进行"劫持",Vue中的data会通过observe添加上getter/setter属性,来对数据进行追踪变化,当对象被读取时会执行getter函数,而当被赋值时执行setter函数。在属性发生变化后通知订阅者;
2、解析器Compile解析模板中的指令,收集方法和数据,等待数据变化然后渲染;
3、Watcher接收到的Observe产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化。
六、vue中的虚拟DOM
虚拟DOM(Virtral DOM)是Vue核心之一,也就是我们经常提到的虚拟节点,是通过javascript的Object对象模拟DOM节点,然后再通过特定方法将其渲染成真实DOM节点。虚拟DOM它不会进行重排与重绘,并且它可以一次性修改真实DOM需要更改的部分,能尽量减少对真实DOM的操作,这样可以减少页面的回流与重绘大大提升页面性能。
虚拟DOM(vdom)它是树状结构,其节点为vnode,只存在于vdom tree中,通过vnode的elm属性可以访问到对应的真实DOM节点。它其实就是用js对象结构表示树的结构,然后树构建一个真正的DOM树,插入到文本当中去,当状态更新,重新构造一棵新的对象树,新旧树进行对比,然后把差异应用到所构建的真正DOM树上,这样就实现了视图的更新操作。
从虚拟DOM到真实DOM之间要经过一系列流程及步骤。当通过Object.defineProperty中的getter及setter实现对数据的劫持操作后虚拟DOM模拟真实DOM结构 ,通过render算法将虚拟DOM进行解析,渲染成真实的DOM。当数据发生更改会触发setter,进而执行对应的Dep.notify函数,这时又会再次生成虚拟DOM,用diff算法计算出修改的最小单位 ,生成patch补丁,重新渲染真实DOM,但这时只渲染变化的部分,所以渲染速度会快很多。
我们可以看出虚拟DOM它有三个关键点:
- JavaScript对象模拟真实DOM树,对真实DOM进行抽象;
- 使用diff算法比较两棵虚拟DOM树差异;
- patch算法将两个虚拟DOM对象差异应用到真正的DOM树上
七、组件
7.1 组件的分类
组件(Component)是Vue中十分亮眼的部分,组件化是Vue中的重要思想 ,任何应用都是可以被抽象成一颗组件树,这样有利用复用十分灵活。
组件使用分为三步:
- 创建组件构造器对象 调用Vue.extend()方法创建组件构造器,它是在Vue中内置的方法,通常在创建组件构造器时传入template代表我们自定义组件的模板,这个模板就是在使用到组件的地方要显示的html代码如下所示:
// 1.创建组件构造器对象
const cpnC = Vue.extend({
// 模板
template: `
<div>
<h2>你好,李银河</h2>
<h3>我是王小波</h3>
</div>`
})
- 注册组件
- 全局注册:Vue.component(tagName,options)来进行注册。其中tagName是注册组件的标签名,不能是已经存在的html标签,最好用-来表示连接,options组件构造器。在组件注册完成后,它就可以在父实例的模板中以自定义元素的形式来使用。全局注册组件可以用在任何新创建的Vue根实例的模板中。
- 局部注册:它是通过使用组件实例选项components来进行的,可以使组件仅在一个实例或者是组件的作用域中可用。它想在哪个组件中使用,那么就在哪个组件在进行注册。
//全局注册:
Vue.component('my-cpn1-tagName',cpnC);
//局部注册:
const app = new Vue({
el:'#app',
components:{
'my-cpn2-tagName':cpnC
}
})
//现在一般使用语法糖
Vue.component('my-cpn1-tagName',{
template: `
<div>
<h2>你好,李银河</h2>
<h3>我是王小波</h3>
</div>`
});
const app = new Vue({
el:'#app',
components:{
'my-cpn2-tagName':{
template: `
<div>
<h2>你好,李银河</h2>
<h3>我是王小波</h3>
</div>`
}
}
})
- 使用组件:在模板中使用
<div id="app">
<my-cpn2-tagName></my-cpn2-tagName>
</div>
我们可以使用<template>标签将模板进行分离,使得结构更加清楚明了,如下所示:
<template id="cpn1">
<div>
<h2>你好,李银河</h2>
<h3>我是王小波</h3>
</div>`
</template>
全局:
Vue.component('my-cpn1-tagName','#cpn1');
局部:
const app = new Vue({
el:'#app',
components:{
'my-cpn2-tagName':{
template: '#cpn1'
}
}
})
一般来说我们在vue组件对象中,通过data来进行数据传递。当一个组件被定义时,data需要声明为返回一个初始对象的函数。
//data
data(){
return {
message:""
}
}
// new Vue
new Vue({
el:"#app",
data{
message:""
}
})
因为组件可能用来创建多个实例,它是用来复用的。如果data还像原来一样仅仅是作为一个对象那所有的实例都将使用这一个数据对象,js对象是引用关系,作用域并没有隔离。子组件中data属性值会相互影响。但如果data是一个函数,它就可以保证数据独立性,每次创建一个实例后,能够调用data函数,那每个实例可以维护一份被返回对象的独立的拷贝,实例之间data属性值不会相互影响。
7.2 组件的通信
Vue中的通信方式有以下三类:父子组件通信,兄弟组件通信,隔代组件通信。
7.2.1 props验证和$emit
父组件通过自定义属性和props向子组件传递数据,子组件向父组件传递数据或父组件想使用子组件里的数据或者方法时,可以用到自定义事件和$emit。如下所示:
App组件:
<template>
<div>
<child :fatherToChild="message" @btnClick="btnClick"></child>
<button v-for="item in data" :key="item.id">{{item.name}}</button>
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "father",
data() {
return {
message: "我是王小波",
data:{},
};
},
components: {
Child,
},
methods: {
btnClick(value){
this.data = value
}
},
};
</script>
child组件:
<template>
<div>
<h2>{{ fatherToChild }}</h2>
<button @click="btnClick">btn</button>
</div>
</template>
<script>
export default {
name: "child",
//子组件的porps
props: {
fatherToChild: {
type: String,
required: false,
default: "hello world",
},
},
data() {
return {
mes: "子组件里的数据",
categories: [
{ id: "0001", name: "热门推荐" },
{ id: "0002", name: "手机数码" },
],
};
},
methods: {
btnClick(){
//发射一个自定义事件,父组件好接收
this.$emit('btnClick' ,this.categories)
}
},
};
</script>
prop是单向下行绑定,是单向数据流。也就是说父组件的prop变化时,将传递给子组件,但是不应该反过来。这样可以防止从子组件意外改变父级组件的状态。每当父组件进行更新时,它可以通过自定义属性和prop来向子组件传递数据,子组件的prop都会更新为最新值,这意味着不应该在子组件的内部改变prop,如果实在是想修改那么也应该是子组件通过$emit向父组件派发一个自定义事件,父组件接收后,由父组件修改。
7.2.2 ref 和$parent与$children
父访问子可以通过:$ref或者是$children
ref它如果在普通DOM元素上使用指向的就是DOM元素,如果用在子组件上引用指向组件实例。ref的作用有以下三点:
- this.$refs.box 获取dom元素
- this.$refs.box.msg 获取子组件中的data
- this.$refs.box.open() 调用子组件中的方法
父组件:
<template>
<div>
<button @click="fatherToSon">父操作子:btn</button>
<child ref="son" />
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "father",
data() {
return {};
},
components: {
Child,
},
methods: {
fatherToSon() {
this.$refs.son.showMessage();
},
},
};
</script>
子组件:
<template>
<div ref="box"></div>
</template>
<script>
export default {
name: "child",
data() {
return {
mes: "通过父组件中的btn按钮点击进行显示",
};
},
methods: {
showMessage() {
this.$refs.box.innerHTML = this.mes;
},
},
};
</script>
$children是一个数组类型,根据索引可以找到对应的子组件。就$children来说,它即不是响应式的,不能保证顺序。$parent获取当前组件的父组件,$root获取根组件。一般这三者都不太常用。
父组件:
<template>
<div>
<button @click="fatherToSon">父操作子:btn</button>
<child />
<child />
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "father",
data() {
return {
mes: "新的消息",
};
},
components: {
Child,
},
methods: {
fatherToSon() {
//父操作子:
this.$children[1].$el.innerHTML = this.mes;
},
},
};
</script>
子组件:
<template>
<div>
<div>{{ mes }}</div>
<button @click="sonToFather">子组件的btn</button>
</div>
</template>
<script>
export default {
name: "child",
data() {
return {
mes: "通过父组件中的btn按钮点击进行显示",
};
},
methods: {
sonToFather() {
//访问父组件 并调用其中的方法
console.log(this.$parent.fatherToSon());
},
},
};
</script>
7.2.3 $attr & $listeners
$attrs:包含了父作用域中不被prop所识别的特性(class和style除外)一个组件没有声明任何prop时,这里会包含所有父作用域的绑定,并且可以通过v-bind:='$attrs'传入内部组件,通常配合inheritAttrs选项一起使用。
$listeners:包含了父组件中的v-o事件监听器,它可以通过v-on="$listeners"传入内部组件
7.2.4 provide & inject
祖先元素中通过provide来提供变量,那么它所有的后代组件都可以通过inject获取provide提供的属性了。它主要解决的是跨组件之间的通信问题。
7.2.5 EventBus
这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
7.2.6 vuex
当需要通信的数据过多过大时,我们可以用vuex来处理数据。
7.3 slot
组件的插槽是为了让我们封装的组件更加具有扩展性。它用于决定将所携带的内容,插入到指定位置。让使用者可以决定组件内部的一些内容到底展示什么。
- 匿名slot:它也叫默认slot。当子组件模板只有一个没有任何属性的slot时,父组件整个内容片断将插入到slot所在的位置并且替换slot标签,它可以放在子组件的任意位置。如下所示:
App父组件
<template>
<div>
<h4>这里是父组件,里面有一个子组件使用多次</h4>
<child>
<h3>{{ mes }}</h3>
<h4>内容都会被插入</h4>
</child>
<child></child>
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "father",
data() {
return {
mes: "新的消息",
};
},
components: {
Child,
},
methods: {},
};
</script>
子组件
<template>
<div>
<div>{{ mes }}</div>
<!-- 这是匿名slot -->
<slot>
<!-- 当父组件中有替代的内容就把slot替换,没有的话就默认使用slot中的内容替换slot
-->
<p>父组件中没有插入内容时,我就是默认内容</p>
</slot>
</div>
</template>
<script>
export default {
name: "child",
data() {
return {
mes: "我是子组件",
};
},
methods: {},
};
</script>
- 具名slot:当有多个slot时,我们可以使用name属性给不同的slot配置插入哪些内容。具名slot将匹配内容片断时有对应slot特性的元素。也可以用v-slot来进行操作。
v-slot:插槽名等同于<标签名 slot ="插槽名">它只能用在template上。
App
<template>
<div>
<h4>这里是父组件</h4>
<child>
<span slot="center">我想替换中间的</span>
<span>其它内容</span>
</child>
<child>
<template v-slot:center>
<span>中间的内容</span>
</template>
</child>
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "father",
data() {
return {
mes: "新的消息",
};
},
components: {
Child,
},
methods: {},
};
</script>
子组件:
<template>
<div>
<div>{{ mes }}</div>
<slot name="left"><span>left</span></slot>
<slot name="center"><span>center</span></slot>
<slot name="right"><span>right</span></slot>
</div>
</template>
<script>
export default {
name: "child",
data() {
return {
mes: "我是子组件",
};
},
methods: {},
};
</script>
<style scoped>
span {
padding: 0 10px;
}
</style>
- 作用域插槽:它是一种特殊的可以带数据的插槽。理解作用域插槽前我们先要理解什么是编译作用域,编译作用域简单来说可以看成父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译,而作用域插槽可以让父组件替换插槽的标签,但是内容不是由子组件来提供。
App父组件
<template>
<div>
<h4>这里是父组件</h4>
<child></child>
<child>
<!-- 老版本:slot-scope后的名字可以随意 -->
<template slot-scope="slot">
<span>{{ slot.data.join(" - ") }}</span>
</template>
</child>
<child>
<!-- 新版本 -->
<template #default="slot">
<span>{{ slot.data.join("*") }}</span>
</template>
</child>
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "father",
data() {
return {
mes: "新的消息",
};
},
components: {
Child,
},
methods: {},
};
</script>
子组件:
<template>
<div>
<div>{{ mes }}</div>
<!-- 名字可以随意-->
<slot :data="data">
<ul>
<li v-for="item in data" :key="item">{{ item }}</li>
</ul>
</slot>
</div>
</template>
<script>
export default {
name: "child",
data() {
return {
mes: "我是子组件",
data: ["vue", "react", "angular"],
};
},
methods: {},
};
</script>
<style scoped>
span {
padding: 0 10px;
}
</style>
八、什么是SPA?
SPA(single-page application)仅在 Web页面初始化时加载相应的HTML、JavaScript 和 CSS。一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
- 用户体验好内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面时候JavaScript、CSS 统一加载,部分页面按需加载;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO优化上其有着天然弱势。 补充:
- 后端渲染:早期网站的开发整个html页面是由服务器来渲染,服务器直接生产渲染好对应的html页面,返回给客户端进行展示,这种情况下渲染好的页面不需要单独加载任何的js和css是可以直接交给浏览器展示,有利用于seo优化但是不利于编写和维护对服务器的压力很大。
- 前端渲染:通过ajax进行前后端分离,后期只负责提供数据,不提供任何界面的内容。浏览器中显示的网页中的大部分内容,都是由前端写的js代码在浏览器中执行,最终渲染出来的网页。这样做前后端责任划分清晰,后端专注于数据,前端专注于交互和可视化。
- SPA:单页面富应用
九、Vue-router
路由就是通过互联的网络把信息从源地址传输到目的地址的活动,路由本质上就是一个映射表,它决定数据包从来源到目的地的路径和转送输入端的数据转换到合适的输出端。
9.1 路由模式
vue-router有三种路由模式:hash、history、abstract。
- hash模式
早期的前端路由是
location.hash来实现。location.hash的值就是URL中#后面的内容。hash值只是客户端的一种状态,当我们用location.hash来修改hash值时不会向后端发送请求,即不会进行页面的刷新,hash部分也不会发送到后端。而我们每一次修改hash都会在浏览器的历史记录中增加一个记录(或者使用history.pushState()增加)所以我们可以通过浏览器的回退,前进来控制hash切换。一般可以可以通过history.go(),history.forward()对页面进行跳转, 使用hashchange事件来监听hash值变化,从而对页面进行跳转(渲染)。 使用hash模式兼容性较好,支持所有浏览器。 - history模式
它主要依赖HTML5中的api
history.pushState()和history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。它们都可以操作url,从而对页面进行跳转,我们可以通过popstate事件来监听url的变化. - abstract模式 它支持所有的JS运行环境,如果发现不存在浏览器的api,会自动进入到这个模式。
9.2 路由使用
搭建:
- 安装与引入
- 通过Vue.use()安装插件
- 创建VueRouter对象导出传到Vue实例中使用
// 1. 安装与导入:npm install vue-router(只考虑模块化)
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/study/Home.vue'
import Home from '../views/study/User.vue'
// 2. 安装插件
Vue.use(VueRouter)
// 3. 创建VueRouter对象
const routes = [{
path: '/',
//重定向
redirect: '/home'
},
{
path:'/home',
name:'Home',
component:Home,
//路由嵌套
children:[
{
path:'detail',
name:'User',
component:User
}
]
}]
const router = new VueRouter({
//配置路由和组件之间的关系
routes,
//默认是hash模式是有#的,可以在这里用mode里的history模式
mode: 'history',
linkActiveClass: 'active',
base: process.env.BASE_URL,
})
//导出在实例中使用
// 导出:
export default router
// 使用:main.js
new Vue({
router,
render: h => h(App)
}).$mount('#app')
使用vue-router的步骤:
- 创建路由组件
- 配置路由映射
- 使用路由。通过
<router-link>和<router-view><router-link>: 它是vue-router内置的组件,会被渲染成一个a标签<router-link>还有一些其他属性:<router-link to='/home' tag='li' replace>to:用于指定跳转的路径tag: tag可以指定<router-link>之后渲染成什么标签,上面的代码会被渲染成一个li而不是a标签;replace: replace不会留下history记录,所以指定replace的情况下,后退键返回不能返回到上一个页面中;active-class:当对应的路由匹配成功时,会自动给当前元素设置一个router-link-active的class,设置active-class可以修改默认的名称。
<router-view>:它也是一个内置的组件,它会根据当前的路径动态渲染出不同组件,网页的其他内容,比如顶部的标题/导航,或者底部的一些版权信息会和处于同一个等级。在路由切换时,切换的是挂载的组件,其他内容不会发生改变。- 也可以通过代码方式修改路由:
this.$router.push() 或者是this.$router.replace()。router对象是全局路由器的VueRouter实例,包括了路由的跳转方法,钩子函数等。route对象表示当前的路由信息,哪个路由处于 活跃状态它就是哪一个路由。包含了当前 URL 解析得到的信息。包含当前的路径、参数、query对象等。
9.3 动态路由 & 传递参数
我们有时需要把某种模式匹配到所有路由,全部映射到同一个组件,一般用于动态路由传参。
params的类型:
- 配置路由格式:/about/:id
- 传递方法:在path后面跟上对应的值
- 传递形成的路径:/about/0001
路由配置
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头 id是一个动态值
{ path: '/about/:id', component: About }
]
})
App组件:
<!-- params传参:动态路由 -->
<router-link :to="'/about/' + userData.userId">about</router-link>
<h3>{{$route.params.id}}</h3>
query类型:
- 配置路由格式:/about也就是说普通的格式
- 传递方法:对象中使用query的key作为传递方式
- 传递后形成的路径:/about?id=0002
路由配置
const router = new VueRouter({
routes: [{
//使用query传参 这里还是普通配置
path: '/about',
name: 'About',
component: About,
}]
})
App组件:
<!-- params传参:使用v-bind和对象 -->
<router-link :to="{ path: '/about', query: { id: '002', name: 'amy' } }">about</router-link>
<h3>{{$route.query.id}}</h3>
9.4 路由的懒加载
路由懒加载就是将不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应的组件,这样webpack打包时js文件不会过大造成用户进入首页等待时间过大,出现白屏效果。它其实就是按需加载,有效的分担了首页所承担的压力,可以减少首页加载用时。使用如下所示:
const router = new VueRouter({
routes: [
{ path: '/home', component: () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') }
]
})
9.5 导航守卫
导航守卫可以在路由跳转前做一些必要的验证,如登录验证。导航守卫有:
- 全局路由守卫:可以用于用户登录的检查和验证
- 全局前置守卫
router.beforeEach - 全局后置钩子
router.afterEach
- 全局前置守卫
- 路由独享守卫
beforeEnter:在路由配置上使用 - 组件内自己的守卫:它可以直接在组件内像生命周期一样使用,如在用户离开当前界面时询问某些信息
beforeRouteEnter,beforeRouteUpdate (2.2 新增)beforeRouteLeave。
//验证是否登录
// 从from到to next()一定要调用这个方法来resolve这个钩子
// 确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错
router.beforeEach((to, from, next) => {
if (to.path === '/login') return next();
let str = localStorage.getItem('user')
if (!str) return next('/login')
next()
})
9.6 keep-alive
keep-alive是Vue内置的一个组件,它不会生成真正的dom节点,可以使被包含的组件保留状态,缓存不活动的组件实例,避免重新渲染。
- 一般结合路由和动态组件一起使用,用于缓存组件;
- 有include 和 exclude 属性,两者都支持字符串或正则表达式,(定义缓存白名单,keep-alive会缓存命中的组件),exclude(黑名单,不会被缓存),其中exclude的优先级比include高;
- 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
十、vuex
Vuex它是专门为Vue.js应用程序设计的状态管理工具。采用集中式存储管理应用的所有组件状态,并以相应的规则保证状态以一种可预测的方式发生变化。通俗来讲当有很多组件,组件与组件之间需要进行相互通信,这时就可以用到Vuex保存多个组件的共享状态,这时只需求state就可以在组件之间同步状态了。
vuex的特点是把数据单独的隔离形成一棵树状图,单独隔离这就说明它有自己的生态系统,其中action作为数据输入,state作为数据输出。只能在mutations里修改state,actions不能直接修改state。在mutations中修改state的数据,它只能是同步操作,不能存在异步的操作(但如果是异步也不会报错,只是不建议)。如果是异步的操作,那可以把操作放在action中,拿到数据再通过mutations同步进行处理。
每一个vuex应用的核心就是store(仓库)。store它就是一个容器,包含着应用中大部分的state(状态)。vuex的状态存储是响应式的,当vue组件从store中读取state,state发生变化 ,那相应的组件也会变化更新。不能直接的改变仓库中的state,改变的唯一方法就是显示的提交mutation。这样可以方便跟踪每一个状态的变化。
主要包含以下几个模块:
- state(数据):定义了应用状态的数据结构,可以在这里设置默认的初始状态。每个应用将仅仅包含一个store实例。它其实就是一个公共状态;
- getter(数据加工):是store的计算属性。有时候需求从store中的state中派生出一些状态,所以要用到getter,getter返回的值会根据依赖被缓存起来,只有当它的依赖值发生改变才会被重新计算。使用方法是 $store.getters.属性名。它可带参数,也可以不带参数,接受state为其第一个参数,也可以接受其它getter作为第二个参数。可以看成是对数据源进行加工,返回需要的数据;
- mutation(执行):更改vuex中store状态的唯一方法commit(mutation)。vuex中的mutation类似于事件,每个mutation都有一个事件类型(type)和一个回调函数(handler),这个回调函数就是实际进行状态更改的地方,可以看成是{type:handler()},调用type时用到store.commit方法。mutation必须是同步的,这是为了调试方便。在项目中使用时,一般将常量放在单独的文件中,这样更有助于协作开发,提高效率。
- action(事件):是为异步操作而设置的。因为mutation只能是同步操作,但在实际的项目中会存在异步操作,所以actions它是要执行的操作,可以进行同步也可以时行异步
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
所以从上面来看简单来说,vuex的工作流程就是:
- 通过dispatch去提交一个actions
- actions接收到这个事件后,在actions中可以执行一些异步或者同步的操作根据不同的情去分发给不同的mutations
- actions通过commit去触发mutations;
- mutations去更新state中的数据,state更新后,就会通过vue进行渲染