使用Vue过渡效果和插槽功能的指南
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文主要记录一下Vue中transition、slot、mixin、filter、plugin的用法
一.transition路由切换动画
实现路由向左切换,向右切换的动画。
1.定义多个跳转路由
定义路由的深度,用于判断路由的方向
{
path: "/",
name: 'home',
component: Home,
meta: {
depth: 1
}
},
{
path: "/list",
name: 'list',
component: List,
meta: {
depth: 2
}
},
{
path: "/detail/:id",
name: 'detail',
component: Detail,
meta: {
depth: 3
}
},
2.监听路由的切换
@Watch("$route")
onRouteChange(to: Route, from: Route) {
this.transitionName =
to!.meta!.depth > from!.meta!.depth ? "slide-left" : "slide-right";
}
3.transition样式
.slide-right-enter-active,
.slide-right-leave-active,
.slide-left-enter-active,
.slide-left-leave-active {
will-change: transform;
transition: transform 350ms;
position: absolute;
overflow: hidden;
}
.slide-right-enter,
.slide-left-leave-active {
transform: translate(-100%, 0);
}
.slide-left-enter,
.slide-right-leave-active {
transform: translate(100%, 0);
}
二、插槽 - Slot
1.用法作用:在组件模板中占位,以复用组件的布局
默认slot使用
⼦组件⽤标签来确定渲染的位置,标签⾥⾯可以放DOM结构,当⽗组件使⽤的时候没有往插 槽传⼊内容,标签内DOM结构就会显示在⻚⾯
具名slot使用
⼦组件⽤v-slot来表示插槽的名字,不传为默认插槽 ⽗组件中在使⽤时在默认插槽的基础上加上slot属性,值为⼦组件插槽v-slot的属性值
// 组件
<template>
<div class="slot-component">
<div class="header">
<slot name="header">我是默认头部文字</slot>
</div>
<div class="middle">
<div class="left">
<slot name="left">我是默认中间左边文字</slot>
</div>
<div class="right">
<slot name="right">我是默认中间右边文字</slot>
</div>
</div>
<div class="footer">
<slot name="footer">我是默认底部文字</slot>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component
export default class SlotComponent extends Vue {
}
</script>
// 页面使用
<template>
<slot-component>
<template v-slot:header>
导航栏
</template>
<template v-slot:left>
侧边栏
</template>
<template v-slot:right>
右侧内容
</template>
<template v-slot:footer>
底部内容
<router-link to="/home">回home</router-link>
</template>
</slot-component>
</template>
<script lang="ts">
import { Component, Vue} from "vue-property-decorator";
import SlotComponent from "../components/SlotComponent.vue";
@Component({
components: { SlotComponent },
})
传参
// 组件
<template>
<div class="slot-component">
<div class="header">
<slot name="header">我是默认头部文字</slot>
</div>
<div class="middle">
<div class="left">
<slot name="left">我是默认中间左边文字</slot>
</div>
<div class="right">
<slot name="right" :count="count">我是默认中间右边文字</slot>
</div>
</div>
<div class="footer">
<slot name="footer">我是默认底部文字</slot>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue} from "vue-property-decorator";
@Component
export default class SlotComponent extend Vue {
count: number = 10000;
}
</script >
// 页面使用
<template>
<slot-component>
<template v-slot:header>
导航栏
</template>
<template v-slot:left>
侧边栏
</template>
<template v-slot:right="slotProps">
右侧内容
<p>count = {{slotProps.count}}</p>
</template>
<template v-slot:footer>
底部内容
<router-link to="/home">回home</router-link>
</template>
</slot-component>
</template>
<script lang="ts">
import { Component, Vue} from "vue-property-decorator";
import SlotComponent from "../components/SlotComponent.vue";
@Component({
components: { SlotComponent },
})
</script >
效果如图:
2.slot的实现原理
slot本质上是返回VNode的函数,⼀般情况下,Vue中的组件要渲染到⻚⾯上需要经过 template render function VNode DOM 过程。
⽐如⼀个带slot的组件
Vue.component('button-counter', {
template: '<div> <slot>我是默认内容</slot></div>'
})
new Vue({
el: '#app',
template: '<button-counter><span>我是slot传⼊内容</span></button-counter>',
components:{buttonCounter}
})
经过vue编译, 组件渲染函数会变成这样
(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})
⽽这个_t就是slot渲染函数
function renderSlot (
name,
fallback,
props,
bindObject
) {
// 得到渲染插槽内容的函数
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
// 如果存在插槽渲染函数,则执⾏插槽渲染函数,⽣成nodes节点返回
// 否则使⽤默认值
nodes = scopedSlotFn(props) || fallback;
return nodes;
}
⽽scopedSlots其实就是递归解析各个节点, 获取slot
function resolveSlots (
children,
context
) {
if (!children || !children.length) {
return {}
}
var slots = {};
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var data = child.data;
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
// 如果slot存在(slot="header") 则拿对应的值作为key
var name = data.slot;
var slot = (slots[name] || (slots[name] = []));
// 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的
template标签并不会渲染成另⼀个标签到⻚⾯
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
// 如果没有就默认是default
(slots.default || (slots.default = [])).push(child);
}
}
// ignore slots that contains only whitespace
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}
三、Mixin 混入
本质其实就是⼀个js对象,可以包含我们组件中任意功能选项,如data、components、methods、created、computed等等
我们一般将公共的功能呢或数据以对象的形式放到mixin中,其他组件或页面组件使用mixin对象时,就会将mixin组件中的功能和数据混入到使用的组件中。
-
当组件存在与mixin对象相同的数据的时候,进⾏递归合并的时候组件的数据会覆盖mixin的数据
-
如果相同数据为⽣命周期钩⼦的时候,会合并成⼀个数组,先执⾏mixin的钩⼦,再执⾏组件的钩⼦
1.mixin使用
// mixin
import { Vue, Component } from 'vue-property-decorator';
@Component
export class CommonMixin extends Vue {
mixinName: string = 'haiyang';
enterTime: number = 0;
mounted() {
console.log('进入了mixin');
this.enterTime = Date.now();
}
beforeDestroy() {
console.log(`页面浏览了${Date.now() - this.enterTime}ms`);
}
}
// 引入
<template>
<div>页面内容</div>
</template>
<script lang="ts">
import { Component, Vue, Mixins } from "vue-property-decorator";
import { CommonMixin } from "../mixins";
export default class Home extends Mixins(CommonMixin) {
mounted() {
console.log('进入了list页面')
}
beforeDestroy() {
console.log('离开了list页面')
}
};
</script>
2.四种策略以及实现原理
- 优先递归处理 mixins,将子组件的mixins合并过来一同处理
- 先遍历合并parent 中的key,调⽤mergeField⽅法进⾏合并,然后保存在变量options
- 再遍历child,合并补上 parent 中没有的key,调⽤mergeField⽅法进⾏合并,保存在变量 options
- 通过mergeField 函数进⾏了合并
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
}
其实主要的逻辑就是合并mixin和当前组件的各种数据, 细分为四种策略:
替换性策略 - 同名的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
}
合并型策略- data, 通过set⽅法进⾏合并和重新赋值
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;
}
队列型策略- ⽣命周期函数和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
})
叠加性策略 - 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
}
四、filter 过滤器
1.使用
不改变原始值,返回加工过的值,是一个纯函数 vue3已经弃用,后面多用computed去实现过滤功能
// vue文件
<template v-slot:left>
手机号
<span> {{phone | phoneFilter}}</span>
</template>
import { Component, Vue} from "vue-property-decorator";
import { phoneFilter } from "../filters";
@Component({
components: { SlotComponent },
filters: { phoneFilter }
})
export default class Home extends Vue() {
readonly phone: string = '15829069561';
};
// filters
export const phoneFilter = (value: string) => {
if (!value) return "";
return value.replace(/(1[3-9]\d)\d{4}(\d{4})/, "$1****$2");
};
2.实现原理
- 在编译阶段通过parseFilters将过滤器编译成函数调⽤ 1.串联过滤器则是⼀个嵌套的函数调⽤, 2.前⼀个过滤器执⾏的结果是后⼀个过滤器函数的参数
function parseFilters (filter) {
let filters = filter.split('|');
let expression = filters.shift().trim() // shift()删除数组第⼀个元素并将其返回,该⽅法会更改原数组
let i;
if (filters) {
for(i = 0;i < filters.length;i++){
experssion = warpFilter(expression,filters[i].trim()) // 这⾥传进去的expression实际上是管道符号前⾯的字符串,即过滤器的第⼀个参数
}
}
return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
// ⾸先判断过滤器是否有其他参数
const i = filter.indexof('(')
if(i<0){ // 不含其他参数,直接进⾏过滤器表达式字符串的拼接
return `_f("${filter}")(${exp})`
}else{
const name = filter.slice(0,i) // 过滤器名称
const args = filter.slice(i+1) // 参数,但还多了 ‘)’
return `_f('${name}')(${exp},${args}` // 注意这⼀步少给了⼀个 ')'
}
}
- 编译后通过调⽤resolveFilter函数找到对应过滤器并返回结果
export function resolveFilter(id){
return resolveAsset(this.$options,'filters',id,true) || identity
}
// 因为我们找的是过滤器,所以在 resolveFilter函数中调⽤时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东⻄
export function resolveAsset(options,type,id,warnMissing){
if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
return
}
const assets = options[type] // 将我们注册的所有过滤器保存在变量中
// 接下来的逻辑便是判断id是否在assets中存在,即进⾏匹配
if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器
// 没有找到,代码继续执⾏
const camelizedId = camelize(id) // 万⼀你是驼峰的呢
if(hasOwn(assets,camelizedId)) return assets[camelizedId]
// 没找到,继续执⾏
const PascalCaseId = capitalize(camelizedId) // 万⼀你是⾸字⺟⼤写的驼峰呢
if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
// 如果还是没找到,则检查原型链(即访问属性)
const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
// 如果依然没找到,则在⾮⽣产环境的控制台打印警告
if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
}
// ⽆论是否找到,都返回查找结果
return result
}
- 执⾏结果作为参数传递给toString函数,⽽toString执⾏后,其结果会保存在Vnode的text属性 中,渲染到视图
function toString(value){
return value == null ? '' :
typeof value === 'object' ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可⽤来控制字符串⾥⾯的间距
: String(value)
}
五、plugin-插件就是指对Vue的功能的增强或补充。
1.使用
使用剪切板插件
- install想要使用的插件(剪切板插件clipboard2)
- 在入口文件引入并use
// main.ts
import VueClipboard from 'vue-clipboard2';
Vue.use(VueClipboard);
- 在文件中使用
<p @click="copyText('底部内容')" style="cursor: pointer;">
底部内容
</p>
2.实现一个自定义的plugin
- 什么是插件? 如何编写⼀个插件?
MyPlugin.install = function (Vue, options) {
// 1. 添加全局⽅法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注⼊组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例⽅法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
Vue.use(plugin, options);
- Vue.use做了什么?
判断当前插件是否已经安装过, 防⽌重复安装 处理参数, 调⽤插件的install⽅法, 第⼀个参数是Vue实例.
// Vue源码⽂件路径:src/core/global-api/use.js
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins
= []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
推荐阅读
-
玩转Vue:匿名、具名和作用域插槽的运用指南
-
使用Vue过渡效果和插槽功能的指南
-
使用Vue的keepAlive功能时,遇到mounted和created函数未被执行的问题
-
使用Vue和Electron进行进程间通信的指南
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
` 自动填充为 `cp test.txt`
- 文件和目录名补全:输入文件名首字母后按 Tab,如 `vi ed
` 显示可用的编辑器列表 - 查看命令帮助: - 使用 `man` 命令配合具体命令名获取详尽帮助,如 `man ls` 或者 `man grep --help`"> 在 Linux 中操作指令指南 - 基本构造与种类 - 指令组成: 1. **主指令 + 选项 + 参数**: 如 `ls -l /home`,`main-action option object` - 内置指令:系统预装的 shell 功能,如 `cd`, `pwd` - 外部指令:独立可执行文件,直接用文件名当作命令,如 `rm`, `mv` - **选项与参数**: - 选项:定制命令行为, `-l` 或 `--long-help` - 短选项:简写形式,例如 `-v` 和 `-V` 可能合并使用 - 长选项:详细描述的选项,如 `--version` 或 `--human-readable` - 参数:命令作用的目标,如 `ls` 对 `/home` 目录的操作 - **指令应用**: - 不同指令需要不同的参数 - 选项可带或不带参数,比如 `grep -i "keyword"` (忽略大小写搜索) - 参数间通常用空格分隔,如 `cp file1 file2 file3` - **中断与完成提示**: - 终止当前指令:按下 Ctrl+C - **自动完成**: - 输入部分命令关键词后,按 Tab 键补全命令,如 `cp ta
` 自动填充为 `cp test.txt` - 文件和目录名补全:输入文件名首字母后按 Tab,如 `vi ed ` 显示可用的编辑器列表 - 查看命令帮助: - 使用 `man` 命令配合具体命令名获取详尽帮助,如 `man ls` 或者 `man grep --help` -
什么是 Vue 的过渡效果?如何使用 Vue 的过渡效果?
-
详细讲解 Vue 插槽的使用方法和案例演示,阅读后完全理解
-
在 Vue 中使用 Slot 插槽是组件中的实用必备知识,它还能实现父组件和子组件之间的通信。
-
Vue 插槽(槽位)的详细信息和使用方法