主讲:云隐
优势一:模板化
插槽 - 模板更加灵活
- 默认插槽:组件外部维护参数以及结构,内部安排放置位置;
- 具名插槽:以
name标识插槽的身份,从而在组件内部可以做到区分开来; - 作用域插槽:可以接受
props(scope-slot);
例子:
src\views\HomeView.vue
<template>
<div class="home">
<HelloWorld msg="Welcome to Your Vue.js App">
<p>{{ msg }}</p>
<template v-slot:header>{{ header }}</template>
<template v-slot:body>{{ body }}</template>
<template v-slot:footer>{{ footer }}</template>
<!-- 老版本写法 -->
<!-- <template slot="content" slot-scope="{ slotProps }">{{ slotProps }}</template> -->
<!-- 新版本写法 -->
<template v-slot:content2="{ slotProps2 }">{{ slotProps2 }}</template>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue';
export default {
name: 'HomeView',
data() {
return {
msg: 'zhaowa start',
header: 'zhaowa header',
body: 'zhaowa body',
footer: 'zhaowa footer',
};
},
components: {
HelloWorld,
},
};
</script>
src\components\HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<!-- 默认插槽 -->
<h3>默认插槽</h3>
<slot></slot>
<!-- 具名插槽 -->
<h3>具名插槽</h3>
<div class="header">
<slot name="header"></slot>
</div>
<slot></slot>
<div class="body">
<slot name="body"></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
<!-- 作用域插槽 -->
<h3>作用域插槽</h3>
<div>
<slot name="content" :slotProps="slotProps"></slot>
</div>
<div>
<slot name="content2" :slotProps2="slotProps2"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
},
data() {
return {
slotProps: 'slotProps start',
slotProps2: 'slotProps2 start',
};
},
};
</script>
模板数据的二次加工上
过滤器
// 使用:管道符 |
{{ timer | format }}
<template>
<div class="hello">
<!-- 过滤器 -->
<h3>money:{{ money | moneyFilter }}</h3>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
money: 100,
};
},
filters: {
// 声明过滤器
moneyFilter(money) {
// 面试题:filter 过滤器中的 this 不指向实例
let res = money > 99 ? 99 : money;
return res && res.toFixed(2);
},
},
};
</script>
面试题:filter 过滤器中的 this 不指向实例
v-html … 指令化
<template>
<div class="hello">
<!-- v-html 也可以处理逻辑 -->
<h3 v-html="money > 99 ? 99 : money"></h3>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
money: 100,
};
}
};
</script>
jsx
<script>
export default {
name: 'HelloWorld1',
data() {
return {
options: [
{
value: 1,
text: 1,
},
{
value: 2,
text: 2,
},
{
value: 3,
text: 3,
},
],
money: 100,
};
},
methods: {
handleClick() {
console.log('==== handleClick ====');
},
},
// node => script(return node)
render(h) {
// 手写节点
const moneyNode = <p>{this.money > 99 ? 99 : this.money}</p>;
return (
<ul>
{
// jsx 实现数组遍历
this.options.map((item, index) => {
return (
// <li>{ item.text }</li>
// 组件引入 - 属性 & 事件
<content
item={item}
value={item.value}
key={index}
onClick={this.handleClick}
>
{moneyNode}
</content>
);
})
}
</ul>
);
},
};
</script>
优势二: 组件化
传统模板化组件
// 注册
Vue.component('component', {
template: '<h2>Essential Links</h2>'
})
// 创建实例
new Vue({
el: '#app'
})
混入 mixin
-
应用场景:抽离公共逻辑(逻辑相同,但是模板不一样);
-
缺点:数据来源不太明确;
export default {
data() {
return {
msg: '我是 mixin',
obj: {
title: 'mixinTitle',
header: 'mixinHeader',
},
};
},
created() {
console.log('mixin created');
},
};
面试题:合并策略
data冲突时,以组件主体为优先;- 生命周期钩子会先后执行,先
mixin后主体; - 递归合并,递归合并优先级仍以主体优先;
// src\components\fragments\demoMixin.js
export default {
data() {
return {
msg: '我是 mixin',
obj: {
title: 'mixinTitle',
header: 'mixinHeader',
},
};
},
created() {
console.log('mixin created');
},
};
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
import demoMixin from '@/components/fragments/demoMixin';
export default {
name: 'HelloWorld',
mixins: [demoMixin],
data() {
return {
msg: 'Welcome to Your Vue.js App',
obj: {
number: 123,
header: 'zhaowa header',
},
};
},
created() {
console.log('hello world created');
},
};
</script>
继承拓展 extends
- 应用场景:拓展独立逻辑;
面试题:合并策略 - 与 mixin 相同
- 合并优先级上
mixin > extends; - 声明周期回调优先级
extends > mixin;
// src\components\fragments\demoExtends.js
export default {
data() {
return {
msg: '我是 extends',
obj: {
title: 'extendsTitle',
header: 'extendsHeader',
},
};
},
created() {
console.log('extends created');
},
};
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>{{ obj.header }}</p>
</div>
</template>
<script>
import demoMixin from '@/components/fragments/demoMixin';
import demoExtends from '@/components/fragments/demoExtends';
export default {
name: 'HelloWorld2',
mixins: [demoMixin],
extends: demoExtends,
data() {
return {
msg: 'Welcome to Your Vue.js App',
obj: {
number: 123,
},
};
},
created() {
console.log('hello world created');
},
};
</script>
整体拓展 - extend
从预定义的配置中拓展一个独立配置项,并且进行 合并;
- 路径:
src\main.js
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
// 拓展一个构造器
let _baseOptions = {
data() {
return {
course: 'zhaowa',
session: 'vue',
teacher: 'yy',
};
},
created() {
console.log('extend base');
},
};
const BaseComponent = Vue.extend(_baseOptions);
// 基于 BaseComponent 再拓展逻辑
new BaseComponent({
created() {
console.log('extend created');
},
});
插件 - Vue.use(VueRouter)
- 注册外部插件,作为整体实例能力的补充;
- 自动做除重,防止多次重复注册相同插件;
- 编写插件:
- 外部使用
Vue.use(myPlugin, options); - 内部默认调用
install;
- 外部使用
// src\plugins\myPlugins.js
export default {
install: (Vue, options) => {
// 添加全局方法或属性
Vue.globalMethod = function () {
console.log('globalMethod');
};
// 全局指令
Vue.directive('my-directive', {
bind(el, binding, vnode) {},
inserted(el, binding, vnode) {},
update(el, binding, vnode) {},
componentUpdated(el, binding, vnode) {},
unbind(el, binding, vnode) {},
});
// 全局 mixin
Vue.mixin({
created() {
console.log('plugin mixin created');
},
});
// 全局方法
Vue.prototype.$yyMethod = function () {
console.log('yyMethod');
// 使用:this.$yyMethod
};
},
};
// src\main.js
import myPlugins from '@/plugins/myPlugins';
// 加载拓展插件
// 插件配置
const _options = {
name: 'my plugin',
};
Vue.use(myPlugins, _options);
扩展
插槽
Vue 2.6.0之后采用全新v-slot语法取代之前的slot、slot-scope。
一、匿名插槽(默认插槽)
- 父组件:
<!-- 匿名插槽 -->
<Comp1></Comp1>
<Comp1>我是组件1的匿名插槽</Comp1>
- 子组件:
<div>
<slot>如果父组件中传递数据,会替换此处内容</slot>
</div>
二、具名插槽
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #,例如 v-slot:header 可以被重写为 #header。
- 父组件:
<!-- 具名插槽 -->
<Comp2>
<!-- <template>我是组件2的匿名插槽</template> -->
<!-- 或者 -->
<!-- <template v-slot:default>我是组件2的匿名插槽</template> -->
<!-- 或者 -->
<template #default>我是组件2的匿名插槽</template>
<template v-slot:content>具名插槽1</template>
<template #content2>具名插槽2</template>
</Comp2>
- 子组件:
<div>
<slot>comp-slot 后备内容</slot>
<br />
<slot name="content">具名插槽</slot>
<br />
<slot name="content2">具名插槽</slot>
</div>
三、作用域插槽
一般情况下,父组件中显示数据要声明的父组件中,使用作用域插槽父组件中要显示的数据可以由子组件决定。
- 父组件:
<!-- 作用域插槽 -->
<Comp3>
<!-- <template v-slot:default="ctx">我是组件3的作用域插槽:{{ ctx.foo }}</template> -->
<!-- 或者 -->
<template #default="ctx">我是组件3的作用域插槽:{{ ctx.foo }}</template>
<template #content="ctx">我是组件3的作用域插槽:{{ ctx.dong }}</template>
<!-- 解构 -->
<template #content2="{ dong }">我是组件3的作用域插槽:{{ dong }}</template>
</Comp3>
- 子组件:
<template>
<div>
<slot :foo="foo"></slot>
<br />
<slot name="content" dong="dong~~"></slot>
<br />
<slot name="content2" :dong="dong"></slot>
</div>
</template>
<script>
export default {
data() {
return {
foo: 'Comp3 的 foo',
dong: 'Comp3 的 dong'
};
}
};
</script>
四、动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名。
- 父组件:
<!-- 动态插槽名 -->
<Comp4>
<template v-slot:[dynamicSlotName]="{ tua }">我是组件4的动态插槽名:{{ tua }}</template>
</Comp4>
<script>
export default {
data() {
return {
dynamicSlotName: 'comp4SlotName'
};
}
};
</script>
- 子组件:
<template>
<div>
<slot name="comp4SlotName" :tua="tua"></slot>
</div>
</template>
<script>
export default {
data() {
return {
tua: 'Comp4 的 tua'
};
}
};
</script>
插槽的解析的流程
对于解析的流程,我们可以分为以下的几个步骤:
- 解析父组件的
vDOM,以配置的形式解析出来每个数组元素的内容; - 将父组件内部的内容转移至子组件
options的renderChildren上(initInternalComponent); - 子组件转移
renderChildren到vm.$slots上 (initRender); - 解析子组件
vDOM,将模版中的slot占位符替换为_t函数,最终_t执行,从vm.$slots中替换渲染内容(renderMixin);
表单组件实现
实现要求:
模仿 Element-UI 的表单,分为三层结构:Form 表单组件、FormItem 表单项组件、Input 和 CheckBox 组件,具体分工如下:
Form表单组件:- 实现:预留插槽、管理数据模型
model、自定义校验规则rules、全局校验方法validate;
- 实现:预留插槽、管理数据模型
FormItem表单项组件:- 实现:预留插槽、显示
label标签、执行数据校验、显示校验结果;
- 实现:预留插槽、显示
Input和CheckBox组件:- 实现:绑定数据模型
v-model、通知FormItem组件执行校验;
- 实现:绑定数据模型
数据校验:
- 思路:校验发生在
FormItem,它需要知道何时校验(让Input通知它),还需要知道怎么校验(注入校验规则); - 任务1:
Input通知校验;
onInput(e) {
// $parent 指的 FormItem
this.$parent.$emit("validate");
}
- 任务2:
FormItem监听校验规则,获取规则并执行校验
inject: ["Form"], // 注入
mounted() {
// 监听校验事件
this.$on("validate", () => {
this.validate();
})
},
methods: {
validate() {
// 执行组件校验
// 1、获取校验规则
const rules = this.form.rules[this.prop];
// 2、获取数据
const value = this.form.model[this.prop];
// 3、执行校验
const desc = {
[this.prop]: rules
};
const schema = new Schema(desc);
// 参数1是值,参数2是校验错误对象数组
// 返回的是 Promise<boolean>
return schema.validate({ [this.prop]: value }, errors => {
if (errors) {
this.errorMessage = errors[0].message;
} else {
this.errorMessage = "";
}
});
}
}
- 任务3:表单全局验证,为
Form提供validate方法
validate(cb) {
// map 的结果是若干 Promise 数组
const tasks = this.$children.filter(item => item.prop).map(item => item.validate());
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false));
}
代码实现:
Input.vue- 双向绑定:
@input、:value - 派发校验事件
- 双向绑定:
<template>
<div>
<!--
1、自定义组件要实现 v-model 必须实现 :value 和 @input
2、通知 FormItem 执行校验
3、密码组件不应该明文显示,使用 v-bind="$attrs"
$attrs 存储的是 props 之外的部分
4、设置 inheritAttrs 为 false, 避免顶层容器继承属性
-->
<input :value="value" @input="onInput" v-bind="$attrs" />
</div>
</template>
<script>
import emitter from '@/mixins/emitter';
export default {
inheritAttrs: false, // 避免顶层容器继承属性
mixins: [emitter],
props: {
value: {
type: String,
default: ''
}
},
data() {
return {};
},
methods: {
onInput(e) {
// 通知父组件数值发生变化
this.$emit('input', e.target.value);
// 通知 FormItem 执行校验
// 这种写法不健壮,因为 Input 组件和 KFormItem 组件之间可能会隔代
// this.$parent.$emit('validate');
/*
这里什么时候用 this.$emit,什么时候用 this.$parent.$emit???
【注意:事件的派发者是谁,事件的监听者就是谁】
==> 使用 $emit('input') 派发 input 事件,因为是 input 组件自己去监听,只不过是包含在 v-model 中了;
==> 使用 $parent.$emit('validate') 派发检验事件,也是父组件自己去监听
*/
// ! 使用 mixins 混入方法
this.dispatch('validate');
}
}
};
</script>
<style scoped></style>
checkbox.vue
<template>
<section>
<input type="checkbox" :checked="checked" @change="onChange" />
</section>
</template>
<script>
import emitter from '@/mixins/emitter';
export default {
mixins: [emitter],
props: {
checked: {
type: Boolean,
default: false
}
},
model: {
prop: 'checked',
event: 'change'
},
methods: {
onChange(e) {
this.$emit('change', e.target.checked);
// this.$parent.$emit("validate");
this.dispatch('validate');
}
}
};
</script>
<style scoped lang="less"></style>
FormItem.vue- 给
Input预留插槽 -slot - 能够展示
label和校验信息 - 能够进行校验
npm i async-validator -S
- 给
<template>
<div class="formItem-wrapper">
<!--
1、给 Input 组件预留插槽
2、显示 label 标签
3、监听校验事件,并执行校验(使用 async-validator 插件进行校验)
4、需要思考的问题:如何得到校验的数据和规则?
校验的数据和规则通过祖代的 provide 注入,在此组件内通过 inject 获取
5、如何知道校验的是哪条信息?
会在该组件上改在一个 prop 属性,属性值为当前 model 的数据
6。显示校验结果
-->
<div class="content">
<label v-if="label" :style="{ width: labelWidth }">{{ label }}:</label>
<slot></slot>
</div>
<p v-if="errorMessage" class="errorStyle">{{ errorMessage }}</p>
</div>
</template>
<script>
import Schema from 'async-validator';
export default {
inject: ['formModel'],
props: {
label: {
type: String,
default: ''
},
prop: String
},
data() {
return {
errorMessage: '',
labelWidth: this.formModel.labelWidth
};
},
mounted() {
// 监听校验事件,并执行校验
this.$on('validate', () => {
this.validate();
});
},
methods: {
validate() {
// 执行组件的校验
// 1、获取数据
const value = this.formModel.model[this.prop];
// 2、获取校验规则
const rules = this.formModel.rules[this.prop];
// 3、执行校验
const desc = {
[this.prop]: rules
};
const schema = new Schema(desc);
// 参数1是值,餐数2是校验错误对象数组
// validate 校验可能是异步的,方法返回值是 Promise<Boolean>
return schema.validate({ [this.prop]: value }, errors => {
if (errors) {
this.errorMessage = errors[0].message;
} else {
this.errorMessage = '';
}
});
}
}
};
</script>
<style scoped lang="less">
@labelWidth: 90px;
.formItem-wrapper {
padding-bottom: 10px;
}
.content {
display: flex;
}
.errorStyle {
font-size: 12px;
color: red;
margin: 0;
padding-left: @labelWidth;
}
</style>
Form.vue- 给
FormItem留插槽 - 设置数据和校验规则
- 全局校验
- 给
<template>
<div>
<!--
1、给 FormItem 组件预留插槽
2、传递 Form 实例给后代,比如 FormItem 用来获取校验的数据和规则
-->
<slot></slot>
</div>
</template>
<script>
export default {
provide() {
return {
formModel: this // 传递 Form 实例给后代,比如 FormItem 用来获取校验的数据和规则
};
},
props: {
model: {
type: Object,
required: true
},
rules: {
type: Object
},
labelWidth: String
},
data() {
return {};
},
methods: {
validate(cb) {
// 执行全局校验
// map 结果是若干 Promise 数组
const tasks = this.$children.filter(item => item.prop).map(item => item.validate());
// 所有任务必须全部校验成功才算校验通过
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false));
// Promise.all(tasks)
// .then(cb(true))
// .catch(cb(false));
// Promise.all(tasks)
// .then(() => {
// cb(true);
// })
// .catch(() => {
// cb(false);
// });
}
}
};
</script>
<style scoped></style>
index.vue
<template>
<div>
<Form :model="formModel" :rules="rules" ref="loginForm" label-width="90px">
<FormItem label="用户名" prop="username">
<Input v-model="formModel.username"></Input>
</FormItem>
<FormItem label="密码" prop="password">
<Input type="password" v-model="formModel.password"></Input>
</FormItem>
<FormItem label="记住密码" prop="remember">
<CheckBox v-model="formModel.remember"></CheckBox>
</FormItem>
<FormItem>
<button @click="onLogin">登录</button>
</FormItem>
</Form>
</div>
</template>
<script>
import Input from '@/components/form/Input';
import CheckBox from '@/components/form/CheckBox';
import FormItem from '@/components/form/FormItem';
import Form from '@/components/form/Form';
import Notice from '@/components/Notice';
import create from '@/utils/create';
export default {
data() {
const validateName = (rule, value, callback) => {
if (!value) {
callback(new Error('用户名不能为空'));
} else if (value !== 'admin') {
callback(new Error('用户名错误 - admin'));
} else {
callback();
}
};
const validatePass = (rule, value, callback) => {
if (!value) {
callback(false);
} else {
callback();
}
};
return {
formModel: {
username: '',
password: '',
remember: false
},
rules: {
username: [{ required: true, validator: validateName }],
password: [{ required: true, message: '密码必填' }],
remember: [{ required: true, message: '记住密码必选', validator: validatePass }]
}
};
},
methods: {
onLogin() {
this.$refs.loginForm.validate(isValid => {
// if (isValid) {
// alert("登录成功");
// } else {
// alert("登录失败");
// }
// 创建弹窗实例
create(Notice, {
title: '提示信息',
message: isValid ? '登录成功' : '登录失败',
duration: 1000
}).show();
});
}
},
components: {
Input,
CheckBox,
FormItem,
Form
}
};
</script>
<style scoped></style>
思考问题:
1、dispatch 和 broadcast 的实现
// 分发 - 自下而上
Vue.prototype.$dispatch = function(eventName, data) {
let parent = this.$parent;
// 查找父元素
while (parent) {
// 父元素用 $emit 触发
parent.$emit(eventName, data);
// 递归查找父元素
parent = parent.$parent;
}
};
Vue.prototype.$broadcast = function(eventName, data) {
broadcast.call(this, eventName, data);
};
// 广播 - 自上而下
function broadcast(eventName, data) {
this.$children.forEach(child => {
// 子元素触发 $emit
child.$emit(eventName, data);
if (child.$children.length) {
// 递归调用,通过 call 修改 this 指向 child
broadcast.call(child, eventName, data);
}
});
}
// 用法
<button @click="$broadcast('broadcast', '我是Child1')">广播子元素</button>
<button @click="$dispatch('dispatch', '哈喽 我是GrandGrandChild1')">dispatch</button>
mounted() {
this.$on("dispatch", msg => {
this.msg = "接收dispatch消息:" + msg;
});
this.$on("broadcast", msg => {
this.msg = "接收broadcast消息:" + msg;
});
}
2、.sync 和 v-model 的异同
v-model是语法糖:
<!-- v-model 是语法糖 -->
<Input v-model="username">
<!-- 默认等效于下面这行 -->
<Input :value="username" @input="username=$event">
但是你也可以通过在 data 中设置 model 选项修改默认行为,例如:Checkbox.vue
{
model: {
prop: 'checked',
event: 'change'
}
}
<!-- 上面这样设置会导致上级使用 v-model 时行为变化,相当于 -->
<KCheckBox :checked="model.remember" @change="model.remember = $event"></KCheckBox>
场景:v-model 通常用于表单控件,它有默认行为,同时属性名和事件名均可在子组件定义;
sync修饰符
<!-- sync修饰符添加于v2.4,类似于 v-model,它能用于修改传递到子组件的属性,如果像下面这样写 -->
<Input :value.sync="model.username">
<!-- 等效于下面这行,那么和 v-model 的区别只有事件名称的变化 -->
<Input :value="username" @update:value="username=$event">
<!-- 这里绑定属性名称更改,相应的属性名也会变化 -->
<Input :foo="username" @update:foo="username=$event">
场景:1、非表单控件;2、父组件传递的属性子组件想修改;
所以 sync 修饰符的控制能力都在 父级,事件名称也相对固定 update:xxx
习惯上表单元素用 v-model
- 父组件
<template>
<div>
<h3>username: {{ modelForm.username }}</h3>
<SyncInput :username.sync="modelForm.username"></SyncInput>
<!-- 等效于下面这行 -->
<!-- <SyncInput :username="modelForm.username" @update:username="val => (modelForm.username = val)"></SyncInput> -->
<!-- <SyncInput :username="modelForm.username" @update:username="updateFn"></SyncInput> -->
</div>
</template>
<script>
import SyncInput from '@/components/5-Sync/syncInput.vue';
export default {
name: '',
data() {
return {
modelForm: {
username: 'Tom'
}
};
},
methods: {
updateFn(val) {
// 接收 $emit 传递的参数
this.modelForm.username = val;
}
},
components: {
SyncInput
}
};
</script>
<style scoped></style>
- 子组件
<template>
<div>
<span>我是 sync 的子组件</span>
<van-button plain size="small" type="primary" @click="handleUpdate">更改值</van-button>
</div>
</template>
<script>
export default {
name: 'syncInput',
data() {
return {
num: 0
};
},
methods: {
handleUpdate() {
this.$emit('update:username', `我是更改之后的值 ${this.num++}`);
}
}
};
</script>
<style scoped lang="less"></style>
Vue 插件 plugin
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制,⼀般有下面几种:
- 添加全局方法或者
property;- 如:
vue-custom-element
- 如:
- 添加全局资源:指令/过滤器/过渡等;
- 如
vue-touch
- 如
- 通过全局混入来添加⼀些组件选项;
- 如
vue-router
- 如
- 添加
Vue实例方法,通过把它们添加到Vue.prototype上实现; - ⼀个库,提供自己的
API,同时提供上面提到的⼀个或多个功能;- 如
vue-router
- 如
对于每个我们需要使用的插件来说,我们需要在具体的业务逻辑前,也就是实例化 Vue 前,使用 Vue.use 来使用插件。
Vue.use(MyPlugin);
new Vue({
// ...组件选项
});
// 事实上等同于
Myplugin.install(Vue);
对于我们开发插件对象来说,我们需要给这个对象下暴露⼀个 install 方法。也就是说只要一个【对象】有 install 方法,同时它的第⼀个参数为 Vue 构造函数,第二个参数为一个 options,那么它就是一个合法的 Vue 插件。
// myPlugin 只需要是一个对象即可
let MyPlugin = {};
let MyPlugin = function () {};
MyPlugin.install = function (Vue, options) {};
我们可以使用插件做很多自动化的事情,某些情况下我们可以比【组件化】能够做更多细粒度的封装。
// 1. 添加全局静态方法 myGlobalMethod
Vue.myGlobalMethod = function () {};
// 2. 为组件增加 created 生命周期
Vue.mixin({ created: function () {} });
// 3. 添加实例方法 this.$myMethod
Vue.prototype.$myMethod = function (methodOptions) {};
我们可以使用插件的形式,更加方便的封装组件和通用的业务逻辑,甚至实现我们自己的⼀些生命周期。
1、使用插件注册一个 echarts 组件
- 安装
echarts:npm install echarts --save
- 新建
./src/plugin.js
import * as echarts from 'echarts';
//! 通过插件方式注入组件
export const useEchartsComponent = {
install: function (Vue) {
let id = 0;
Vue.component('echart', {
data() {
return {
id: `__echart-el=${id++}`
};
},
props: {
options: {
type: Object
}
},
mounted() {
const $el = document.getElementById(this.id);
const myChart = echarts.init($el);
myChart.setOption(this.options);
},
render(createElement) {
return createElement('div', {
attrs: {
id: this.id
},
style: {
width: '500px',
height: '400px'
}
});
}
});
}
};
- 在
./main.js中测试
import { useEchartsComponent } from './plugin';
Vue.use(useEchartsComponent);
- 在
./App.vue中使用
<template>
<div id="app">
<echart :options="options"></echart>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
options: {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
}
};
}
};
</script>
2、范例:移动 $bus 到插件
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));
}
}
}
Bus.install = function(Vue) {
Vue.prototype.$bus = new Vue();
};
export default Bus;
// 使用
// main.js
import Bus from "./plugins/bus";
Vue.use(Bus);
// this.$bus...
Vue 混合 mixin
混入(mixin)提供了⼀种非常灵活的方式,来分发 Vue 组件中的可复用功能。⼀个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被 “混合” 进入该组件本身的选项。
我们的 mixin 可以在 组件级别 和 全局 两种方式进行混入。
1、组件混入
// 定义⼀个混入对象
var myMixin = {
created: function () {
this.hello();
},
methods: {
hello: function () {
console.log('hello from mixin!');
}
}
};
// 定义⼀个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
});
var component = new Component(); // => "hello from mixin!"
2、全局对象
Vue.mixin({
created: function () {
var myOption = this.$options.myOption;
if (myOption) {
console.log(myOption);
}
}
});
对于 merge 的策略,主要分为以下几类:
- 关于
data:data部分会在内部进行递归的处理,也就是deepMerge,遇到同名key以 组件内部 的为准; - 关于生命周期:生命周期函数会进行⼀个数组处理,所有
mixin进来的生命周期都会执行,并且mixin传入的生命周期会在组件内部生命周期之前执行。
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies 添加⼀个函数:
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
};
3、在插件中定义混入
src\plugin.js
//! 通过插件方式注入 mixin,混入生命周期
export const notify = {
install(Vue) {
//! 全局注册混入
Vue.mixin({
data() {
return {
text: 'mixin.text',
next: {
info: 'mixin.next.info',
text: 'mixin.next.text'
}
};
},
async created() {
console.log('mixin 的 created');
let promise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise执行了~');
}, 2000);
});
};
let result = await promise();
console.log('result >>>>>> ', result);
}
});
}
};
//! 通过插件方式注入 mixin,混入方法
export const noReachBottomNotify = {
install(Vue) {
Vue.mixin({
mounted() {
const THRESHOLD = 50;
if (typeof this.onReachBottom === 'function') {
window.addEventListener('scroll', () => {
// window.screen.height:屏幕高度
// window.scrollY:滚动高度
// document.documentElement.offsetHeight:文档的高度
const sub = document.documentElement.offsetHeight - (window.scrollY + window.screen.height);
if (sub < THRESHOLD) {
this.onReachBottom();
}
});
}
}
});
}
};
- 在
main.js中注册:
import { notify, noReachBottomNotify } from './plugin';
Vue.use(notify);
Vue.use(noReachBottomNotify);
- 在组件中使用:
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{ next.text }}</h1>
<h1>{{ next.info }}</h1>
<p v-for="(item, index) in list" :key="item.title">{{ index }}: {{ item.title }}</p>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
next: {
text: 'this is next.text'
},
list: [
{ title: 'title 1' },
{ title: 'title 2' },
{ title: 'title 3' },
{ title: 'title 4' },
{ title: 'title 5' }
]
};
},
created() {
console.log('组件中的 created');
},
methods: {
onReachBottom() {
console.log('触发了 onReachBottom');
this.list.push(
...[
{ title: Math.random() },
{ title: Math.random() },
{ title: Math.random() },
{ title: Math.random() },
{ title: Math.random() }
]
);
}
}
};
</script>
- 页面展示:
4、范例:移动 dispatch 和 broadcast 到 mixins
// mixins/emitter.js
export default {
methods: {
dispatch(eventName, data) {
let parent = this.$parent;
// 查找父元素
while (parent) {
// 父元素用$emit触发
parent.$emit(eventName, data);
// 递归查找父元素
parent = parent.$parent;
}
},
broadcast(eventName, data) {
broadcast.call(this, eventName, data);
}
}
};
function broadcast(eventName, data) {
this.$children.forEach(child => {
// 子元素触发$emit
child.$emit(eventName, data);
if (child.$children.length) {
// 递归调用,通过call修改this指向 child
broadcast.call(child, eventName, data);
}
});
}
- 在
form/Kinput.vue中使用
<template>
<section class="kInput">
<input :value="value" @input="onInput" v-bind="$attrs" />
</section>
</template>
<script>
import emitter from '@/mixins/emitter';
export default {
inheritAttrs: false, // 避免顶层容器继承属性
mixins: [emitter],
props: {
value: {
type: String,
default: ''
}
},
methods: {
onInput(e) {
// 通知父组件数值变化
this.$emit('input', e.target.value);
// 通知 FormItem 校验
// this.$parent.$emit("validate");
this.dispatch('validate'); // 使用 mixin 中 emitter 的 dispatch,解决跨级问题
}
}
};
</script>
<style scoped lang="less">
.kInput {
display: inline-block;
}
</style>
render 函数详解
1、使用
- 一些场景中需要
JavaScript的完全编程的能力,这时可以用渲染函数,它比模板更接近编译器。
render(h) {
return h(tag, {...}, [children])
}
createElement函数
{
// 与 v-bind:class 的 API 相同,
// 接受⼀个字符串、对象或字符串和对象组成的数组
class: {
foo: true,
bar: false
},
// 与 v-bind:style 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML 特性
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM 属性
domProps: {
innerHTML: 'baz'
},
// 事件监听器器在 on 属性内,
// 但不再支持如 v-on:keyup.enter 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
}
2、自定义全局组件
./main.js页面
/*
<div id="box" class="foo"><span>aaa</span></div>
*/
Vue.component("comp", {
// 报错 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.
// 不报错的情况:当前在浏览器里面,通过 script 的方式引入一个带编译器的版本,才能使用字符串模板
// 当前运行环境是 webpack 的打包环境,打包环境默认是不带编译器的,因为要生成最小的输出的包,所以不能带编译器
// template: '<div id="box" class="foo"><span>aaa</span></div>',
render(h) {
return h("div", { class: { foo: true }, attrs: { id: "box" } }, [
h("span", "aaa")
]);
}
// jsx 写法
// render() {
// return (
// <div id="box" class="foo">
// <span>aaa</span>
// </div>
// );
// }
});
App.vue页面
<!-- 使用 App.vue -->
<comp></comp>
$el
- 类型:
string | Element - 限制:只在用
new创建实例时生效。 - 详细:
- 提供一个在页面上已存在的
DOM元素作为Vue实例的挂载目标。可以是CSS选择器,也可以是一个HTMLElement实例; - 在实例挂载之后,元素可以用
vm.$el访问; - 如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用
vm.$mount()手动开启编译;
- 提供一个在页面上已存在的
提供的元素只能作为挂载点。不同于
Vue 1.x,所有的挂载元素会被Vue生成的DOM替换。因此 不推荐 挂载root实例到<html>或者<body>上。
vm.$mount()
- 参数:
{Element | string} [elementOrSelector]{boolean} [hydrating]
- 返回值:
vm- 实例自身 - 用法:
- 如果
Vue实例在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。可以使用vm.$mount()手动地挂载一个未挂载的实例; - 如果没有提供
elementOrSelector参数,模板将被渲染为文档之外的元素,并且你必须使用原生DOM API把它插入文档中; - 这个方法返回实例自身,因而可以链式调用其它实例方法;
- 如果
- 示例:
// Vue.extend: 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象
var MyComponent = Vue.extend({
template: '<div>Hello!</div>'
})
// 创建实例并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')
// 同上
new MyComponent({ el: '#app' })
// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
Vue.extends(options)
-
参数:
{Object} options
-
用法:
使用基础
Vue构造器,创建一个 “子类”。参数是一个包含组件选项的对象。data选项是特例,需要注意 - 在Vue.extend()中它 必须是函数。<div id="mount-point"></div>// 创建构造器 var Profile = Vue.extend({ template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>', data: function () { return { firstName: 'Walter', lastName: 'White', alias: 'Heisenberg' } } }) // 创建 Profile 实例,并挂载到一个元素上。 new Profile().$mount('#mount-point')结果如下:
<p>Walter White aka Heisenberg</p>
实现弹窗组件
弹窗这类组件的特点是它们在当前 Vue 实例之外独立存在,通常挂载于 body;它们是通过 JS 动态创建的,不需要在任何组件中声明。
1、常见使用姿势
this.$create(Notice, {
title: '社会你杨哥喊你来搬砖',
message: '提示信息',
duration: 1000
}).show();
2、create.js
create函数用于动态创建指定组件实例并挂载至body:- 使用
new Vue创建 Vue 的实例,并使用$mount()挂载函数; - 获取真正的
DOM节点:vm.$el;
- 使用
import Vue from 'vue';
/**
* @description: 创建指定组件实例并挂载于 body 上
* @param {*} Component 组件名称
* @param {*} props 属性
* @return {*} 组件实例
*/
export default function create(Component, props) {
// ! vue 文件中 export default { ... } 导出的是一个对象,就是组件的相关配置,不是一个组件的构造函数,所以无法通过 new Component 去创建实例
// ! 可以使用 Vue.extend(options),返回值是一个构造函数,然后再 new XXX
// 1. 创建 vue 实例
const vm = new Vue({
render(h) {
// render 方法提供给我们一个 h 函数,它可以渲染 VNode(就是虚拟 DOM)
return h(Component, { props });
}
}).$mount(); // 更新操作,执行挂载函数,但未指定挂载目标,表示只执行初始化工作
/*
$mount('#app'):通常情况下会指定一个更新目标,例如 '#app'
$mount():没有指定目标,只把更新函数准备好,后面通过手动的方式挂载上去 - document.body.appendChild(vm.$el);
*/
// 2. 上面 vm 帮我们创建组件实例
// 3. 通过 $children 获取该组件实例,$children 返回的是子组件的数组
const comp = vm.$children[0];
// 4. 追加至 body
// document.body.appendChild(comp); // 不可以这么写
// comp 只是组件实例,不能真实的 DOM 节点,可以通过 vm.$el 获取真实的 DOM 节点
document.body.appendChild(vm.$el);
// 5. 给组件实例添加销毁方法,防止内存泄漏
comp.remove = () => {
document.body.removeChild(vm.$el);
vm.$destroy();
};
// 6. 返回组件实例
return comp;
}
3、创建通知组件 - Notice.vue
Notice.vue
<template>
<div class="box" v-if="isShow">
<h3 class="title">{{ title }}</h3>
<p class="box-content">{{ message }}</p>
</div>
</template>
<script>
export default {
name: 'notice',
props: {
title: {
type: String,
default: ''
},
message: {
type: String,
default: ''
},
duration: {
type: Number,
default: 0
}
},
data() {
return {
isShow: false
};
},
methods: {
show() {
this.isShow = true;
setTimeout(() => {
this.hide();
}, this.duration);
},
hide() {
this.isShow = false;
this.remove();
}
}
};
</script>
<style scoped lang="less">
.box {
position: fixed;
top: 50px;
left: 0;
right: 0;
width: 300px;
padding: 15px 20px;
border: 1px solid #dddddd;
background-color: #dddddd;
border-radius: 5px;
text-align: center;
margin: 0 auto;
.title {
font-size: 20px;
padding-bottom: 20px;
font-weight: 600;
}
.box-content {
width: 100%;
box-sizing: border-box;
font-size: 14px;
text-align: center;
}
}
</style>
4、使用 create api
<script>
import Notice from "../Notice"
import create from "@/utils/create"
export default {
methods: {
onLogin() {
this.$refs.loginForm.validate(isValid => {
// 创建弹窗实例
create(Notice, {
title: "这是一个标题",
message: isValid ? "登录!!!" : "有错!!!",
duration: 30000
}).show()
});
},
}
}
</script>
递归组件
递归组件是可以在它们自己模板中调用自身的组件;
- 必须有结束条件;
name对递归组件是必要的;
// Node.vue
<template>
<div>
<h3>{{ data.title }}</h3>
<!-- 必须有结束条件 -->
<Node v-for="d in data.children" :key="d.id" :data="d"></Node>
</div>
</template>
<script>
export default {
name: 'Node', // name 对递归组件是必要的
props: {
data: {
type: Object,
require: true
},
}
}
</script>
// 使用
<Node :data="{id:'1',title:'递归组件',children:[{...}]}"></Node>
实现 Tree 组件
Tree 组件是典型的递归组件,其他的诸如菜单组件都属于这一类,也是相当常见的。
1、组件设计
Tree 组件最适合的结构是无序列表 ul,创建⼀个递归组件 Item 表示 Tree 选项,如果当前 Item 存在 children,则递归渲染子树,以此类推;同时添加一个标识管理当前层级 item 的展开状态。
2、实现 Item 组件
<template>
<li>
<div @click="handleToggle" :class="{ handle: isFolder }">
<!-- 标题 -->
{{ model.title }}
<!-- 有子元素就显示 +/- -->
<span v-if="isFolder">{{ open ? '-' : '+' }}</span>
</div>
<ul v-show="open" v-if="isFolder" class="list">
<TreeItem v-for="model in model.children" :model="model" :key="model.title"></TreeItem>
</ul>
</li>
</template>
<script>
export default {
name: 'TreeItem',
props: {
model: {
type: Object
}
},
data() {
return {
open: false
};
},
methods: {
handleToggle() {
if (this.isFolder) {
this.open = !this.open;
}
}
},
computed: {
isFolder() {
return this.model.children && this.model.children.length;
}
}
};
</script>
<style scoped lang="less">
.handle {
cursor: pointer;
}
.list {
padding-left: 50px;
}
.item {
line-height: 25px;
}
</style>
3、使用
./index.vue
<template>
<div>
<ul>
<Item class="item" :model="treeData"></Item>
</ul>
</div>
</template>
<script>
import Item from '@/components/6-Tree/Item';
export default {
name: 'Tree',
data() {
return {
treeData: {
title: 'Web全栈架构师',
children: [
{
title: 'JavaScript'
},
{
title: 'JS高级',
children: [
{
title: 'ES6'
},
{
title: '动效'
}
]
},
{
title: 'Web全栈',
children: [
{
title: 'Vue训练营',
expand: true,
children: [
{
title: '基础知识'
},
{
title: '组件化'
},
{
title: '源码'
}
]
},
{
title: 'React',
children: [
{
title: 'JSX'
},
{
title: '虚拟DOM'
}
]
},
{
title: 'Node'
},
{
title: 'WebPack'
}
]
}
]
}
};
},
components: {
Item
}
};
</script>
<style scoped></style>