组件基础
什么是组件
其实突然出来的这个名词,会让您不知所以然,如果⼤家使⽤过bootstrap的同学⼀定会对这个名词不陌⽣,我们其实在很早的时候就接触这个名词
通常⼀个应⽤会以⼀颗嵌套的组件树的形式来阻⽌:
局部组件
使⽤局部组件的打油诗: 建⼦ 挂⼦ ⽤⼦
注意::在组件中这个data必须是⼀个函数,返回⼀个对象。
<template>
<div id="app">
<!-- 3.使⽤⼦组件 -->
<App></App>
</div>
</template>
<script>
const App = {
//必须是⼀个函数
data() {
return {
msg: '我是App组件'
}
},
components: {
Vcontent
},
template: `
<div>
<Vheader></Vheader>
<div>
<Vaside />
<Vcontent />
</div>
</div>
`
}
new Vue({
el: '#app',
data: {},
components: {
// 2.挂载⼦组件
App
}
})
</script>
全局组件
通过 Vue.component(组件名,{}) 创建全局组件,此时该全局组件可以在任意模板(template)中使⽤
Vue.component('Child', {
template: `
<div>
<h3>我是⼀个⼦组件</h3>
</div>`
})
组件通信
父传子
如果⼀个⽹⻚有⼀个博⽂组件,但是如果你不能向这个组件传递某⼀篇博⽂的标题和内容之类想展示的数据的话,它是没有办法使⽤的.这也正是prop的由来
⽗组件往⼦组件通信: 通过Prop向⼦组件传递数据
<script>
Vue.component('Child', {
template: `
<div>
<h3>我是⼀个⼦组件</h3>
<h4>{{childData}}</h4>
</div>
`,
props: ['childData']
})
const App = {
data() {
return {
msg: '我是⽗组件传进来的值'
}
},
template: `
<div>
<Child :childData = 'msg'></Child>
</div>
`,
computed: {}
}
</script>
子传父
⽹⻚上有⼀些功能可能要求我们和⽗组件组件进⾏沟通
⼦组件往⽗组件通信: 监听子组件事件,使用事件抛出一个值
<script>
Vue.component('Child', {
template: `
<div>
<h3>我是⼀个⼦组件</h3>
<h4>{{childData}}</h4>
<input type="text" @input =
'handleInput'/>
</div>
`,
props: ['childData'],
methods: {
handleInput(e) {
const val = e.target.value;
//使⽤$emit触发⼦组件的事件
this.$emit('inputHandler', val);
},
})
const App = {
data() {
return {
msg: '我是⽗组件传进来的值',
newVal: ''
}
},
methods: {
input(newVal) {
// console.log(newVal);
this.newVal = newVal;
}
},
template: `
<div>
<div class='father'>
数据:{{newVal}}
</div>
<!--⼦组件监听事件-->
<Child :childData = 'msg' @inputHandler
= 'input'></Child>
</div>`,
computed: {
}
}
</script>
- 在⽗组件中 ⼦组件上绑定⾃定义事件。
- 在⼦组件中 触发原⽣的事件 在事件函数通过this.$emit触发⾃定义的事件
平行组件
在开发中,可能会存在没有关系的组件通信,⽐如有个博客内容显示组件,还有⼀个表单提交组件,我们现在提交数据到博客内容组件显示,这显示有点费劲.
为了解决这种问题,在vue中我们可以使⽤bus,创建中央事件总线
<script>
const bus = new Vue();
// 中央事件总线 bus
Vue.component('B', {
data() {
return {
count: 0
}
},
template: `<div>{{count}}</div>`,
created() {
// $on 绑定事件
bus.$on('add', (n) => {
this.count += n;
})
}
})
Vue.component('A', {
data() {
return {
}
},
template: `
<div>
<button @click='handleClick'>加⼊购物⻋
button>
</div>
`,
methods: {
handleClick() {
// 触发绑定的函数 // $emit 触发事件
bus.$emit('add', 1);
}
}
})
</script>
其他组件通信方式
⽗组件 provide来提供变量,然后再⼦组件中通过inject来注⼊变量.⽆论组件嵌套多深
<script>
Vue.component('B', {
data() {
return {
count: 0
}
},
inject: ['msg'],
created() {
console.log(this.msg);
},
template: `
<div>
{{msg}}
</div>
`,
})
Vue.component('A', {
data() {
return {}
},
created() {
// console.log(this.$parent.$parent);
// console.log(this.$children);
console.log(this);
},
template: `
<div>
<B></B>
</div>
`
})
new Vue({
el: '#app',
data: {
},
components: {
// 2.挂载⼦组件
App
}
})
</script>
插槽
匿名插槽
⼦组件定义 slot 插槽,但并未具名,因此也可以说是默认插槽。只要在⽗元素中插⼊的内容,默认加⼊到这个插槽中去
<script>
Vue.component('MBtn', {
template: `
<button>
<slot></slot>
</button>
`,
props: {
type: {
type: String,
defaultValue: 'default'
}
},
})
const App = {
data() {
return {
}
}, template: `
<div>
<m-btn>登录</m-btn>
<m-btn>注册</m-btn>
<m-btn>提交</m-btn>
</div>
`,
}
new Vue({
el: '#app',
data: {
},
components: {
// 2.挂载⼦组件
App
}
})
</script>
具名插槽
具名插槽可以出现在不同的地⽅,不限制出现的次数。只要匹配了 name 那么这些内容就会被插⼊到这个 name 的槽中去
<script>
Vue.component('MBtn', {
template: ` 3 <button :class='type' @click='clickHandle'>
<slot name='register'></slot>
<slot name='login'></slot>
<slot name='submit'></slot>
</button>
`, props: {
type: {
type: String,
defaultValue: 'default'
}
},
methods: {
clickHandle() {
this.$emit('click');
}
}
})
const App = {
data() {
return {
}
},
methods: {
handleClick() {
alert(1);
},
handleClick2() {
alert(2);
}
},
template: `
<div>
<MBtn type='default' @click='handleClick'>
<template slot='register'>
注册
</template>
</MBtn>
<MBtn type='success' @click='handleClick2'>
<template slot='login'>
登录
</template>
</MBtn>
<MBtn type='danger'>
<template slot='submit'>
提交
</template>
</MBtn></div>
`,
}
new Vue({
el: '#app',
data: {
},
components: {
App
}
})
</script>
作用域插槽
通常情况下普通的插槽是⽗组件使⽤插槽过程中传⼊东⻄决定了插槽的内容。但有时我们需要获取到⼦组件提供的⼀些数据,那么作⽤域插槽就排上⽤场了
<script>
Vue.component('MyComp', {
data() {
return {
data: {
username: '⼩⻢哥'}
}
},
template: `
<div>
<slot :data = 'data'></slot>
<slot :data = 'data' name='one'></slot>
</div>`
})
const App = {
data() {
return {
}
},
template: `
<div>
<MyComp>
<!--默认的插槽 default可以省略-->
<template v-slot:default='user'>
{{user.data.username}}
</template>
</MyComp>
<MyComp>
<!--与具名插槽配合使⽤-->
<template v-slot:one='user'>
{{user.data.username}}
</template>
</MyComp>
</div>
`,
}
new Vue({
el: '#app',
data: {
},
components: {
App
}
})
</script>
作用域插槽的应用
先说⼀下我们假设的应⽤常⽤场景,我们已经开发了⼀个代办事项列表的组件,很多模块在⽤,现在要求在不影响已测试通过的模块功能和展示的情况下,给已完成的代办项增加⼀个对勾效果。
也就是说,代办事项列表组件要满⾜⼀下⼏点
- 之前数据格式和引⽤接⼝不变,正常展示
- 新的功能模块增加对勾
<script>
const todoList = {
data() {
return {
}
},
props: {
todos: Array, 9 defaultValue: []
},
template: `
<ul>
<li v-for='item in todos' :key='item.id'>
<slot :itemValue='item'>
{{item.title}}
</slot>
</li>
</ul>
`
}
const App = {
data() {
return {
todoList: [
{
title: '⼤哥你好么',
isComplate: true,
id: 1
},
{
title: '⼩弟我还⾏',
isComplate: false,
id: 2
},
{
title: '你在⼲什么',
isComplate: false,
id: 3
},
{
title: '抽烟喝酒烫头',
isComplate: true,
id: 4
}
]
}
},
components: {
todoList
},
template: `
<todoList :todos='todoList'>
<template v-slot='data'>
<input type='checkbox' vmodel='data.itemValue.isComplate'/>
{{data.itemValue.title}}
</template>
</todoList>
`,
}
new Vue({
el: '#app',
data: {
},
components: {
App
}
})
</script>
生命周期
“你不需要⽴⻢弄明⽩所有的东⻄,不过随着你的不断学习和使⽤,它的参考值会越来越⾼。当你在做项⽬过程中,遇到了这种问题的时候,再回过头来看这张图。
什么是生命周期
每个 Vue 实例在被创建时都要经过⼀系列的初始化过程。例如:从开始创建、初始化数据、编译模板、挂载Dom、数据变化时更新DOM、卸载等⼀系列过程。我们称 这⼀系列的过程 就是Vue的⽣命周期。通俗说就是Vue实例从创建到销毁的过程,就是⽣命周期。同时在这个过程中也会运⾏⼀些叫做⽣命周期钩⼦的函数,这给了⽤户在不同阶段添加⾃⼰的代码的机会,利⽤各个钩⼦来完成我们的业务代码。
干活满满
生命周期钩子
beforCreate
实例初始化之后、创建实例之前的执⾏的钩⼦事件
<script>
Vue.component('Test', {
data() {
return {
msg: '⼩⻢哥'
}
},
template: `
<div>
<h3>{{msg}}</h3>
</div>
`,
beforeCreate: function () {
// 组件创建之前
console.log(this.$data);//undefined
}
})
</script>
效果:
created
实例创建完成后执⾏的钩⼦
created() {
console.log('组件创建', this.$data);
}
效果
beforeMount
将编译完成的html挂载到对应的虚拟DOM时触发的钩⼦ 此时⻚⾯并没有内容。
即此阶段解读为: 即将挂载
beforeMount(){
// 挂载数据到 DOM之前会调⽤
console.log('DOM挂载之
前',document.getElementById('app'));
}
效果
mounted
编译好的html挂载到⻚⾯完成后所执⾏的事件钩⼦函数
mounted() {
console.log('DOM挂载完
成',document.getElementById('app'));
}
效果
beforeUpdate和updated
beforeUpdate() {
// 在更新DOM之前 调⽤该钩⼦,应⽤:可以获取原始
的DOM
console.log('DOM更新之前',
document.getElementById('app').innerHTML);
},
updated() {
// 在更新DOM之后调⽤该钩⼦,应⽤:可以获取最新的
DOM
console.log('DOM更新完成',
document.getElementById('app').innerHTML);
}
效果:
beforeDestroy和destroyed
当⼦组件在v-if的条件切换时,该组价处于创建和销毁的状态
beforeDestroy() {
console.log('beforeDestroy');
},
destroyed() {
console.log('destroyed');
},
activated和deactivated
当配合vue的内置组件 ⼀起使⽤的时候,才会调⽤下⾯此⽅法 组件的作⽤它可以缓存当前组件
activated() {
console.log('组件被激活了');
},
deactivated() {
console.log('组件被停⽤了');
},
组件进阶
获取DOM和子组件对象
尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript ⾥直接访问⼀个⼦组件。为了达到这个⽬的,你可以通过 ref 特性为这个⼦组件赋予⼀个ID 引⽤。例如:
<script>
const Test = {
template: `<div class='test'>我是测试组件
</div>`
}
const App = {
data() {
return {
}
},
created() {
console.log(this.$refs.test);
//undefined
},
mounted() {
// 如果是组件挂载了ref 获取是组件对象,如果
是标签挂载了ref, 则获取的是DOM元素
console.log(this.$refs.test);
console.log(this.$refs.btn);
// 加载⻚⾯ 让input⾃动获取焦点
this.$refs.input.focus();
},
components: {
Test
},
template: `
<div>
<button ref = 'btn'></button>
<input type="text" ref='input'>
<Test ref = 'test'></Test>
</div>
`
}
new Vue({
el: '#app',
data: {
},
components: {
App
}
})
</script>
nextTick的用法
将回调延迟到下次 DOM 更新循环之后执⾏。在修改数据之后⽴即使⽤它,然后等待 DOM 更新有些事情你可能想不到,vue在更新DOM时是异步执⾏的.只要侦听到数据变化,Vue将开启⼀个队列,并缓存在同⼀事件循环中发⽣的所有数据变更.如果同⼀个wather被多次触发,只会被推⼊到队列中⼀次.这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是⾮常重要的。然后,在下⼀个的事件循环“tick”中,Vue 刷新队列并执⾏实际 (已去重的) ⼯作
<div id="app">
<h3>{{message}}</h3>
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: '123'
}
})
vm.message = 'new Message';//更新数据
console.log(vm.$el.textContent); //123
Vue.nextTick(() => {
console.log(vm.$el.textContent); //new
Message
})
</script>
当你设置 vm.message = 'new Message' ,该组件不会⽴即重新渲染.当刷新队列时,组件会在下⼀个事件循环'tick'中更新.多数情况我们不需要关⼼这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘⼿。虽然Vue.js 通常⿎励开发⼈员使⽤“数据驱动”的⽅式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后⽴即使⽤ Vue.nextTick(callback) 。这样回调函数将在 DOM 更新完成后被调⽤。
nextTick的应用
有个需求:
在⻚⾯拉取⼀个接⼝,这个接⼝返回⼀些数据,这些数据是这个⻚⾯的⼀个浮层组件要依赖的,然后我在接⼝⼀返回数据就展示了这个浮层组件,展示的同时,上报⼀些数据给后台(这些数据就是⽗组件从接⼝拿的),这个时候,神奇的事情发⽣了,虽然我拿到数据了,但是浮层展现的时候,这些数据还未更新到组件上去,上报失败
<script>
const Pop = {
data() {
return {
isShow: false
}
},
template: `
<div v-show = 'isShow'>
{{name}}
</div>
`,
props: ['name'],
methods: {
show() {
this.isShow = true;
alert(this.name);
}
},
}
const App = {
data() {
return {
name: ''
}
},
created() {
// 模拟异步请求的数据
setTimeout(() => {
this.name = '⼩⻢哥',
this.$refs.pop.show();
}, 2000);
},
components: {
Pop
},
template: `<pop ref='pop' :name='name'>pop>`
}
const vm = new Vue({
el: '#app',
components: {
App
}
})
</script>
完美解决
created() {
// 模拟异步请求的数据
setTimeout(() => {
this.name = '⼩⻢哥', this.$nextTick(() => {
this.$refs.pop.show();
})
}, 2000);
},
对象变更检测注意事项
由于JavaScript的限制,Vue不能检测对象属性的添加和删除
对于已经创建的实例,Vue不允许动态添加根级别的响应式属性.但是,可以通过 Vue.set(object,key,value) ⽅法向嵌套独享添加响应式属性
<div id="app">
<h3>
{{user.name}}{{user.age}}
<button @click='handleAdd'>添加年龄
</button>
</h3>
</div>
<script src="./vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
user: {},
},
created() {
setTimeout(() => {
this.user = {
name: '张三'
}
}, 1250);
},
methods: {
handleAdd() {
console.log(this);
// ⽆响应式
// this.user.age = 20;
// 响应式的
this.$set(this.user, 'age', 20);
}
},
})
</script>
this.$set(this.user,'age',20);//它只是全局Vue.set
的别名
如果想为已存在的对象赋值多个属性,可以使⽤ Object.assign()
this.user = Object.assign({}, this.user, {
age: 20,
phone: '113131313'
})
混入minxin偷懒
混⼊(mixin)提供了⼀种⾮常灵活的⽅式,来分发Vue组件中的可复⽤功能.⼀个混⼊对象可以包含任意组件选项.
⼀个混⼊对象可以包含任意组件选项。当组件使⽤混⼊对象时,所有混⼊对象的选项将被“混合”进⼊该组件本身的选项。
<div id="app">
{{msg}}
</div>
<script src="./vue.js"></script>
<script>
const myMixin = {
data() {
return {
msg: '123'
}
},
created() {
this.sayHello()
},
methods: {
sayHello() {
console.log('hello mixin')
}
},
}
new Vue({
el: '#app',
data() {
return {
msg: '⼩⻢哥'
}
},
mixins: [myMixin]
})
</script>
mixin应用
有⼀种很难常⻅的情况:有两个⾮常相似的组件,他们共享同样的基本函数,并且他们之间也有⾜够的不同,这时你站在了⼀个⼗字路⼝:我是把它拆分成两个不同的组件?还是只使⽤⼀个组件,创建⾜够的属性来改变不同的情况。
这些解决⽅案都不够完美:如果你拆分成两个组件,你就不得不冒着如果功能变动你要在两个⽂件中更新它的⻛险,这违背了 DRY 前提。另⼀⽅⾯,太多的属性会很快会变得混乱不堪,对维护者很不友好,甚⾄是你⾃⼰,为了使⽤它,需要理解⼀⼤段上下⽂,这会让你感到失望。
使⽤混合。Vue 中的混合对编写函数式⻛格的代码很有⽤,因为函数式编程就是通过减少移动的部分让代码更好理解。混合允许你封装⼀块在应⽤的其他组件中都可以使⽤的函数。如果被正确的使⽤,他们不会改变函数作⽤域外部的任何东⻄,所以多次执⾏,只要是同样的输⼊你总是能得到⼀样的值。这真的很强⼤。
我们有⼀对不同的组件,他们的作⽤是切换⼀个状态布尔值,⼀个模态框和⼀个提示框.这些提示框和模态框除了在功能,没有其它共同点:它们看起来不⼀样,⽤法不⼀样,但是逻辑⼀样
<div id="app">
<App></App>
</div>
<script src="./vue.js"></script>
<script> // 全局混⼊ 要格外⼩⼼ 每次实例创建 都会调⽤
Vue.mixin({
reated() {
onsole.log('hello from mixin!!');
}
})
// 抽离
const toggleShow = {
data() {
return {
isShow: false
}
},
methods: {
toggleShow() {
this.isShow = !this.isShow
}
}
}
const Modal = {
template: `<div v-if='isShow'><h3>模态框
组件</h3></div>`,
data() {
return {
}
},
mixins: [toggleShow]
}
const ToolTip = {
data() {
return {
}
},
template: `<div v-if='isShow'><h3>提示组</h3></div>`,
mixins: [toggleShow]
}
const App = {
data() {
return {
}
},
template: `
<div>
<button @click='handleModel'>模态框
</button>
<button @click='handleToolTip'>提示
框</button>
<Modal ref='modal'></Modal>
<ToolTip ref="toolTip"></ToolTip>
</div>
`,
components: {
Modal,
ToolTip
},
methods: {
handleModel() {
this.$refs.modal.toggleShow()
},
handleToolTip() {
this.$refs.toolTip.toggleShow()
}
},
}
new Vue({
el: '#app',
data: {},
components: {
App
},
})
</script>