更新记录
- 2023-09-14:对于“Mixins”作了补充。
非单文件组件
创建方法
-
定义组件
使用
Vue.extend(options)
创建,其中options
和new Vue(options)
时传入的options
几乎一样,但也有区别:-
el
不要写,因为最终所有的组件都要经过一个vm
的管理,由vm
中的el
才决定服务哪个容器。 -
data
必须写成函数,避免组件被复用时,数组存在单引用关系。直接将data写成对象,而不是一个函数,那么每个组件的data属性会共享同一个对象实例。这意味着当一个组件的data属性被修改时,其他组件的data属性也会受到影响,导致数据混乱和不可预料的行为。
为了避免这个问题,Vue要求将data属性写成一个函数。这样一来,每个组件实例都会调用该函数来返回一个新的数据对象。这样每个组件都会独立拥有自己的数据对象,互不干扰。
-
-
注册组件
- 局部注册:
new Vue()
的时候options
传入components
选项。 - 全局注册:
Vue.component('组件名', 组件)
- 局部注册:
-
使用组件
//定义school组件 const school = Vue.extend({ name:'school', template:` <div> <h2>学校名称:{{name}}</h2> <h2>学校地址:{{address}}</h2> <button @click="showName">点我提示学校名</button> </div> `, data(){ return { name:'尚硅谷', address:'北京' } }, methods: { showName(){ console.log('showName',this) } }, })
VueComponent与Vue的关系
- 一个重要的内置关系:
VueComponent.prototype. _proto === Vue.prototype
- 为什么要有这个关系:让组件实例对象VueComponent可以访问到Vue原型上的属性、方法。
创建组件的注意事项:
组件名:
- 一个单词组成
- 第一种写法(首字母小写):school
- 第二种写法(首字母大写):School(只能在
- 多个单词组成
- 第一种写法(kebab-case命名):my-school
- 第二种写法(CameCase命名):MySchool(需要Vue脚手架支持)
- 备注
- 可以使用name配置项指定组件在开发者工具中呈现的名字
组件标签:
- 第一种写法:
<school></school>
- 第二种写法:
<school/>
(需要Vue脚手架支持)
注意:
在HTML模板中使用组件时,首字母大写的形式只能在template配置项中的HTML代码中使用,或者在单文件组件的HTML中使用。
如果在像index.html这种纯html文件中使用组件时,必须使用小写单词+短横线的形式。
定义名字的时候尽量使用两个或以上的单词,避免和原生的HTML标签名冲突。
组件数据的流向设计:属性向下,事件向上,即父组件只能向子组件传递数据,子组件不能修改父组件的属性,父组件也不能访问子组件中的属性,子组件可通过事件触发父组件中的事件。
props:
组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。也就是
props
是子组件访问父组件数据的唯一接口。
- 单向数据流
- 父组件属性变化,子组件自动刷新
- 子组件不能直接修改父组件属性
你可以基于对象的语法使用以下选项:
type
: 可以是下列原生构造函数中的一种:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。Prop 类型的更多信息在此。default
:any
为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。required
:Boolean
定义该 prop 是否是必填项。在非生产环境中,如果这个值为 truthy 且该 prop 没有被传入的,则一个控制台警告将会被抛出。validator
:Function
自定义验证函数会将该 prop 的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。你可以在这里查阅更多 prop 验证的相关信息。
示例:
props: ["sender", "msg", "time"]
props:{
isStudent:{
type:Boolean,
default:true // 可声明默认值
}
}
// 不同类型的数据,注意默认值的返回类型
props: {
user: {
type: Object,
default() {
return { username: "abc" };
},
},
followers: {
type: Array,
default() {
return []; // 即使是空数组,也需要从函数中返回
},
},
handleClick: {
type: Function,
default() {
console.log("点击事件");
},
},
},
props: {
time: {
type: Number,
// 可以在props中校验数据
validator(value) {
return value > 0;
},
},
sender: {
type: String,
},
msg: {
type: String,
},
},
在父组件中,首先需要引入组件:
<script>
import MessageItem from "./components/MessageItem.vue"
export default {
components:{
MessageItem
}
}
</script>
向子组件传值:
<!-- 使用v-bind才可以传送数据本身的属性值 -->
<MessageItem :user={haha:222} :time=2022 sender="q"></MessageItem>
emit:
子组件可以使用 $emit 触发父组件的自定义事件。
Vue虽然也支持通过props向父组件传递数据,但如此做子组件就会依赖父组件,即如果父组件没有传递参数,且子组件的事件处理函数也没有默认值,那么子组件调用事件处理函数就会报出异常;而使用emit事件传递则没有这个问题,如果父组件不处理子组件的事件也没有任何问题,因为子组件不依赖父组件的事件监听。
-
参数
{string} eventName
[...args]
触发当前实例上的事件。附加参数都会传给监听器回调。
-
示例
在子组件中,定义emits数组以及触发事件,可添加多个参数:
<script> export default { emits: ["deletePosts"] }; </script>
<button @click="$emit('deletePosts', 5)"></button>
也可自定义方法,例如:
<button @click="selectPosts(5)"></button>
<script> export default { methods:{ selectPosts(id){ this.$emit("deletePosts",id); // selectPosts事件触发后,自动触发deletePosts事件 } } } </script>
在父组件中,首先需要引入组件:
<script> import MessageItem from "./components/MessageItem.vue" export default { components:{ MessageItem } } </script>
父组件触发方法:
<MessageItem @deletePosts="handleDeletePosts"></MessageItem>
<script> import MessageItem from "./components/MessageItem.vue" export default { components:{ MessageItem }, methods:{ handleDeletePosts(id){ console.log(id); } } } </script>
provide和inject:
在实际开发中,可能需要传递数据到更深层次的组件,例如子组件的子组件,即使中间的组件没有用到这个属性也要层层传递。
Vue中提供了provide
配置项来向所有下层组件传递数据,而用到数据的组件可以使用inject
配置项来获取数据:
- 类型:
- provide:
Object | () => Object
- inject:
Array<string> | { [key: string]: string | Symbol | Object }
- provide:
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol
和 Reflect.ownKeys
的环境下可工作。
inject
选项应该是:
- 一个字符串数组,或
- 一个对象,对象的 key 是本地的绑定名,value 是:
- 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
- 一个对象,该对象的:
from
属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
属性是降级情况下使用的 value
注意:
provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
-
示例:
MovieCard.vue文件:
<template> <div class="card"> <MovieItem :title="movie.title" :description="movie.description" /> </div> </template> <script> import MovieItem from "./MovieItem.vue"; export default { components: { MovieItem, }, data() { return { movie: { title: "电影", description: "这是一段电影的描述", }, }; }, }; </script> <style scoped> .card { padding: 12px; border: 1px solid hsl(240deg, 100%, 80%); border-radius: 4px; } </style>
MovieItem.vue文件:
<template> <MovieTitle :title="title" /> <div class="description"> {{ description }} </div> </template> <script> import MovieTitle from "./MovieTitle.vue"; export default { components: { MovieTitle }, props: ["title", "description"], }; </script> <style scoped> .description { margin-top: 12px; color: hsl(240deg, 10%, 88%); } </style>
MovieTitle.vue文件:
<template> <h2>{{ title }}</h2> </template> <script> export default { props: ["title"], }; </script> <style scoped> h2 { color: hsl(240deg, 100%, 80%); } </style>
可以看出组件的层级关系,中间组件尽管没有使用到title属性,但仍需要将其加入到props配置项中,向下传递到MovieTitle.vue文件中。若使用
provide
和inject
配置项,则简洁很多:<template> <div class="card"> <!-- <MovieItem :title="movie.title" :description="movie.description" /> --> <MovieItem :description="movie.description" /> </div> </template> <script> import MovieItem from "./MovieItem.vue"; export default { components: { MovieItem, }, data() { return { movie: { title: "电影", description: "这是一段电影的描述", }, }; }, provide: { title: "测试电影", }, }; </script> <style scoped> .card { padding: 12px; border: 1px solid hsl(240deg, 100%, 80%); border-radius: 4px; } </style>
<template> <!-- <MovieTitle :title="title" /> --> <MovieTitle /> <div class="description"> {{ description }} </div> </template> <script> import MovieTitle from "./MovieTitle.vue"; export default { components: { MovieTitle }, // props: ["title", "description"], props: ["description"], }; </script> <style scoped> .description { margin-top: 12px; color: hsl(240deg, 10%, 88%); } </style>
<template> <h2>{{ title }}</h2> </template> <script> export default { // props: ["title"], inject: ["title"], }; </script> <style scoped> h2 { color: hsl(240deg, 100%, 80%); } </style>
但如此做
provide
中的title属性没有与data中的title属性相对应,若修改为以下代码:provide: { title: this.movie.title },
则会报错:
原因是如果需要
provide
提供data中的属性,则需要将provide
写为一个函数,类似data中的形式:provide() { return { title: this.movie.title, }; }
如何修改子组件中的样式:
对于子组件中的根元素,可通过普通CSS样式书写格式进行修改,若想为子组件中除根元素以外的元素,则需要使用deep
选择器:
.text :deep(a) {
display: block;
text-decoration: none;
color: hsl(0deg, 80%, 70%);
}
ref:
Vue提供了一种机制,可以获取到组件的实例并访问其中的属性,即为ref
。使用ref
可以访问到原生的HTML的DOM实例,也可以获取Vue组件的实例,但会破坏数据的流向,所以万不得已的时候最好不要使用。
在子组件中:
<template>
<input type="text" v-model="inputText" ref="inputControl" />
</template>
<script>
export default {
data() {
return {
inputText: "",
};
},
mounted() {
this.$refs.inputControl.focus();
},
methods: {
blur() {
this.$refs.inputControl.blur();
},
},
};
</script>
<style scoped>
input {
padding: 8px 14px;
border: 1px solid hsl(280deg, 50%, 50%);
border-radius: 4px;
outline: none;
background: hsl(280deg, 50%, 30%, 0.2);
color: white;
}
</style>
在父组件中:
<template>
<main>
<div>
<AutoFocus ref="autofocus" />
</div>
</main>
</template>
<script>
import AutoFocus from "./components/AutoFocus.vue";
export default {
components: {
AutoFocus,
},
mounted() {
setTimeout(() => {
console.log(this.$refs.autofocus.inputText);
this.$refs.autofocus.blur();
}, 5000);
},
};
</script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}
body {
background-color: #0f141c;
opacity: 1;
background-image: radial-gradient(
#212943 0.6000000000000001px,
#0f141c 0.6000000000000001px
);
background-size: 12px 12px;
color: white;
}
#app {
width: 100vw;
height: 100vh;
max-width: 100%;
display: grid;
place-items: center;
}
</style>
让组件支持v-model指令:
示例:
子组件SearchInput.vue文件:
<template>
<label>
<span>搜索:</span>
<input type="text" />
</label>
</template>
<script>
export default {};
</script>
<style scoped>
input {
padding: 8px 14px;
border: 1px solid hsl(280deg, 50%, 50%);
border-radius: 4px;
outline: none;
background: hsl(280deg, 50%, 30%, 0.2);
color: white;
}
</style>
父组件App.vue文件:
<template>
<main>
<div>
<SearchInput v-model="searchTerm"/>
<p>{{searchTerm}}</p>
</div>
</main>
</template>
<script>
import SearchInput from "./components/SearchInput.vue";
export default {
components: {
SearchInput,
},
data(){
return{
SearchTerm: ""
}
}
};
</script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}
body {
background-color: #0f141c;
opacity: 1;
background-image: radial-gradient(
#212943 0.6000000000000001px,
#0f141c 0.6000000000000001px
);
background-size: 12px 12px;
color: white;
}
#app {
width: 100vw;
height: 100vh;
max-width: 100%;
display: grid;
place-items: center;
}
p {
margin-top: 12px;
}
</style>
此时v-model并不能生效,需要在子组件中添加属性:
<template>
<label
><span>搜索:</span
><input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</label>
</template>
<script>
export default {
props: ["modelValue"],
emits: ["update:modelValue"],
};
</script>
<style scoped>
input {
padding: 8px 14px;
border: 1px solid hsl(280deg, 50%, 50%);
border-radius: 4px;
outline: none;
background: hsl(280deg, 50%, 30%, 0.2);
color: white;
}
</style>
若需要多个v-model,添加更多属性,并加以区分:
<template>
<label
><span>搜索:</span
><input
type="text"
:value="searchTerm"
@input="$emit('update:searchTerm', $event.target.value)"
/>
</label>
<label>
<span>类别:</span>
<select
:value="category"
@change="$emit('update:category', $event.target.value)"
>
<option value="default">默认</option>
<option value="fontend">前端</option>
<option value="backend">后端</option>
<option value="fullstack">全栈</option>
</select>
</label>
</template>
<script>
export default {
props: ["searchTerm", "category"],
emits: ["update:searchTerm", "update:category"],
};
</script>
<style scoped>
label {
display: block;
margin-bottom: 20px;
}
input {
padding: 8px 14px;
border: 1px solid hsl(280deg, 50%, 50%);
border-radius: 4px;
outline: none;
background: hsl(280deg, 50%, 30%, 0.2);
color: white;
}
select {
border: 1px solid hsl(280deg, 50%, 50%);
background: none;
color: white;
padding: 6px;
border-radius: 4px;
}
</style>
<template>
<main>
<div>
<!-- v-model 后面的参数必须和子组件接收的属性名相同,例如 searchTerm -->
<SearchInput
v-model:searchTerm="searchTerm"
v-model:category="category"
/>
<div class="splitLine"></div>
<p>搜索词:{{ searchTerm }}</p>
<p>类别:{{ category }}</p>
</div>
</main>
</template>
<script>
import SearchInput from "./components/SearchInput.vue";
export default {
components: {
SearchInput,
},
data() {
return {
// 名字无需和 SearchInput 中的属性名相同
// 例如这里可以叫 searchQuery,
searchTerm: "",
category: "default",
};
},
};
</script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}
body {
background-color: #0f141c;
opacity: 1;
background-image: radial-gradient(
#212943 0.6000000000000001px,
#0f141c 0.6000000000000001px
);
background-size: 12px 12px;
color: white;
}
#app {
width: 100vw;
height: 100vh;
max-width: 100%;
display: grid;
place-items: center;
}
.splitLine {
width: 100%;
height: 1px;
border-bottom: 1px dashed hsl(280deg, 50%, 50%);
}
p {
margin-top: 12px;
color: hsl(50deg, 100%, 80%);
}
</style>
使用动态组件渲染不同的HTML标签:
使用
:is
+HTML标签(可能需要用到计算属性)
父组件App.vue文件:
<template>
<main>
<div>
<!-- 动态 HTML 元素 -->
<TextHeading level="1">一级标题</TextHeading>
<TextHeading level="2">二级标题</TextHeading>
<TextHeading level="3">三级标题</TextHeading>
<TextHeading level="4">四级标题</TextHeading>
<TextHeading level="5">五级标题</TextHeading>
<TextHeading level="6">六级标题</TextHeading>
</div>
</main>
</template>
<script>
import TextHeading from "./components/TextHeading.vue";
export default {
components: {
TextHeading,
},
};
</script>
子组件TextHeading.vue文件:
<template>
<Component :is="heading"><slot></slot></Component>
</template>
<script>
export default {
props: ["level"],
computed: {
heading() {
return `h${this.level}`;
},
},
};
</script>
<style scoped>
h1 {
font-size: 3em;
}
h2 {
font-size: 2.4em;
}
h3 {
font-size: 1.8em;
}
h4 {
font-size: 1.4em;
}
h5 {
font-size: 1.2em;
}
h6 {
font-size: 1em;
}
</style>
使用动态组件渲染不同的Vue组件:
使用
:is
+组件名称
例如此功能实现两个表单组件的切换:
登录表单文件ProfileForm.vue:
<template>
<form @submit.prevent>
<label>昵称:<input type="text" /></label>
<label>生日:<input type="date" /></label>
<label>地址:<input type="text" /></label>
</form>
</template>
<script>
export default {};
</script>
<style scoped>
::-webkit-calendar-picker-indicator {
filter: invert(1);
}
</style>
注册表单文件RegisterForm.vue:
<template>
<form @submit.prevent>
<label>手机号:<input type="number" /></label>
<label
>验证码:<input type="number" /><button class="sendSMSCodeBtn">
发送验证码
</button></label
>
</form>
</template>
<script>
export default {};
</script>
<style scoped>
.sendSMSCodeBtn {
margin-left: 24px;
}
</style>
父组件App.vue文件:
<template>
<main>
<div>
<Component :is="currentForm" />
<div class="buttons">
<button
v-if="currentForm === 'RegisterForm'"
@click="currentForm = 'ProfileForm'"
>
下一步
</button>
<template v-else-if="currentForm === 'ProfileForm'">
<button @click="currentForm = 'RegisterForm'">
上一步
</button>
<button>完成</button>
</template>
</div>
</div>
</main>
</template>
<script>
import RegisterForm from "./components/RegisterForm.vue";
import ProfileForm from "./components/ProfileForm.vue";
export default {
components: {
RegisterForm,
ProfileForm,
},
data() {
return {
currentForm: "RegisterForm",
};
},
};
</script>
注意,如果仅使用这种方法切换表单,会导致表单数据缺失,原因是因为使用动态组件每次切换组件时,都会创建新的组件实例。
改进:
在
Component
标签外绑定KeepAlive
标签
<!-- 动态组件默认每次渲染都会重新创建,数据会丢失,使用 <KeepAlive></KeepAlive> 组件可以缓存组件,避免数据丢失 -->
<KeepAlive>
<Component :is="currentForm" />
</KeepAlive>
组件传送
在其他DOM元素挂载组件:
某些组件可能在逻辑上不属于任何父组件,例如页面边缘的提示框,它们会根据
body
元素进行绝对定位。如果我们将它们放在某个父组件中,那么它们的位置就会限制于父组件容器内。
Vue提供了
teleport
属性,可以让子组件挂载在其他页面元素上。
<template>
<Teleport to="body">
<div v-if="show" class="alertBox">
<div class="closeIcon" @click="show = false">X</div>
<div class="content">
<slot>消息提示框组件</slot>
</div>
</div>
</Teleport>
</template>
多次传送组件:
如果一个组件多次传送到相同的页面下,那么传送的顺序会追加到页面中,例如要弹出多个消息提示框:
在index.html文件中添加:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<!-- 添加messages -->
<div id="messages"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
在父组件App.vue文件中:
<template>
<main>
<div class="container">
<button @click="msgs.push(`这是一段消息${msgs.length + 1}`)">
添加消息
</button>
<AlertBox v-for="msg in msgs">{{ msg }}</AlertBox>
</div>
</main>
</template>
<script>
import AlertBox from "./components/AlertBox.vue";
export default {
components: {
AlertBox,
},
data() {
return {
msgs: [],
};
},
};
</script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}
body {
background-color: #0f141c;
opacity: 1;
background-image: radial-gradient(
#212943 0.6000000000000001px,
#0f141c 0.6000000000000001px
);
background-size: 12px 12px;
color: white;
}
#app {
width: 100vw;
height: 100vh;
max-width: 100%;
display: grid;
place-items: center;
}
button {
border: none;
background: linear-gradient(
90deg,
hsl(240deg, 50%, 50%),
hsl(280deg, 50%, 50%)
);
padding: 12px 18px;
margin-top: 12px;
border-radius: 4px;
color: white;
}
.container {
position: relative;
/* border: 1px solid hsl(280deg, 100%, 50%); */
}
/*注意此处布局*/
#messages {
position: absolute;
right: 12px;
bottom: 12px;
display: flex;
flex-direction: column-reverse;
gap: 12px;
}
</style>
<template>
<!-- 此处改为#messages -->
<Teleport to="#messages">
<div v-if="show" class="alertBox">
<div class="closeIcon" @click="show = false">X</div>
<div class="content">
<slot>消息提示框组件</slot>
</div>
</div>
</Teleport>
</template>
<script>
export default {
data() {
return {
show: true,
};
},
mounted() {
//可添加计时器,3s后关闭提示框
setTimeout(() => {
this.show = false;
}, 3000);
},
};
</script>
<style scoped>
.alertBox {
width: 350px;
height: 80px;
border: 1px solid hsl(280, 100%, 50%);
border-radius: 8px;
padding: 24px;
/* position: absolute;
right: 12px;
bottom: 12px; */
/*改为相对定位,因为关闭按钮在其中*/
position: relative;
}
.content {
height: 100%;
display: flex;
align-items: center;
}
.closeIcon {
position: absolute;
right: 12px;
top: 12px;
font-size: 12px;
font-weight: 900;
cursor: pointer;
}
</style>
Mixins:
概念
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。
本质其实就是一个js
对象,它可以包含我们组件中任意功能选项,如data
、components
、methods
、created
、computed
等等
我们只要将共用的功能以对象的方式传入 mixins
选项中,当组件使用 mixins
对象时所有mixins
对象的选项都将被混入该组件本身的选项中来
在Vue
中我们可以局部混入跟全局混入
局部混入
定义一个mixin
对象,有组件options
的data
、methods
属性
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
组件通过mixins
属性调用mixin
对象
var component = new Component() // => "hello from mixin!"
该组件在使用的时候,混合了mixin
里面的方法,在自动执行created
生命钩子,执行hello
方法
全局混入
通过Vue.mixin()
进行全局的混入
import { createApp } from "vue";
import App from "./App.vue";
import PaginationMixin from "./mixins/PaginationMixin";
const app = createApp(App);
app.mixin(PaginationMixin);
app.mixin({
siteTitle: "我的 Vue 应用",
computed: {
siteTitle() {
return this.$options.siteTitle;
},
},
});
app.mount("#app");
<template>
<main>
<div class="container">
<h1>
{{ siteTitle }}
</h1>
<PaginationComponent
:totalPage="totalPage"
:defaultCurrentPage="currentPage"
/>
<PaginationComponent2
:totalPage="totalPage"
:defaultCurrentPage="currentPage"
/>
<BaseButton :defaultCurrentPage="currentPage" />
</div>
</main>
</template>
<script>
import BaseButton from "./components/BaseButton.vue";
import PaginationComponent from "./components/PaginationComponent.vue";
import PaginationComponent2 from "./components/PaginationComponent2.vue";
export default {
siteTitle: "Mixin 全局注册",
components: {
PaginationComponent,
PaginationComponent2,
BaseButton,
},
data() {
return {
totalPage: 6,
currentPage: 4,
};
},
};
</script>
使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)
PS:全局混入常用于插件的编写
注意事项
-
当组件存在与
mixin
对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin
的选项 -
但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行
mixin
的钩子,再执行组件的钩子(而不是覆盖)
使用场景
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立
这时,可以通过Vue
的mixin
功能将相同或者相似的代码提出来
定义一个modal
弹窗组件,内部通过isShowing
来控制显示
const Modal = {
template: '#modal',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}
定义一个tooltip
提示框,内部通过isShowing
来控制显示
const Tooltip = {
template: '#tooltip',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}
通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin
就派上用场了
首先抽出共同代码,编写一个mixin
const toggle = {
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}
两个组件在使用上,只需要引入mixin
const Modal = {
template: '#modal',
mixins: [toggle]
};
const Tooltip = {
template: '#tooltip',
mixins: [toggle]
}
通过上面小小的例子,让我们知道了Mixin
对于封装一些可复用的功能如此有趣、方便、实用
源码分析
首先从Vue.mixin
入手
源码位置:/src/core/global-api/mixin.js
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
主要是调用merOptions
方法
源码位置:/src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并
}
for (key in child) {
if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了
mergeField(key) // 处理child中的key 也就parent中没有处理过的key
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
}
return options
}
从上面的源码,我们得到以下几点:
- 优先递归处理
mixins
- 先遍历合并
parent
中的key
,调用mergeField
方法进行合并,然后保存在变量options
- 再遍历
child
,合并补上parent
中没有的key
,调用mergeField
方法进行合并,保存在变量options
- 通过
mergeField
函数进行了合并
下面是关于Vue
的几种类型的合并策略
- 替换型
- 合并型
- 队列型
- 叠加型
替换型
替换型合并有props
、methods
、inject
、computed
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
const ret = Object.create(null) // 创建一个第三方对象 ret
extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中
if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中
return ret
}
strats.provide = mergeDataOrFn
同名的props
、methods
、inject
、computed
会被后来者代替
合并型
和并型合并有:data
strats.data = function(parentVal, childVal, vm) {
return mergeDataOrFn(
parentVal, childVal, vm
)
};
function mergeDataOrFn(parentVal, childVal, vm) {
return function mergedInstanceDataFn() {
var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象
var parentData = parentVal.call(vm, vm)
if (childData) {
return mergeData(childData, parentData) // 将2个对象进行合并
} else {
return parentData // 如果没有childData 直接返回parentData
}
}
}
function mergeData(to, from) {
if (!from) return to
var key, toVal, fromVal;
var keys = Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
// 如果不存在这个属性,就重新设置
if (!to.hasOwnProperty(key)) {
set(to, key, fromVal);
}
// 存在相同属性,合并对象
else if (typeof toVal =="object" && typeof fromVal =="object") {
mergeData(toVal, fromVal);
}
}
return to
}
mergeData
函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:
- 当目标 data 对象不包含当前属性时,调用
set
方法进行合并(set方法其实就是一些合并重新赋值的方法) - 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性
队列性
队列性合并有:全部生命周期和watch
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// watch
strats.watch = function (
parentVal,
childVal,
vm,
key
) {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
/* istanbul ignore if */
if (!childVal) { return Object.create(parentVal || null) }
{
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
生命周期钩子和watch
被合并为一个数组,然后正序遍历一次执行
叠加型
叠加型合并有:component
、directives
、filters
strats.components=
strats.directives=
strats.filters = function mergeAssets(
parentVal, childVal, vm, key
) {
var res = Object.create(parentVal || null);
if (childVal) {
for (var key in childVal) {
res[key] = childVal[key];
}
}
return res
}
叠加型主要是通过原型链进行层层的叠加
小结
- 替换型策略有
props
、methods
、inject
、computed
,就是将新的同名参数替代旧的参数 - 合并型策略是
data
, 通过set
方法进行合并和重新赋值 - 队列型策略有生命周期函数和
watch
,原理是将函数存入一个数组,然后正序遍历依次执行 - 叠加型有
component
、directives
、filters
,通过原型链进行层层的叠加
异步组件加载:
对于大型项目,一次性加载所有组件会影响页面的首次打开速度。正确方法应该是根据用户的操作与导航,加载相应的组件。
defineAsyncComponent
:接收一个回调函数作为参数,可用于异步加载组件。
import { defineAsyncComponent } from "vue";
const ProductPage = defineAsyncComponent(() =>
import("./components/ProductPage.vue")
);
组件错误处理:
全局错误处理:
errorHandler
:
-
类型:
Function
-
默认值:
undefined
-
用法:
Vue.config.errorHandler = function (err, vm, info) { // handle error // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子 // 只在 2.2.0+ 可用 }
指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是
undefined
时,被捕获的错误会通过console.error
输出而避免应用崩溃。从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。
从 2.6.0 起,这个钩子也会捕获
v-on
DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。
示例:
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.config.errorHandler = (err, vm, info) => {
console.log(err);
console.log(vm);
console.log(info);
};
app.mount("#app");
错误边界:
如果某个Vue组件提供了错误处理能力,那么它就被称为错误边界。Vue组件会以事件冒泡方式向父组件传递错误信息,只有当父组件处理了错误并手动停止了错误的传播,错误才不会继续向上传递,这样这个子组件所有的子组件发生的错误都会交给他进行处理。
示例:
<template>
<main>
<div>
<p v-if="error">啊哦,我是错误的顶级组件了!</p>
<!-- <AppList v-else :data="data" /> -->
<template v-else>
<AppList :data="data" />
<AppButton>测试按钮</AppButton>
</template>
</div>
</main>
</template>
<script>
import AppList from "./components/AppList.vue";
import AppButton from "./components/AppButton.vue";
export default {
components: {
AppList,
AppButton,
},
data() {
return {
data: [1, 2, 3],
error: false,
};
},
errorCaptured() {
this.error = true;
// 停止错误向上传播
return false;
},
};
</script>