本系列文章是对vue2.x的知识进行系统梳理,也算是一个回顾。
本系列只针对基础知识,难度较浅,如有错误,欢迎大佬指正,不胜感激!!!
组件
使用组件
Vue中使用组件的三大步骤:
一、定义组件
- 使用
Vue.extend(options)
创建,其中options
和new Vue(options)
时传入的几乎一样,但有如下区别:- el不需要写,因为所有的组件都要经过一个
vm
的管理,由vm
中的el
决定服务哪个容器。 - data必须写成函数, 避免组件被复用时,数据存在引用关系。
- 使用template配置组件结构。
- el不需要写,因为所有的组件都要经过一个
二、注册组件
- 局部注册:
new Vue()
的时候传入components
选项 - 全局注册:使用
Vue.component('组件名',组件)
三、使用组件(写组件标签):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>第一个组件</title>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<hello></hello>
<hr>
<!-- 编写组件标签 -->
<home></home>
</div>
</body>
<script type="text/javascript">
// 创建home组件
const home = Vue.extend({
template:`
<div>
<h2>名称:{{name}}</h2>
<h2>地址:{{address}}</h2>
<button @click="showName">点我提示名称</button>
</div>
`,
data(){
return {
name:'理发店',
address:'小鸡岛'
}
},
methods: {
showName(){
alert(this.name)
}
},
})
// 创建hello组件
const hello = Vue.extend({
template:`
<div>
<h2>你好啊!{{name}}</h2>
</div>
`,
data(){
return {
name:'伍六七'
}
}
})
// 全局注册组件
Vue.component('hello',hello)
// 创建vm
new Vue({
el:'#root',
// 注册组件(局部注册)
components:{
home
}
})
</script>
</html>
几个注意点
1.关于组件名:
- 一个单词组成:
- 第一种写法(首字母小写):home
- 第二种写法(首字母大写):Home
- 多个单词组成:
- 第一种写法(kebab-case命名):my-home
- 第二种写法(CamelCase命名):MyHome(需要Vue脚手架支持)
- 备注:
- (1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
- (2).可以使用name配置项指定组件在开发者工具中呈现的名字。
2.关于组件标签:
- 第一种写法:
<home></home>
- 第二种写法:
<home/>
3.简写方式:const home = Vue.extend(options)
可简写为:const home = options
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>第一个组件</title>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<home></home>
</div>
</body>
<script type="text/javascript">
// 创建home组件
const home = {
template:`
<div>
<h2>名称:{{name}}</h2>
<h2>地址:{{address}}</h2>
<button @click="showName">点我提示名称</button>
</div>
`,
data(){
return {
name:'理发店',
address:'小鸡岛'
}
},
methods: {
showName(){
alert(this.name)
}
},
}
// 创建vm
new Vue({
el:'#root',
// 注册组件(局部注册)
components:{
home
}
})
</script>
</html>
关于VueComponent:
home
组件本质是一个名为VueComponent的构造函数,不是程序员定义的,是Vue.extend
生成的。- 我们只需要写
<home/>
或<home></home>
,Vue解析时会帮我们创建home组件的实例对象,即Vue帮我们执行的:new VueComponent(options)
。 - 每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!
- 关于this指向:
- (1).组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数,它们的this均是
VueComponent实例对象
- (2).new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数,它们的this均是
Vue实例对象
。
VueComponent
的实例对象,简称vc
(也可称之为:组件实例对象)。Vue的实例对象,简称vm
。
重要的内置关系:
- 重要的内置关系:
VueComponent.prototype.__proto__ === Vue.prototype
- 为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。
Render函数和Template
在平时编程时,大部分是通过template
来创建html。但是在一些特殊的情况下,使用template方式时,就无法很好的满足需求,在这个时候就需要 通过JavaScript 的编程能力来进行操作。此时,就到了render
函数展示拳脚去时候了。
render函数
render函数的作用是,当场景中用 template 实现起来代码冗长繁琐而且有大量重复,这个时候使用就可以极大的简化代码。 在使用render函数中,会使用到一个参数createElement,而这个createElement参数,本质上,也是一个函数,是vue中构建虚拟dom所使用的工具。
在createElement(,{},[])
方法中,有三个参数:
- 第一个参数(必要参数):主要是用于提供dom中的html内容,类型可以是字符串、对象或函数。
- 第二个参数(对象类型,可选):用于设置这个dom中的一些样式、属性、传的组件的参数、绑定事件之类的。
- 第三个参数(类型是数组,数组元素类型是VNode,可选):主要用于设置分发的内容,如新增的其他组件。 注意:组件树中的所有vnode必须是唯一的 通过传入createElement参数,创建虚拟节点,然后再将节点返回给render返回出去。总的来说,render函数的本质就是创建一个虚拟节点。
render和template的区别
相同之处:
- render 函数 跟 template 一样都是创建 html 模板
不同之处:
- Template适合逻辑简单,render适合复杂逻辑。
- 使用者template理解起来相对容易,但灵活性不足;自定义render函数灵活性高,但对使用者要求较高。
- render的性能较高,template性能较低。
- 使用render函数渲染没有编译过程,相当于使用者直接将代码给程序。所以,使用它对使用者要求高,且易出现错误
- Render 函数的优先级要比template的级别要高,但是要注意的是Mustache(双花括号)语法就不能再次使用
注意:template和render不能一起使用,否则无效
render举例
如一次封装一套通用按钮组件,按钮有四个样式(success、error、warning、default)。 template方式是如下:
<div class="btn btn-success" v-if="type === 'success'">{{text}}<div>
<div class="btn btn-error" v-else-if="type === 'error'">{{text}}<div>
<div class="btn btn-warning" v-else-if="type === 'warning'">{{text}}<div>
<div class="btn btn-default" v-else="type === 'default'">{{text}}<div>
这样写在按钮少的时候没有问题,但是一旦按钮数量变多,这样写就会显得特别冗长,在这个时候,就需要render函数。 在使用render函数前,需要先把template标签去掉,只保留逻辑层。 通过传入的type动态填入class,通过inderText将内容添加入DOM中。
render(h){
return h('div', {
class: {
btn: true,
'btn-success': this.type === 'success',
'btn-error': this.type === 'error',
'btn-warning': this.type === 'warning',
'btn-default': this.type === 'default',
},
domProps: {
innerText: this.text
},
on: {
click: this.handleClick
}
})
}
ref属性
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
<Home ref="home"/>
</div>
</template>
<script>
// 引入Home组件
import Home from './components/Home'
export default {
name:'App',
components:{School},
data() {
return {
msg:'我是伍六七!'
}
},
methods: {
showDOM(){
console.log(this.$refs.title) //真实DOM元素
console.log(this.$refs.btn) //真实DOM元素
console.log(this.$refs.home) //Home组件的实例对象(vc)
}
},
}
</script>
props配置
父组件向子组件传值
<!--父组件-->
<template>
<div>
<Home name="伍六七" sex="男" :age="18"/>
</div>
</template>
<script>
import Home from './components/Home'
export default {
name:'App',
components:{Home}
}
</script>
<!--子组件-->
<template>
<div>
<h1>{{msg}}</h1>
<h2>姓名:{{name}}</h2>
<h2>性别:{{sex}}</h2>
<h2>年龄:{{myAge}}</h2>
<button @click="updateAge">修改年龄</button>
</div>
</template>
<script>
export default {
name:'Home',
data() {
return {
msg:'我是小鸡岛居民',
myAge: this.age
}
},
methods: {
updateAge(){
this.myAge++
}
},
// 写法一:简单声明接收
// props:['name','age','sex']
// 写法二:接收的同时对数据进行类型限制
/*
props:{
name:String,
age:Number,
sex:String
}
*/
// 写法三:接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
props:{
name:{
type: String, //name的类型是字符串
required: true, //name是必要的
},
age:{
type: Number,
default: 10 //默认值
},
sex:{
type: String,
required: true
}
}
}
</script>
minins混入
混入 (mixins): 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
混入策略:
- 数据对象
data
在内部会进行浅合并 (一层属性深度),在和组件的数据发生冲突时以组件数据优先。 - 同名钩子函数将混合为一个数组,因此都将被调用。另外,
混入对象的钩子
将在组件自身钩子
之前调用。 - 值为对象的选项,例如
methods
,components
,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。
与vuex的区别
- vuex:用来做状态管理的,里面定义的变量在每个组件中均可以使用和修改,在任一组件中修改此变量的值之后,其他组件中此变量的值也会随之修改。
- Mixins:可以定义共用的变量,在每个组件中使用,引入组件中之后,各个变量是相互独立的,值的修改在组件中不会相互影响。
与公共组件的区别
- 公共组件:在父组件中引入组件,相当于在父组件中给出一片独立的空间供子组件使用,然后根据props来传值,但本质上两者是相对独立的。
- Mixins:则是在引入组件之后与组件中的对象和方法进行合并,相当于扩展了父组件的对象与方法,可以理解为形成了一个新的组件。
插件
定义插件
// plugin.js
export default {
install(Vue,x,y,z){
console.log(x,y,z)
//全局过滤器
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
}
})
//给Vue原型上添加一个方法(vm和vc就都能用了)
Vue.prototype.hello = ()=>{alert('你好啊')}
}
}
应用插件
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//引入插件
import plugins from './plugins'
//应用(使用)插件
Vue.use(plugins,1,2,3)
//创建vm
new Vue({
el:'#app',
render: h => h(App)
})
使用插件功能
<template>
<div>
<h2>名称:{{name | mySlice}}</h2>
<h2>地址:{{address}}</h2>
<input type="text" v-fbind:value="name">
<button @click="test">点我测试一个插件的hello方法</button>
</div>
</template>
<script>
export default {
name:'home',
data() {
return {
name:'理发店567',
address:'小鸡岛',
}
},
methods: {
test(){
this.hello()
}
},
}
</script>
scoped样式
**scoped的作用功能:**组件样式私有化,不对全局造成样式污染,表示当前style属性只属于当前模块。 原理 通过观察DOM结构可以发现:vue通过在DOM结构以及css样式上添加唯一的标记,确保唯一,达到样式私有化,不污染全局的作用。样式属性上也会多一个该字符,确保唯一。
<button data-v-4780f187 type="button" name="button">添加<button>
<button data-v-4780f187 type="button" name="button">修改<button>
<button data-v-4780f187 type="button" name="button">删除<button>
可以看出,添加scoped后的组件里的标签都会多一个data-v-4780f187的属性,并且在css样式部分可以看出来:
button[data-v-4780f187]{
background: red
}
由此可知,添加scoped属性的组件,为了达到不污染全局,做了如下处理:
- 给HTML的DOM节点加一个不重复属性
data-v-4780f187
标志唯一性 - 在添加scoped属性的组件的每个样式选择器后添加一个等同"不重复属性"相同的字段,实现类似于"作用域"的作用,不影响全局。
- 如果组件内部还有组件,只会给最外层的组件里的标签加上唯一属性字段,不影响组件内部引用的组件。
慎用原因:
从原理可见,之所以scoped可达到类似组件私有化、样式设置"作用域"的效果,其实只是在设置scoped属性的组件上的所有标签添加data开头的属性,且在标签选择器的结尾加上和属性相同的字段,起到唯一性的作用,但是这样如果组件中也引用其他组件就会出现类似下面的问题:
- 父组件无scoped属性,子组件带有scoped,父组件是无法操作子组件的样式的,虽然我们可以在全局中通过该类标签的标签选择器设置样式,但会影响到其他组件
- 父组件有scoped属性,子组件没有。父组件也无法设置子组件样式,因为父组件的所有标签都带有data-v-469af010唯一标志,但子组件不会带有这个唯一标志属性,与1同理,虽然我们可以在全局中通过该类标签的标签选择器设置样式,但会影响到其他组件
- 父子组件都有,同理也无法设置样式,更改起来增加代码质量。
自定义事件(子组件向父组件传递数据)
方式一:通过父组件给子组件传递函数类型的props实现:子给父传递数据
<!--父组件-->
<template>
<div class="app">
<h1>我是:{{name}}</h1>
<!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
<Home :getAddress="getAddress"/>
</div>
</template>
<script>
import Home from './components/Home'
export default {
name:'App',
components:{Home},
data() {
return {
name:'伍六七'
}
},
methods: {
getAddress(addr){
console.log('收到了地址:',addr)
}
}
}
</script>
<!--子组件-->
<template>
<div>
<h2>地址:{{address}}</h2>
<button @click="sendAddr">给父组件发送地址</button>
</div>
</template>
<script>
export default {
name:'home',
props:['getAddress'],
data() {
return {
address:'玄武国'
}
},
methods: {
sendAddr(){
this.getAddress(this.address)
}
},
}
</script>
方法二:通过父组件给子组件绑定一个自定义事件实现:
<!--父组件-->
<template>
<div class="app">
<h1>我是:{{name}}</h1>
<Home @getAddress="getAddress"/>
</div>
</template>
<script>
import Home from './components/Home'
export default {
name:'App',
components:{Home},
data() {
return {
name:'伍六七'
}
},
methods: {
getAddress(addr){
console.log('收到了地址:',addr)
}
}
}
</script>
<!--子组件-->
<template>
<div>
<h2>地址:{{address}}</h2>
<button @click="sendAddr">给父组件发送地址</button>
<button @click="unbind">解绑getAddress事件</button>
<button @click="death">销毁当前Home组件的实例(vc)</button>
</div>
</template>
<script>
export default {
name:'home',
data() {
return {
address:'玄武国'
}
},
methods: {
sendAddr(){
this.$emit('getAddress',this.address)
},
unbind(){
// 解绑一个自定义事件
// 传字符串解绑一个,传数组解绑多个,不传解绑所有
this.$off('getAddress')
},
death(){
// 销毁了当前Home组件的实例,销毁后所有Home实例的自定义事件全都不奏效。
this.$destroy()
}
},
}
</script>
方法三:通过父组件给子组件绑定一个自定义事件实现:子给父传递数据
<!--父组件-->
<template>
<div class="app">
<h1>我是:{{name}}</h1>
<Home ref='home' @getAddress="getAddress"/>
</div>
</template>
<script>
import Home from './components/Home'
export default {
name:'App',
components:{Home},
data() {
return {
name:'伍六七'
}
},
mounted() {
this.$refs.home.$on('getAddress',this.getAddress) //绑定自定义事件
},
methods: {
getAddress(addr){
console.log('收到了地址:',addr)
}
}
}
</script>
<!--子组件-->
<template>
<div>
<h2>地址:{{address}}</h2>
<button @click="sendAddr">给父组件发送地址</button>
<button @click="unbind">解绑getAddress事件</button>
<button @click="death">销毁当前Home组件的实例(vc)</button>
</div>
</template>
<script>
export default {
name:'home',
data() {
return {
address:'玄武国'
}
},
methods: {
sendAddr(){
this.$emit('getAddress',this.address)
},
unbind(){
// 解绑一个自定义事件
// 传字符串解绑一个,传数组解绑多个,不传解绑所有
this.$off('getAddress')
},
death(){
// 销毁了当前Home组件的实例,销毁后所有Home实例的自定义事件全都不奏效。
this.$destroy()
}
},
}
</script>
全局事件总线($bus-组件间通信)
事件总线$bus
主要使用vue高级APIvm.$on
原理,例:
// main.js
Vue.prototype.$bus = new Vue(); // $bus是原型对象上的实例
// child1
this.$bus.$on('foo', handle) //子组件通过$bus监听事件
// child2
this.$bus.$emit('foo') //子组件通过$emit触发事件
以上写法等同于以下写法:
class Bus {
constructor() {
this.callbacks = {}
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach(cb => cb(args))
}
}
}
// main.js
Vue.prototype.$bus = new Bus()
// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')
v-on(@)与$on的区别
v-on:
- 可监听普通dom的原生事件;
- 可监听子组件emit的自定义事件; vm.$on:
- 监听当前实例的自定义事件
$nextTick
在vue
中有nextTick
这个API,官方解释,它可以在DOM
更新完毕之后(页面渲染之前)执行一个回调。
作用:
- Vue是异步渲染的框架。
- data改变之后,DOM不会立刻渲染。
- $nextTick会在DOM修改之后被触发,以获取最新的DOM节点。
- 连续多次的异步渲染,$nextTick只会执行最后一次渲染后的结果。
原理:
$nextTick
主要通过事件循环中的任务队列的方式异步执行传入的回调函数,首先会判断当前的执行环境是否支持Promise,MutationObserver,setImmediate,setTimeout
。如果支持则创建对应的异步方法,这里的MutationObserver并不是监听DOM,而是利用其微任务特性。需要注意的是更新DOM的方法也是通过nextTick进行调用的,因此就可以实现传入$.nextTick的回调函数在DOM渲染完成之后执行这些微任务。
Vue处于性能考虑,Vue会将用户同步修改的多次数据缓存起来,等同步代码执行完,说明这一次的数据修改就结束了,然后才会去更新对应DOM,一方面可以省去不必要的DOM操作,比如同时修改一个数据多次,只需要关心最后一次就好了,另一方面可以将DOM操作聚集,提升render性能。
涉及到微任务(microtask
)和宏任务(macrotask
)
microtask有:Promise、MutationObserver,以及nodejs中的process.nextTick
macrotask有:setTimeout, setInterval, setImmediate, I/O, UI rendering
插槽(slot)
默认插槽
<!--定义插槽-->
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
<slot>我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title']
}
</script>
<!--使用插槽-->
<template>
<div class="container">
<Category title="美食" >
<img src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
</Category>
<Category title="游戏" >
<ul>
<li v-for="(g,index) in games" :key="index">{{g}}</li>
</ul>
</Category>
<Category title="电影">
<video controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
data() {
return {
foods:['火锅','烧烤','小龙虾','牛排'],
games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
films:['《教父》','《拆弹专家》','《你好,李焕英》']
}
},
}
</script>
具名插槽
<!--定义插槽-->
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
<slot name="center">我是一些默认值,当使用者没有传递具体结构时,我会出现1</slot>
<slot name="footer">我是一些默认值,当使用者没有传递具体结构时,我会出现2</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title']
}
</script>
<!--使用插槽-->
<template>
<div class="container">
<Category title="美食" >
<img slot="center" src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
</Category>
<Category title="游戏" >
<ul slot="center">
<li v-for="(g,index) in games" :key="index">{{g}}</li>
</ul>
<div class="foot" slot="footer">
<a href="http://www.atguigu.com">单机游戏</a>
<a href="http://www.atguigu.com">网络游戏</a>
</div>
</Category>
<Category title="电影">
<video slot="center" controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
<!--完整写法
<template v-slot:footer>
-->
<!--简写-->
<template #footer>
<div class="foot">
<a href="http://www.atguigu.com">经典</a>
<a href="http://www.atguigu.com">热门</a>
<a href="http://www.atguigu.com">推荐</a>
</div>
<h4>欢迎前来观影</h4>
</template>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
data() {
return {
foods:['火锅','烧烤','小龙虾','牛排'],
games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
films:['《教父》','《拆弹专家》','《你好,李焕英》']
}
},
}
</script>
作用域插槽
为预留的 <slot>
插槽绑定props 数据,这种带有props 数据的 <slot>
叫做“作用域插槽”
slot-scope(2.6版本之前)
v-slot(2.6版本之后)
<!--
定义插槽
插槽向外传递数据games,由插槽使用者处理
-->
<template>
<div class="category">
<h3>{{title}}分类</h3>
<slot :games="games" msg="hello">我是默认的一些内容</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title'],
data() {
return {
games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
}
},
}
</script>
<!--
使用插槽
在外部使用插槽内部传递的数据
-->
<template>
<div class="container">
<Category title="游戏">
<template scope="scopeName">
<ul>
<li v-for="(g,index) in scopeName.games" :key="index">{{g}}</li>
</ul>
</template>
</Category>
<Category title="游戏">
<template scope="{games}">
<ol>
<li style="color:red" v-for="(g,index) in games" :key="index">{{g}}</li>
</ol>
</template>
</Category>
<Category title="游戏">
<template slot-scope="{games}">
<h4 v-for="(g,index) in games" :key="index">{{g}}</h4>
</template>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
}
</script>