前端 - Vue组件与脚手架
组件化编程
组件的定义:实现应用中局部功能的代码(HTML, CSS, JS等)和资源(images等)的整合。
组件分为:
- 非单文件组件:一个文件中包含若干个组件
- 单文件组件:一个文件中只有一个组件,即我们看到的
.vue
文件
非单文件组件
实现对下面HTML页面的组件化编程:
<div id="root">
<h1>学校名称:{{schoolName}}</h1>
<h1>地址:{{address}}</h1>
<hr>
<h1>学生姓名:{{stuName}}</h1>
<h1>年龄:{{age}}</h1>
</div>
通过非单文件组件:
<div id="root">
<!-- 第三步:编写组件标签 -->
<School></School>
<hr>
<student></student>
<!-- 组件复用, 且数据互相独立 -->
<student></student>
</div>
/* 第一步:定义school组件和student组件 */
// 定义school组件
const s = Vue.extend({
// 不能写el, 因为所有的组件都要被一个vm管理, 由vm决定组件给哪个容器服务
// 使用template必须只有一个根元素, 即所有元素最终都要被一个根元素包裹
template: `
<div>
<h1>学校名称:{{schoolName}}</h1>
<h1>地址:{{address}}</h1>
</div>
`,
// data必须写成函数, 这样每个实例可以维护一份被返回对象的独立拷贝
data() {
return {
schoolName: 'My school',
address: 'Hangzhou',
}
},
})
// 定义student组件
const student = Vue.extend({
template: `
<div>
<h1>学生姓名:{{stuName}}</h1>
<h1>年龄:{{age}}</h1>
</div>
`,
data() {
return {
stuName: 'Lee',
age: 30
}
},
})
// 创建vm
const vm = new Vue({
el: '#root',
/* 第二步:注册组件(局部注册) */
components: {
School: school,
student // 同名, 可以简写
}
});
上面使用了局部注册,使用全局注册的方法:
/* 第二步:注册组件(全局注册, 所有Vue实例都能用) */
Vue.component('School', s);
Vue.component('student', student);
// 创建vm
const vm = new Vue({
el: '#root'
});
组件名
- 一个单词:
school
或者School
- 多个单词:
my-school
或者MySchool
(该方式需要Vue脚手架支持)
注意:
- 组件名尽可能回避HTML标签名
- 创建组件时,配置
name
属性,可以指定组件在Vue开发者工具中显示的名字,实际上使用时还是需要注册时的名字
组件标签
可以写为<school></school>
,也可以<school/>
(该方式需要Vue脚手架支持,否则后续组件无法渲染)
简写
对上面的school
简写,可以直接写成对象,不写Vue.extend()
方法:
const s = {
template: `
<div>
<h1>学校名称:{{schoolName}}</h1>
<h1>地址:{{address}}</h1>
</div>
`,
data() {
return {
schoolName: 'My school',
address: 'Hangzhou',
}
},
};
const vm = new Vue({
el: '#root',
components: {
school: s, // 注册时如果发现是对象, 自动调用Vue.extend()方法
student
}
});
组件的嵌套
一般来说,vm
只管理一个app
组件,而app
负责管理剩下的组件。
<div id="root"></div>
// 首先定义 子组件student
const student = {
template: `
<div>
<h1>学生姓名:{{stuName}}</h1>
<h1>年龄:{{age}}</h1>
</div>
`,
data() {
return {
stuName: 'Lee',
age: 30
}
},
};
// 然后定义 父组件school
const school = {
// 父组件template可以调用子组件
template: `
<div>
<h1>学校名称:{{schoolName}}</h1>
<h1>地址:{{address}}</h1>
<student></student>
</div>
`,
data() {
return {
schoolName: 'My school',
address: 'Hangzhou',
}
},
// 注册组件, 当前组件管理student组件
components: {
student
}
};
// 定义app组件, 管理其他组件
const app = {
template: `
<div>
<school></school>
</div>
`,
// 注册组件, 当前组件管理school组件
components: {
school
}
}
// 创建vm
const vm = new Vue({
el: '#root',
template: `<app></app>`,
// 只需要注册app组件
components: {
app
}
});
VueComponent构造函数
首先看一下一个简单的例子:
// 定义组件school
const school = Vue.extend({
template: `
<div>
<h1>学校名称:{{schoolName}}</h1>
</div>
`,
data() {
return {
schoolName: 'My school'
}
}
});
// 创建vm
const vm = new Vue({
el: '#root',
template: `<school></school>`,
components: {
school
}
});
输出school
,可以发现school
组件就是一个VueComponent
构造函数:
console.log(school); // ƒ VueComponent (options) { this._init(options); }
总结:
school
组件本质上是一个名为VueComponent
的构造函数,该函数由Vue.extend()
生成- 每当有
<school></school>
,Vue解析时会执行new VueComponent(options)
,创建school
组件的实例对象 - 注意:每次调用
Vue.extend()
,返回的都是一个全新的VueComponent
构造函数 - 在组件配置(
data
,watch
,computed
等的函数)中,this
指向的是VueComponent
实例对象
Vue实例与组件实例
一个重要的关系:
// VueComponent.prototype.__proto__ === Vue.prototype
console.log(school.prototype.__proto__ === Vue.prototype); // true
这样的话,组件实例就能够访问到Vue原型上的属性、方法。
$delete: ƒ del(target, key)
$destroy: ƒ ()
$emit: ƒ (...args)
$forceUpdate: ƒ ()
$inspect: ƒ ()
$mount: ƒ ( el, hydrating )
$nextTick: ƒ (fn)
$off: ƒ (event, fn)
$on: ƒ (event, fn)
$once: ƒ (event, fn)
$set: ƒ (target, key, val)
$watch: ƒ ( expOrFn, cb, options )
...
正常来说,Vue
的实例对象满足vm.__proto__ === Vue.prototype
,而Vue.prototype
是一个Object
实例对象,所以Vue.prototype.__proto__ === Object.prototype
,即vm.__proto__.__proto__ === Object.prototype
。因此,vm
既能使用Vue
的原型的属性和方法,也能使用Object
的原型的属性和方法。
所以,由于school
是一个VueComponent
构造函数,按理来说,school
的实例化对象应该也满足类似关系。但是在Vue中,手动地将VueComponent.prototype.__proto__
赋值为Vue.prototype
。建立了这样一条原型链,VueComponent
的实例对象就能够同时使用VueComponent
的原型、Vue
的原型、Object
的原型的属性和方法。
在Vue源码中:
var Sub = function VueComponent (options) {
this._init(options);
};
// 创建一个新对象, 它的__proto__为Super.prototype, 然后将Sub.prototype设置为这个新创建的对象
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub; // 修正constructor
Object.create()
方法创建一个新对象,新创建的对象的__proto__
为传入的对象。
单文件组件
暴露的三种方法
<script>
/* 方式一 */
export const school = Vue.extend({
template: `
<div>
<h1>学校名称:{{schoolName}}</h1>
</div>
`,
data() {
return {
schoolName: 'My school'
}
}
});
</script>
<script>
/* 方式二 */
const school = Vue.extend({...});
export {school};
</script>
<script>
/* 方式三 */
const school = Vue.extend({...});
export default school;
</script>
一般来说采取方式三,默认暴露的方式书写。由于只需要暴露一个变量,所以不需要中转变量school
;并且Vue.extend()
可以简写,所以一般写成:
<script>
export default {...};
</script>
单文件组件举例
<body>
<!-- index.html -->
<div id="root"></div>
<script type="text/javascript" src="../js/vue.js"></script>
<script type="text/javascript" src="./main.js"></script>
</body>
/* main.js Vue实例 */
import App from './App.vue'
new Vue({
el: '#root',
template: '<App></App>',
components: { App }
});
<template>
<div>
<School></School>
<School></School>
</div>
</template>
<script>
import School from "./School.vue";
export default {
name: "App",
components: {
School,
},
};
</script>
<template>
<!-- 组件结构 -->
<div>
<h1>学校名称:{{ name }}</h1>
<h1>地址:{{ address }}</h1>
<Student></Student>
<Student></Student>
</div>
</template>
<script>
import Student from "./Student.vue";
export default {
name: "School",
data() {
return {
name: "My school",
address: "Hangzhou",
};
},
components: {
Student,
},
};
</script>
<style>
/* 组件的样式 */
</style>
<template>
<div>
<h2>学生姓名:{{ name }}</h2>
<h2>性别:{{ sex }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "Lee",
sex: "female",
};
},
};
</script>
单文件组件需要在Vue脚手架环境下运行。
Vue脚手架
脚手架分析
在执行下面命令后,自动生成脚手架:
vue create hello_cli
main.js
是整个项目的入口文件:
import Vue from 'vue' // 引入Vue
import App from './App.vue' // 引入App组件, 它是所有组件的父组件
Vue.config.productionTip = false // 关闭vue生产提示
// 创建Vue实例对象, 即vm
new Vue({
render: h => h(App), // 将App组件放入容器中
}).$mount('#app')
完成对应的.vue
文件编写,即能成功运行。
render()
如果将上面的main.js
稍作修改
new Vue({
el: '#app',
template: '<App></App>',
components: { App }
});
会出现错误:
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
这是因为通过下面这条语句引入的Vue是不完整的,无法解析实例化Vue时的template
。
import Vue from 'vue'
字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个
createElement
方法作为第一个参数用来创建VNode
。
render()
函数会接受一个 createElement
方法,所以相当于调用了createElement(App)
。
ref属性
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的$refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
<div id="app">
<Student ref="stu"/>
<button @click="check">Click</button>
<h3 ref="h3test">Test</h3>
</div>
export default {
name: 'App',
components: {
Student
},
methods: {
check() {
console.log(this.$refs); // {stu: VueComponent, h3test: h3}
}
}
}
props配置
通过数组简单接收
<div>
<h1>{{hello}}</h1>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age + 1}}</h2>
</div>
export default {
name : "Student",
data() {
return {
hello: 'Hello!!!'
}
},
props: ['name', 'age'] // 数组接受
}
在Student
的父组件App
中:
<div id="app">
<Student name="zhangsan" :age="20"/>
<Student name="lisi" :age="30"/>
<Student name="wangwu" :age="25"/>
</div>
注意:
- 使用
v-bind
绑定age
属性,这样属性值不再是字符串,而是作为JS表达式运行 - props不能使用
key
和ref
等特殊属性
通过对象指定数据
仅对数据类型指定:
export default {
// ...
props: {
name: String,
age: Number
}
}
指定数据是否是必须的以及默认值:
export default {
// ...
props: {
name: {
type: String,
require: true // 属性必须
},
age: {
type: Number,
default: 100 // 属性默认值是100
}
}
}
修改props
尝试修改age
:
<div>
<h1>{{hello}}</h1>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age + 1}}</h2>
<button @click="changeAge">Age++</button>
</div>
export default {
// ...
methods: {
changeAge() {
this.age++;
}
},
}
虽然能成功修改,但是会有如下警告
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
备注:props是只读的,如果确实需要修改,需要复制props
的内容到data
,然后修改。
使用data
中的变量接收props:
<div>
<h1>{{hello}}</h1>
<h2>姓名:{{myName}}</h2>
<h2>年龄:{{myAge + 1}}</h2>
<button @click="changeAge">Age++</button>
</div>
export default {
name : "Student",
data() {
return {
hello: 'Hello!!!',
myName: this.name, // 接收props
myAge: this.age
}
},
// props: {...},
methods: {
changeAge() {
this.myAge++; // 修改data中的变量
}
},
}
mixin混入
可以把多个组件公用的配置提取成一个混入对象。
局部混入
/* mixin.js */
export const mixin = {
data() {
return {
name: 'mixin_test',
hello: 'Hello!!'
}
},
methods: {
showName() {
console.log(this.name);
}
},
mounted() {
console.log('Mounted!!!!');
},
}
/* Student.vue */
import {mixin} from '../mixin'
export default {
// name: "Student",
// data() {...},
mixins: [mixin]
};
/* School.vue */
import Student from "./Student.vue";
import {mixin} from "../mixin"
export default {
// name: "School",
// data() {...},
// components: {...},
mixins: [mixin],
mounted() {
console.log('Mounted School!');
},
};
运行后发现:
Student
组件和School
组件都能使用showName()
方法Student
组件和School
组件本身在data
中定义的name
属性值没有被mixin
覆盖,而原来没有定义的hello
属性出现在data
中,值为mixin
的值- 生命周期钩子
mounted()
则是二者都触发,School
组件先输出Mounted!!!!
再输出Mounted Schoool!
全局混入
import Vue from 'vue'
import App from './App.vue'
import {mixin} from './mixin'
Vue.config.productionTip = false
Vue.mixin(mixin)
new Vue({
render: h => h(App),
}).$mount('#app')
如果使用全局混入,Root
和App
也会被混入。
插件
用于增强Vue,实际上是包含install()
方法的一个对象,第一个参数是Vue
,之后的参数是插件使用者传入的参数。
import Vue from 'vue'
import App from './App.vue'
import plugins from './plugins' // 引入插件
Vue.config.productionTip = false
Vue.use(plugins, 'aa', 'bb', 'cc'); // 使用插件并传入参数
new Vue({
render: h => h(App),
}).$mount('#app');
/* plugins.js */
export default {
install(Vue, a, b, c) {
console.log(a, b, c); // aa bb cc
/* 定义全局过滤器 */
// Vue.filter(...)
/* 定义全局指令 */
// Vue.directive(...)
/* 定义全局混入 */
// Vue.mixin(...)
/* 给Vue原型添加方法(vm和vc都能使用) */
Vue.prototype.testInstall = () => console.log('Test install!');
}
}
scoped样式
当CSS类名出现冲突时,会根据引用顺序决定最终显示效果:
在Student.vue
中定义.test
样式:
<template>
<div class="test">
<h2 @click="showName">学生姓名:{{ name }}</h2>
<h2 @click="testInstall">性别:{{ sex }}</h2>
</div>
</template>
<script>
// ...
</script>
<style>
.test {
background-color: skyblue;
}
</style>
在School.vue
中也定义.test
样式:
<template>
<div class="test">
<h1 @click="showName">学校名称:{{ name }}</h1>
<h1>地址:{{ address }}</h1>
</div>
</template>
<script>
// ...
</script>
<style>
.test {
background-color: orange;
}
</style>
那么最终显示效果根据App.vue
的引入顺序决定,如果是下面的顺序,则显示为orange
;当顺序调换,显示为skyblue
:
<template>
<div>
<School></School>
<Student></Student>
</div>
</template>
<script>
import Student from "./components/Student.vue"
import School from "./components/School.vue"
// export default {...};
</script>
使用scoped
属性让样式只在局部生效School.vue
和Student.vue
二者中任意的样式添加scoped
属性,即可恢复正常:
<style scoped>
.test {
background-color: skyblue;
}
</style>
此外,使用npm
下载less
后,还可以使用less
:
<style lang="less" scoped>
.test {
background-color: skyblue;
}
</style>
TodoList样例
流程
- 实现静态组件
- 展示动态数据:数据保存在哪个组件
- 交互:绑定事件监听
源码
安装nanoid
用于生成唯一id:
npm i nanoid
App.vue
存储待办事项,并且提供一些修改待办事项的方法,通过props
传输给子组件。
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 通过props将添加item的方法传给MyHeader -->
<MyHeader :addTodo="addTodo" />
<!-- 通过props将itemList等传给MyList -->
<MyList
:itemList="itemList"
:checkTodo="checkTodo"
:removeTodo="removeTodo"
/>
<MyFooter
:itemList="itemList"
:selectAll="selectAll"
:removeAllCompleted="removeAllCompleted"
/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: {
MyHeader,
MyList,
MyFooter,
},
data() {
return {
// 存储事项
itemList: [
{ id: "001", isDone: true, content: "学习Vue" },
{ id: "002", isDone: false, content: "学习ES6" },
{ id: "003", isDone: false, content: "学习AJAX" },
],
};
},
methods: {
// 添加一个新item
addTodo(todo) {
this.itemList.unshift(todo);
},
// 将一个item的isDone取反
checkTodo(itemId) {
this.itemList.forEach((todo) => {
if (todo.id === itemId) {
todo.isDone = !todo.isDone;
}
});
},
// 删除一个todo
removeTodo(itemId) {
this.itemList = this.itemList.filter((todo) => {
return todo.id !== itemId;
});
},
// 全选/全不选
selectAll(isSeleted) {
this.itemList.forEach((todo) => {
todo.isDone = isSeleted;
});
},
// 删除所有已经完成的todo
removeAllCompleted() {
this.itemList = this.itemList.filter((todo) => {
return !todo.isDone;
});
},
},
};
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
MyHeader.vue
负责添加新事项。
<template>
<div class="todo-header">
<!-- 绑定按下回车键 -->
<input
type="text"
v-model="content"
placeholder="请输入你的任务名称,按回车键确认"
@keydown.enter="addItem"
/>
</div>
</template>
<script>
import { nanoid } from "nanoid"; // 引入nanoid, 生成唯一id
export default {
name: "MyHeader",
data() {
return {
content: "",
};
},
props: {
addTodo: Function, // 接收addTodo()方法
},
methods: {
addItem(event) {
if (!this.content) return; // 判断输入是否为空
// 获取用户输入, 并且包装成对象传输
const todo = {
id: nanoid(),
content: this.content,
isDone: false,
};
this.addTodo(todo);
this.content = ""; // 将输入框重新变为空
},
},
};
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
MyList.vue
负责展示事项。
<template>
<ul class="todo-main">
<!-- 使用props传数据给MyItem -->
<MyItem
v-for="todo of itemList"
:key="todo.id"
:todo="todo"
:checkTodo="checkTodo"
:removeTodo="removeTodo"
/>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
props: {
itemList: Array,
checkTodo: Function,
removeTodo: Function
},
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
MyItem.vue
是一个事项的组件,可以删除该事项。
<template>
<li>
<label>
<!-- checkbox被勾选时触发事件 -->
<input type="checkbox" :checked="todo.isDone" @change="handleChange">
<!-- 不建议用v-model绑定todo.isDone, 因为这样相当于修改props -->
<!-- <input type="checkbox" v-model="todo.isDone"> -->
<span>{{todo.content}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
props: {
todo: Object,
checkTodo: Function,
removeTodo: Function
},
methods: {
handleChange() {
this.checkTodo(this.todo.id);
},
handleDelete() {
if(confirm('是否删除该事项?')) {
this.removeTodo(this.todo.id);
}
}
},
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: #ddd;
}
/* 鼠标在li上, 显示button */
li:hover button {
display: block;
}
</style>
MyFooter.vue
展示已经完成的事项和总事项,可以全选和全不选,可以删除所有已完成的事项。
<template>
<div class="todo-footer" v-show="itemList.length">
<label>
<!-- 当todo被全选时打勾 -->
<input type="checkbox" v-model="isAll"/>
</label>
<span> <span>已完成({{completed}})</span> / 全部 ({{itemList.length}})</span>
<button class="btn btn-danger" @click="handleRemoveCompleted">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter",
props: {
itemList: Array,
selectAll: Function,
removeAllCompleted: Function
},
computed: {
completed() {
// prev先前的返回值, curr当前对象, 0初始值
return this.itemList.reduce((prev, curr) => prev + curr.isDone, 0);
},
isAll: {
get() {
return this.itemList.length !==0 && this.completed === this.itemList.length;
},
// 通过setter完成全选
set(value) {
this.selectAll(value);
}
}
},
methods: {
handleRemoveCompleted() {
if(confirm('是否删除所有已完成事项?')) {
this.removeAllCompleted();
}
}
},
};
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
总结
v-model
不能绑定props
传来的值,因为props
传来的值是只读的- 父组件可以通过
props
将修改自身数据的方法传给子组件,从而间接实现子组件向父组件的通信
本地存储
将itemList
存在localStorage
,需要在App.vue
中添加数据监视:
export default {
name: "App",
// ...
data() {
return {
// 初始化时从本地存储中读取
itemList: JSON.parse(localStorage.getItem("itemList")) || [],
};
},
watch: {
itemList: {
deep: true, // 深度监视, 因为监视的目标是一个对象
handler(value) {
localStorage.setItem("itemList", JSON.stringify(value));
},
},
},
};
组件自定义事件
之前我们通过父组件给子组件传输props
实现子给父传输数据,可以通过自定义事件传输。
触发自定义事件
首先需要触发自定义事件,以上面的MyFooter
为例,当用户点击全选/全不选时,将布尔值传输给App
:
export default {
name: "MyFooter",
// props: {...},
computed: {
// completed() {...},
isAll: {
get() {
return this.itemList.length !==0 && this.completed === this.itemList.length;
},
// 通过setter完成全选
set(value) {
// this.selectAll(value); // 原来的处理方法
// 当isAll被改变时, 触发selectAllChanged事件
this.$emit('selectAllChanged', value);
}
}
}
};
绑定自定义事件
事件在MyFooter
组件触发,于是需要在App
组件绑定:
- 方式一:通过
v-on
,在组件标签上绑定.once
等修饰符通用
<!-- 当MyFooter触发selectAllChanged事件时, selectAll函数处理该事件 -->
<MyFooter
:itemList="itemList"
:removeAllCompleted="removeAllCompleted"
@selectAllChanged="selectAll"
/>
- 方式二:通过
ref
,拿到组件实例对象vc
之后,再绑定
<MyFooter
:itemList="itemList"
:removeAllCompleted="removeAllCompleted"
ref="footer"
/>
export default {
name: "App",
// ...
mounted() {
/* 更灵活, 例如可以通过setTimeout延时绑定 */
this.$refs.footer.$on('selectAllChanged', this.selectAll); // 通过$on来绑定
// this.$refs.footer.$once('selectAllChanged', this.selectAll); // 只触发一次
},
};
自定义事件解绑
export default {
name: "MyFooter",
// ...
methods: {
// handleRemoveCompleted() {...},
unbind() {
// this.$off('selectAllChanged'); // 解绑单个自定义事件
// this.$off(['selectAllChanged', 'removeAllCompleted']); // 解绑多个自定义事件
this.$off(); // 解绑全部自定义事件
}
},
};
this指向
如果绑定时这样写回调函数,则this
是触发事件的组件,即MyFooter
。
this.$refs.footer.$on("removeAllCompleted", function() {
console.log(this); // VueComponent {…}
});
可以修改为箭头函数,或恢复成之前的写法,配置在methods
中。
原生事件
给组件的原生事件绑定回调函数,需要添加.native
(否则认为是自定义事件),默认绑定给组件最外面的标签。
<MyFooter :itemList="itemList" @click.native="show"/>
全局事件总线
实现任意组件间通信,原理是设置一个中间对象,需要发数据的组件触发该对象的事件并传输数据,需要收数据的组件对象接收事件并在回调函数中处理发来的数据。
- 关键:需要写一个所以组件对象都能访问到,且能够触发/绑定事件的对象
通过VueComponent对象
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
const VC = Vue.extend({}); // 创建一个VueComponent对象
Vue.prototype.x = new VC(); // 实例化一个vc对象, 并且添加到Vue的原型对象中
new Vue({
render: h => h(App),
}).$mount('#app');
可以实现,但是更推荐下面的写法。
通过Vue对象
此时,触发事件的对象实际上就是vm
。
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this; // 安装全局事件总线
}
}).$mount('#app');
TodoList改进
之前的自定义事件无法实现兄弟组件对象、爷孙组件对象的通信,使用事件总线完善TodoList:
/* App.vue */
export default {
name: "App",
// ...
mounted() {
this.$bus.$on('removeTodo', this.removeTodo); // 给bus绑定事件(接收数据)
this.$bus.$on('checkTodo', this.checkTodo);
},
beforeDestroy() {
this.$bus.$off('removeTodo'); // 组件对象销毁时解绑事件, 减轻bus负担
this.$bus.$off('checkTodo');
},
/* MyItem.vue */
export default {
name: "MyItem",
// ...
methods: {
handleChange() {
// this.$emit('checkTodo', this.todo.id);
this.$bus.$emit('checkTodo', this.todo.id); // 触发bus的事件(发送数据)
},
handleDelete() {
if(confirm('是否删除该事项?')) {
// this.$emit('removeTodo', this.todo.id);
this.$bus.$emit('removeTodo', this.todo.id); // 触发bus的事件(发送数据)
}
}
},
};
nextTick
在下一次DOM更新结束后执行回调函数。
- 当改变数据后,需要基于更新后的DOM进行操作时使用
例如下面的标签,其根据todo.isEdit
展示和隐藏:
<input ref="inputEdit" v-show="todo.isEdit" type="text">
handleEdit(event) {
if(this.todo.hasOwnProperty('isEdit')) {
this.todo.isEdit = true;
}
else {
this.$set(todo, 'isEdit', true);
}
// 当修改了isEdit时, 由于DOM还未更新, 直接调用focus()无法获取焦点
this.$nextTick(function() {
this.$refs.inputEdit.focus();
});
},
消息的订阅与发布
安装pubsub.js
:
npm i pubsub-js
还是上面的通信为例子:
/* App.vue */
import pubsub from "pubsub-js"
export default {
name: "App",
// ...
methods: {
// ...
// 注意回调函数, 第一个参数是消息名, 第二个才是传来的数据
removeTodo(msg, itemId) {
console.log(msg); // removeTodo, 即消息名
this.itemList = this.itemList.filter((todo) => {
return todo.id !== itemId;
});
},
},
mounted() {
this.pubId = pubsub.subscribe('removeTodo', this.removeTodo); // 订阅消息, 返回id
},
beforeDestroy() {
pubsub.unsubscribe(this.pubId); // 根据id取消订阅
}
};
/* MyItem.vue */
import pubsub from 'pubsub-js'
export default {
name: "MyItem",
// ...
methods: {
// handleChange() {...},
handleDelete() {
if(confirm('是否删除该事项?')) {
pubsub.publish('removeTodo', this.todo.id); // 发布消息
}
}
},
};
由于消息的订阅与发布需要安装第三方库,所以更推荐使用事件总线。
动画与过渡
v-enter
:进入的起点v-enter-active
:进入过程中v-enter-to
:进入的终点
动画效果
一个简单的动画:
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- appear表示页面初始化时是否播放动画 -->
<transition name="hello" appear>
<h1 v-show="isShow">你好!</h1>
</transition>
</div>
h1 {
background-color: orangered;
}
/* 如果没有在上面指定类名, 则默认为v-enter-active */
.hello-enter-active {
animation: show 1s linear;
}
.hello-leave-active {
animation: show 1s linear reverse;
}
/* 自定义动画 */
@keyframes show {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
过渡效果
通过过渡实现相同效果:
/* 进入的起点, 离开的终点 */
.hello-enter, .hello-leave-to {
transform: translateX(-100%);
}
/* 进入的终点, 离开的起点 */
.hello-enter-to, .hello-leave {
transform: translateX(0);
}
.hello-enter-active, .hello-leave-active {
transition: 1s linear;
}
当需要给多个元素过渡时,需要使用transition-group
,且指定key
:
<transition-group name="hello" appear>
<!-- key必需, 实际情况一般为v-for -->
<h1 v-show="isShow" key="1">你好!</h1>
<h1 v-show="!isShow" key="2">你好!</h1>
</transition-group>
第三方动画
推荐animate.css,使用npm
安装:
npm install animate.css
在JS引入:
<script>
import 'animate.css';
// export default {...};
</script>
修改<transition-group>
标签的3个属性,给进入和离开添加动画:
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__backInDown"
leave-active-class="animate__backOutDown"
>
<h1 v-show="isShow" key="1">你好111!</h1>
<h1 v-show="!isShow" key="2">你好222!</h1>
</transition-group>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现