目录
3.3.6.1 向子元素或子组件传递 attribute 和事件
1.组件之处理边界情况
1.1 子组件访问根组件数据
在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问
代码实例:
在根组件main.js中定义data数据,methods及computed属性
页面子组件中调用根组件数据实现
<template>
<div>
<h3>子组件访问根组件数据</h3>
<left></left>
</div>
</template>
<script>
//JavaScript 对象来定义局部组件
//left子组件
var LeftCom={
data:function(){
return {
title:this.$root.appName //默认根组件数据appName
}
},
methods:{
//调用根组件方法
getRootMethods(){
this.$root.changeappName();
this.title=this.$root.appName;
},
//调用根组件计算属性
getRootCompu(){
let comvalue=this.$root.newTitle;
this.title=comvalue;
},
//写入根组件数据
setRootData(){
this.$root.appName='你好,我的Vue2学习之旅';
this.title=this.$root.appName;
}
},
template:`<div>
<button @click='getRootMethods()'>调用根组件method</button>
<button @click='getRootCompu()'>调用根组件computed</button>
<button @click='setRootData()'>写入根组件data</button>
深入组件:{{title}}
</div>`
};
export default {
//数据
data(){
return {
countNum:1
}
},
//注册局部组件
components:{
'Left':LeftCom
},
//方法
methods:{
},
//计算属性
computed:{
}
}
</script>
<style scoped>
</style>
注意:对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。
1.2 子组件访问父组件数据
使用 $parent property 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。
var map = this.$parent.map || this.$parent.$parent.map
在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。
此时我们建议使用依赖注入的方式
1.3 父组件访问子组件
尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref
这个 attribute 为子组件赋予一个 ID 引用
<template>
<div>
<h3>父组件访问子组件</h3>
<button @click="changeSubData">通过父组件修改子组件data</button>
<button @click="getSubMethod">通过父组件调用子组件方法</button>
<left ref="leftRef"></left>
</div>
</template>
<script>
//JavaScript 对象来定义局部组件
//left子组件
var LeftCom={
data:function(){
return {
title:'父组件访问子组件'
}
},
methods:{
focusInput(){
this.$refs.input.focus();
}
},
template:`<div>
深入组件:{{title}}
<input ref='input'>
</div>`
};
export default {
//数据
data(){
return {
countNum:1
}
},
//注册局部组件
components:{
'Left':LeftCom
},
//方法
methods:{
//通过父组件修改子组件data
changeSubData(){
this.$refs.leftRef.title="父组件访问子组件,设置子组件data";
},
getSubMethod(){
this.$refs.leftRef.focusInput();
}
},
//计算属性
computed:{
}
}
</script>
<style scoped>
</style>
$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
1.4 依赖注入
当我们从子组件访问父组件,其中一层的父组件发生了改变的时候,使用 $parent property 无法很好的扩展到更深层级的嵌套组件上,这个时候就要用到依赖注入,它用到了两个新的实例选项:provide 和 inject
provide 选项允许我们指定我们想要提供给后代组件的数据/方法,该选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。
inject 选项来接收指定的我们想要添加在这个实例上的 property,该选项应该是:
一个字符串数组,或
一个对象,对象的 key 是本地的绑定名,value 是:
在可用的注入内容中搜索用的 key (字符串或 Symbol),或
一个对象,该对象的:
from property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)
default property 是降级情况下使用的 value
provide 和 inject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中
实例代码:
<template>
<div>
<h3>组件的依赖注入</h3>
<left>
<left-sub></left-sub>
</left>
</div>
</template>
<script>
//JavaScript 对象来定义局部组件
//left子组件
var LeftCom={
provide: function() {
return {
changeT:this.title
}
},
data:function(){
return {
title:'组件依赖注入'
}
},
template:`<div>深入组件:{{title}}<slot></slot></div>`
};
var LeftSub={
inject: ["changeT"],
data:function(){
return {
parentTitle:''
}
},
created:function() {
this.parentTitle=this.changeT;
},
template:`<div>父组件标题:{{parentTitle}}</div>`
};
export default {
//数据
data(){
return {
}
},
//注册局部组件
components:{
'Left':LeftCom,
'LeftSub':LeftSub
},
//方法
methods:{
},
//计算属性
computed:{
}
}
</script>
<style scoped>
</style>
1.5 程序化的事件侦听器
在前面的学习中我们知道$emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法:
- 通过 $on(eventName, eventHandler) 侦听一个事件
- 通过 $once(eventName, eventHandler) 一次性侦听一个事件
- 通过 $off(eventName, eventHandler) 停止侦听一个事件
当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。
$on、$once、$off实例代码:
<template>
<div>
<h3>$on侦听事件</h3>
<button @click="addNum">$on侦听加法事件</button>
<button @click="reduceNum">$once侦听减法事件</button>
<button @click="offAdd">$off停止侦听加法事件</button>
<input v-model="numVal" />
<p>this.$on('add', eventHandler) 定义一个监听的(add)事件并指定他的执行对象/执行函数(eventHandler)</p>
<p>this.$emit('add', 'my params')消费add事件</p>
<p>使用on和emit后,我们可以把事件的定义和消费实现逻辑的解耦,可以在父组件监听($on)子组件中直接调用事件($emit)</p>
<p>要强调的一点是:$on和$emit事件必须是在一个公共的实例上才能触发!!!</p>
</div>
</template>
<script>
export default {
//数据
data(){
return {
numVal:1
}
},
//注册局部组件
components:{
},
//方法
methods:{
addNum(){
//消费add事件
this.$emit('add');
},
reduceNum(){
this.$emit('reduce');
},
offAdd(){
this.$off('add');
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
//定义一个监听的(add)事件并指定他的执行对象/执行函数
this.$on('add',function(){
console.log('触发方法add');
this.numVal++;
});
//$once监听事件只执行一次
this.$once('reduce',function(){
console.log('触发方法reduce');
this.numVal--;
});
}
}
</script>
<style scoped>
</style>
1.6 递归组件
组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:
name: 'unique-name-of-my-component'
当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。
Vue.component('unique-name-of-my-component', {
// ...
})
稍有不慎,递归组件就可能导致无限循环:
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。
递归组件基础实例代码:
<template>
<div>
<h3>递归组件基础使用</h3>
<left :myTitle="defaultTitle"></left>
</div>
</template>
<script>
//JavaScript 对象来定义局部组件
//left子组件
var LeftCom={
name:'left',
props:["myTitle"],
data:function(){
return {
title:'递归',
show:false
}
},
methods:{
componentsSelf(){
this.show=true;
}
},
template:`<div>
深入组件:{{myTitle}}
<button @click="componentsSelf">组件自调用</button>
<left v-if="show" :myTitle="title"></left>
</div>`
};
export default {
//数据
data(){
return {
titleArrary:["1","2","3"],
defaultTitle:"hello vue"
}
},
//注册局部组件
components:{
'Left':LeftCom
},
//方法
methods:{
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
1.7 内联模板
当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。
内联模板实例代码
<template>
<div>
<h3>内联模板</h3>
<left :myTitle="defaultTitle" inline-template>
<div><p>hello world</p></div>
</left>
</div>
</template>
<script>
//JavaScript 对象来定义局部组件
//left子组件
var LeftCom={
props:["myTitle"],
data:function(){
return {
title:'递归'
}
},
methods:{
},
template:`<div>深入组件:{{myTitle}}</div>`
};
export default {
//数据
data(){
return {
defaultTitle:"内联模板"
}
},
//注册局部组件
components:{
'Left':LeftCom
},
//方法
methods:{
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
内联模板会把子组件中的内容替换为内联模板的内容,比如上述实例的最终渲染结果为:
1.8 X-Template
X-Template是在 JavaScript 标签里使用 text/x-template 类型,并且指定一个id来定义模板的一种方式,例如:
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
template: '#hello-world-template'
})
x-template 需要定义在 Vue 所属的 DOM 元素外。
这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。
1.9 强制更新
在vue的开发过程中,数据的绑定通常来说都不用我们操心,例如在data中有一个msg的变量,只要修改它,那么在页面上,msg的内容就会自动发生变化。但是如果对于一个复杂的对象,例如一个对象数组,直接去给数组上某一个元素增加属性,或者直接把数组的length变成0,vue可能就无法知道发生了改变。这个其实就是考验对于双向绑定的更进一步的理解应用了。
在Vue中,双向绑定属于自动档,然而在特定的情况下,需要手动触发“刷新”操作,目前有四种方案可以选择:
- 刷新整个页面
- 使用v-if标记
- 使用内置的forceUpdate方法
- 使用key-changing优化组件
使用forceUpdate是比较好的一种方式,比如说我们尝试直接给某个object
增加一个属性,发现页面上没有效果;直接将length变成0来清空数组,也没有效果,
关键代码如下
change: function(index) {
// 增加性格属性
this.girls[idx].character = 'lovely';
},
clear: function() {
// 清空数组
this.girls.length = 0;
}
按照上面的写法没有产生想要的效果,是因为没有按照vue的规范去写,因为文档里面写了,对于深层的,最好用$set方法,这样vue就可以知道发生了变化,同时vue也不建议直接修改length,而是给一个空数组来置空。
如果我们按照vue的规范去写的,是可以实现变化的,
change: function(index) {
// 增加性格属性
this.$set(this.girls[idx],'character','lovely')
},
clear: function() {
// 清空数组
this.girls = [];
}
如果我们不想利用$set去设置,非要按照我们第一种方式去写,可以实现么?可以的,就是利用$forceUpdate,此时修改了数据,然而页面层没有变动,之后通过日志打印的方式确认数据是否有修改过,之后再确认有没有监听到,用$forceUpdate就相当于按照最新数据给渲染一下。
change: function(index) {
this.girls[idx].character = '男';
this.$forceUpdate();
},
clear: function() {
this.girls.length = 0;
this.$forceUpdate();
}
这种做法实际上并不推荐,官方说如果你现在的场景需要用forceUpdate方法 ,那么99%是你的操作有问题,如上data里不显示声明对象的属性,之后添加属性时正确的做法时用 $set()方法,所以forceUpdate请慎用。
该同等效果的:window.location.reload()
1.10 v-once
只渲染元素和组件一次,随后的渲染,使用了此指令的元素/组件及其所有的子节点,都会当作静态内容并跳过,这可以用于优化更新性能。
常见用法实例代码
<template>
<div>
<h3>v-once</h3>
<button @click="changeMsg">修改消息</button>
<input v-once v-model="msg"/>
<p>{{msg}}</p>
</div>
</template>
<script>
export default {
//数据
data(){
return {
msg:"hello world",
countNum:1
}
},
//注册局部组件
components:{
},
//方法
methods:{
changeMsg(){
this.msg="你好消息"+this.countNum;
this.countNum++;
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
当msg值改变时,input文本框中的内容不会变化,而p元素的值会随之变化
2.过渡效果与状态
2.1 过渡效果
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:
- 在 CSS 过渡和动画中自动应用 class
- 可以配合使用第三方 CSS 动画库,如 Animate.css
- 在过渡钩子函数中使用 JavaScript 直接操作 DOM
- 可以配合使用第三方 JavaScript 动画库,如 Velocity.js
2.1.1 单元素/组件的过渡
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
元素封装成过渡组件之后,在遇到插入或删除时,Vue 将
- 自动嗅探目标元素是否有 CSS 过渡或动画,并在合适时添加/删除 CSS 类名。
- 如果过渡组件设置了过渡的 JavaScript 钩子函数,会在相应的阶段调用钩子函数。
- 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作(插入/删除)在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,与 Vue,和Vue的 nextTick 概念不同
2.1.1.1 过渡的CSS类名
在进入/离开的过渡中,会有 6 个 class 切换。
- v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
- v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
- v-enter-to:2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
- v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
- v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
- v-leave-to:2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。)
对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。如果你使用了 <transition name="my-transition">,那么 v-enter 会替换为 my-transition-enter。
v-enter-active 和 v-leave-active 可以控制进入/离开过渡的不同的缓和曲线
2.1.1.2 CSS过渡
实例代码
<template>
<div>
<h3>CSS过渡</h3>
<button @click="show=!show">切换渲染</button>
<transition name="slide-fade">
<p v-if="show">Hello World</p>
</transition>
</div>
</template>
<script>
export default {
//数据
data(){
return {
show:true
}
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}
</style>
2.1.1.3 CSS动画
CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter 类名在节点插入 DOM 后不会立即删除,而是在 animationend 事件触发时删除
实例代码
<template>
<div>
<h3>CSS动画</h3>
<button @click="show=!show">切换渲染</button>
<transition name="bounce">
<p v-if="show">Hello World</p>
</transition>
</div>
</template>
<script>
export default {
//数据
data(){
return {
show:true
}
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
</style>
2.1.1.4 自定义过渡类名
我们可以通过以下属性来自定义过渡类名:
- enter-class
- enter-active-class
- enter-to-class (2.1.8+)
- leave-class
- leave-active-class
- leave-to-class (2.1.8+)
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。
实例代码:
<template>
<div>
<h3>自定义过渡的类名</h3>
<button @click="show=!show">切换渲染</button>
<transition enter-active-class="animate__animated animate__tada" leave-active-class="animate__animated animate__bounceOutRight">
<p v-if="show">Hello World</p>
</transition>
</div>
</template>
<script>
export default {
//数据
data(){
return {
show:true
}
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
animate的引用说明:
1、npm i animate.css--save 安装
2、main.js中全局引入
import animate from 'animate.css'
Vue.use(animate)
3、animate css样式引入,注意需要添加前缀animate__
<transition enter-active-class="animate__animated animate__tada" leave-active-class="animate__animated animate__bounceOutRight">
<p v-if="show">Hello World</p>
</transition>
2.1.1.5 同时使用过渡和动画
Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 transitionend 或 animationend,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。
但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type attribute 并设置 animation 或 transition 来明确声明你需要 Vue 监听的类型。
实例代码:
<template>
<div>
<h3>同时使用过渡和动画</h3>
<button @click="show=!show">切换渲染</button>
<transition name="fade" enter-active-class="animate__animated animate__tada fade-enter-active" leave-active-class="animate__animated animate__bounceOutRight fade-leave-active">
<p v-if="show">Hello World</p>
</transition>
</div>
</template>
<script>
export default {
//数据
data(){
return {
show:true
}
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
/*延迟加载*/
.fade-enter, .fade-leave-to{
opacity: 0;
}
.fade-enter-active, .fade-leave-active{
transition: opacity 5s;
}
</style>
2.1.1.6 显性的过渡时间
在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend 或 animationend 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。
在这种情况下你可以用 <transition> 组件上的 duration prop 定制一个显性的过渡持续时间 (以毫秒计):
<transition :duration="1000">...</transition>
你也可以定制进入和移出的持续时间:
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
2.1.1.7 JavaScript钩子
可以在属性中声明 JavaScript 钩子,包含
- before-enter
- enter
- after-enter
- before-leave
- leave
- after-leave
实例代码:
<template>
<div>
<h3>JavaScript钩子</h3>
<button @click="show=!show">切换渲染</button>
<transition
enter-active-class="animate__animated animate__tada "
leave-active-class="animate__animated animate__bounceOutRight"
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:beofre-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
>
<p v-if="show">Hello World</p>
</transition>
</div>
</template>
<script>
import Velocity from 'velocity-animate';
export default {
//数据
data(){
return {
show:true
}
},
//注册局部组件
components:{
},
//方法
methods:{
//进入前触发事件
beforeEnter:function(el){
console.log("进入前触发事件");
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
//进入时触发事件
enter:function(el,done){
console.log("进入时触发事件");
Velocity(el, { opacity: 1, fontSize: '5em' }, { duration: 300 });
Velocity(el, { fontSize: '1em' }, { complete: done });
},
//进入后触发事件
afterEnter:function(el){
console.log("进入后触发事件");
el.style.fontSize="30px";
},
//离开前触发事件
beforeLeave(el){
console.log("离开前触发事件");
el.style.opacity = 0;
el.style.transformOrigin = 'right';
},
//离开时触发事件
leave(el,done){
console.log("离开时触发事件");
Velocity(el, { opacity: 1, fontSize: '5em' }, { duration: 300 });
Velocity(el, { fontSize: '1em' }, { complete: done });
},
//来开后触发事件
afterLeave(el){
console.log("离开时触发事件");
el.style.fontSize="30px";
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
关于Velocity插件
Velocity是一个简单易用,高性能且功能丰富的轻量级JavaScript动画库,它拥有颜色动画,转换动画(transforms)、循环、缓动、SVG动画、和滚动动画等特色功能
支持Chaining链式动画,当一个元素连续应用多个velocity()时,动画以队列的方式执行
1、执行npm install velocity-animate命令,进行安装
2、页面引入 import Velocity from 'velocity-animate';
2.1.2 初始渲染的过渡
通过 appear 属性设置节点在初始渲染的过渡
appear与进入/离开过渡一样,可以自定义过渡CSS列名,可以设置JavaScript钩子
自定义过渡类名的属性:
- appear-class
- appear-active-class
- appear-to-class (2.1.8+)
JavaScript钩子:
- before-appear
- appear
- after-appear
- appear-cancelled
appear-cancelled的触发条件就是before-appear/appear两个钩子函数中有不显示该节点的操作v-if/ v-show都会触发appear-cancelled钩子函数。实际场景不多,一般在动画执行过程中被用户取消后,可以执行一些操作。
实例代码:
<template>
<div>
<h3>初始渲染的过渡</h3>
<button @click="show=!show">切换渲染</button>
<transition
appear
appear-active-class="animate__animated animate__tada "
appear-to-class="animate__animated animate__fadeIn"
v-on:before-appear="beforeAppear"
v-on:appear="appear"
v-on:after-appear="afterAppear"
v-on:appear-cancelled="appearCancelled"
>
<p v-if="show">Hello World</p>
</transition>
</div>
</template>
<script>
import Velocity from 'velocity-animate';
export default {
//数据
data(){
return {
show:true
}
},
//注册局部组件
components:{
},
//方法
methods:{
//初始渲染前触发事件
beforeAppear:function(el){
console.log("初始渲染前触发事件");
el.style.opacity = 0;
el.style.transformOrigin = 'left';
//this.show=false;//此代码可触发appear-cancelled事件
},
//初始渲染时触发事件
appear:function(el,done){
console.log("初始渲染时触发事件");
Velocity(el, { opacity: 1, fontSize: '5em' }, { duration: 300 });
Velocity(el, { fontSize: '1em' }, { complete: done });
},
//初始渲染后触发事件
afterAppear:function(el){
console.log("初始渲染后触发事件");
el.style.fontSize="30px";
},
//初始渲染取消触发事件
appearCancelled(el){
console.log("初始渲染取消触发事件");
el.style.opacity = 0;
el.style.transformOrigin = 'right';
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
2.1.3 多个元素的过渡
我们平时在多个不同元素上会使用v-if/v-show来控制元素是否显示,这也是控制多个元素过渡的一种方式,但是这样使用时,需要注意:
当有相同标签名的元素切换时,需要通过 key
attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在<transition>组件中的多个元素设置 key 是一个更好的实践。
实例代码:
<template>
<div>
<h3>多个元素的过渡</h3>
<button @click="changeBtn">变换按钮</button>
<transition>
<button v-bind:key="buttonState">{{buttonText}}</button>
</transition>
</div>
</template>
<script>
export default {
//数据
data(){
return {
buttonState:"save"
}
},
//注册局部组件
components:{
},
//方法
methods:{
changeBtn(){
if(this.buttonState=="save"){
this.buttonState="edit";
}
else if(this.buttonState=="edit"){
this.buttonState="delete";
}
else if(this.buttonState=="delete"){
this.buttonState="save";
}
}
},
//计算属性
computed:{
buttonText:function (){
let returnVal="保存";
if(this.buttonState=="save"){
returnVal="保存";
}
else if(this.buttonState=="edit"){
returnVal="编辑";
}
else if(this.buttonState=="delete"){
returnVal="删除";
}
return returnVal;
}
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
2.1.3.1 过渡模式
在上面的实例中,我们会发现,当我们改变buttonState值的时候,会发现两个按钮每次都会刷新,一个离开过渡的时候另一个开始进入过渡,这是 <transition>
的默认行为 - 进入和离开同时发生。
同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了过渡模式:
- in-out:新元素先进行过渡,完成之后当前元素过渡离开
- out-in:当前元素先进行过渡,完成之后新元素过渡进入
我们在transition中设置mode属性为out-in,就可已解决这个问题
<transition mode="out-in">
<!-- ... the buttons ... -->
</transition>
2.1.4 多个组件的过渡
多个组件的过渡我们不需要使用key属性,只需要使用动态组件
实例代码:
<template>
<div>
<h3>多个组件的过渡</h3>
<button @click="changeCom">变换组件</button>
<transition mode="out-in">
<component :is="comState"></component>
</transition>
</div>
</template>
<script>
//left子组件
var LeftCom={
data:function(){
return {
title:'多个组件的过渡'
}
},
methods:{
},
template:`<div>Left深入组件:{{title}}</div>`
};
//right组件
var RightCom={
data:function(){
return {
title:'多个组件的过渡'
}
},
methods:{
},
template:`<div>Right组件</div>`
};
export default {
//数据
data(){
return {
comState:"Left"
}
},
//注册局部组件
components:{
'Left':LeftCom,
'Right':RightCom
},
//方法
methods:{
changeCom(){
if(this.comState=="Left"){
this.comState="Right";
}
else if(this.comState=="Right"){
this.comState="Left";
}
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
2.1.5 列表过渡
列表过渡我们需要用到<transition-group>组件,其特点:
- 不同于 <transition>,它会以一个真实元素呈现:默认为一个 <span>。你也可以通过 tag 属性 更换为其他元素。
- 过渡模式不可用,因为我们不再相互切换特有的元素。
- 内部元素总是需要提供唯一的 key attribute 值。
- CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
2.1.5.1 列表的进入/离开过渡
实例代码:
<template>
<div>
<h3>列表的进入/离开过渡</h3>
<button @click="add">添加列表元素</button>
<button @click="remove">删除列表元素</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{item}}
</span>
</transition-group>
</div>
</template>
<script>
export default {
//数据
data(){
return {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
}
},
//注册局部组件
components:{
},
//方法
methods:{
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
//添加列表元素方法
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
//删除列表元素方法
remove: function () {
this.items.splice(this.randomIndex(), 1)
},
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
2.1.5.2 列表的排序过渡
在列表的进入/离开实例中,代码运行时,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,这时我们需要用到排序过渡
要实现排序过渡,我们这里需要使用lodash插件,
1、执行 npm i lodash --save 命令安装插件
2、页面中引入lodash, import Lodash from 'lodash';
实例代码:
<template>
<div>
<h3>列表的排序过渡</h3>
<button @click="add">添加列表元素</button>
<button @click="remove">删除列表元素</button>
<button @click="reorder">随机排序</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{item}}
</span>
</transition-group>
</div>
</template>
<script>
import Lodash from 'lodash';
export default {
//数据
data(){
return {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
}
},
//注册局部组件
components:{
},
//方法
methods:{
randomIndex: function () {
return Math.floor(Math.random() * this.items.length);
},
//添加列表元素方法
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++);
},
//删除列表元素方法
remove: function () {
this.items.splice(this.randomIndex(), 1);
},
//元素排序过渡
reorder: function () {
this.items = Lodash.shuffle(this.items);
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
排序过渡的实现,是Vue 使用了一个叫 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡新的位置
需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline
。作为替代方案,可以设置为 display: inline-block
或者放置于 flex 中
FLIP 动画不仅可以实现单列过渡,多维网格也同样可以过渡
2.1.5.3 列表的交错过渡
通过 data 属性 与 JavaScript 通信,就可以实现列表的交错过渡
实例代码:
<template>
<div>
<h3>列表的交错过渡</h3>
<input v-model="query">
<transition-group name="staggered-fade" tag="ul"
v-bind:css="false"
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:leave="leave"
>
<li v-for="(item, index) in computedList" v-bind:key="item.msg" v-bind:data-index="index">{{ item.msg }}</li>
</transition-group>
</div>
</template>
<script>
import Velocity from 'velocity-animate';
export default {
//数据
data(){
return {
query: '',
list: [
{ msg: 'Bruce Lee' },
{ msg: 'Jackie Chan' },
{ msg: 'Chuck Norris' },
{ msg: 'Jet Li' },
{ msg: 'Kung Fury' }
]
}
},
//注册局部组件
components:{
},
//方法
methods:{
//进入前触发事件
beforeEnter: function (el) {
el.style.opacity = 0
el.style.height = 0
},
//进入时触发事件
enter: function (el, done) {
var delay = el.dataset.index * 150
setTimeout(function () {
Velocity(
el,
{ opacity: 1, height: '1.6em' },
{ complete: done }
)
}, delay)
},
//离开时触发事件
leave: function (el, done) {
var delay = el.dataset.index * 150
setTimeout(function () {
Velocity(
el,
{ opacity: 0, height: 0 },
{ complete: done }
)
}, delay)
}
},
//计算属性
computed:{
computedList: function () {
var vm = this
return this.list.filter(function (item) {
return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
})
}
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
2.1.6 可复用的过渡
要创建一个可复用过渡组件,你需要做的就是将 <transition>
或者 <transition-group>
作为根组件,然后将任何子组件放置在其中就可以了。
简单组件代码示例:
Vue.component('my-special-transition', {
template: '\
<transition\
name="very-special-transition"\
mode="out-in"\
v-on:before-enter="beforeEnter"\
v-on:after-enter="afterEnter"\
>\
<slot></slot>\
</transition>\
',
methods: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
})
2.1.7 动态过渡
动态过渡最基本的例子是通过 name
属性 来绑定动态值。
<transition v-bind:name="transitionName">
<!-- ... -->
</transition>
当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用。
所有过渡属性都可以动态绑定,但我们不仅仅只有属性可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现。
实例代码:
<template>
<div>
<h3>动态过渡</h3>
Fade In: <input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration">
Fade Out: <input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration">
<transition v-bind:css="false"
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:leave="leave"
>
<p v-if="show">hello</p>
</transition>
<button v-if="stop" v-on:click="stop = false; show = false">开始动画</button>
<button v-else v-on:click="stop = true">结束动画</button>
</div>
</template>
<script>
import Velocity from 'velocity-animate';
export default {
//数据
data(){
return {
show: true,
fadeInDuration: 1000,
fadeOutDuration: 1000,
maxFadeDuration: 1500,
stop: true
}
},
//注册局部组件
components:{
},
//方法
methods:{
//进入前触发事件
beforeEnter: function (el) {
el.style.opacity = 0
},
//进入时触发事件
enter: function (el, done) {
var vm = this
Velocity(el,{ opacity: 1 },
{
duration: this.fadeInDuration,
complete: function () {
done();
if (!vm.stop) {
vm.show = false;
}
}
}
);
},
//离开时触发事件
leave: function (el, done) {
var vm = this;
Velocity(el,{ opacity: 0 },
{
duration: this.fadeOutDuration,
complete: function () {
done();
vm.show = true;
}
}
);
}
},
//计算属性
computed:{
},
//渲染dom
mounted:function(){
this.show = false;
}
}
</script>
<style scoped>
</style>
2.2 过渡状态
Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如:
- 数字和运算
- 颜色的显示
- SVG 节点的位置
- 元素的大小和其他的 property
这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。
2.2.1 状态动画与侦听器
通过侦听,我们能监听到任何数值属性的数值更新,首先我们先看一下通过监听数字文本框中数值变化,从而实现数字动态变化的实例:
首先执行npm i gsap@3.2.4 命令安装gsap插件(动画库与图片预览插件)
实例代码:
<template>
<div>
<h3>状态动画与侦听器</h3>
<input type="number" v-model.number="number" step="20"/>
<p>{{ animatedNumber }}</p>
</div>
</template>
<script>
import gsap from 'gsap';
export default {
//数据
data(){
return {
number: 0,
tweenedNumber: 0
}
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
animatedNumber:function(){
return this.tweenedNumber.toFixed(0);
}
},
//侦听
watch:{
number: function(newValue) {
gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
}
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
对于不能直接像数字一样存储的值,比如 CSS 中的 color 的值,使用tween和color-js插件实现
1)执行命令npm i tween 安装tween插件
2)执行命令 npm i color-js 安装color插件
实例代码:
<template>
<div>
<h3>状态动画与侦听器</h3>
<input v-model="colorQuery" v-on:keyup.enter="updateColor" placeholder="输入一个颜色">
<button v-on:click="updateColor">更新</button>
<p>Preview:</p>
<span v-bind:style="{ backgroundColor: tweenedCSSColor }" class="example-7-color-preview"></span>
<p>{{ tweenedCSSColor }}</p>
</div>
</template>
<script>
import TWEEN from 'tween';
import Color from 'color-js';
export default {
//数据
data(){
return {
colorQuery: '',
colorData: {
red: 0,
green: 0,
blue: 0,
alpha: 1
},
tweenedColor: {}
}
},
created: function () {
this.tweenedColor = Object.assign({}, this.colorData);
},
//注册局部组件
components:{
},
//方法
methods:{
//更新颜色
updateColor: function () {
this.colorData = new Color(this.colorQuery).toRGB();
this.colorQuery = '';
}
},
//计算属性
computed:{
tweenedCSSColor: function () {
return new Color({
red: this.tweenedColor.red,
green: this.tweenedColor.green,
blue: this.tweenedColor.blue,
alpha: this.tweenedColor.alpha
}).toCSS();
}
},
//侦听
watch:{
colorData: function () {
function animate () {
if (TWEEN.update()) {
requestAnimationFrame(animate);
}
}
new TWEEN.Tween(this.tweenedColor).to(this.colorData, 750).start();
animate();
}
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
.example-7-color-preview {
display: inline-block;
width: 50px;
height: 50px;
}
</style>
2.2.2 动态状态过渡
就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。这里我们使用TweenLite实现一个缓动作的动态效果:
1)执行npm i gsap 安装插件
注:TweenLite最初是GreenSock公司推出的一款基于ActionScript的免费开源的缓动引擎,是webgame开发人员比较常用的一个动画库,使用TweenLite能够简化动画制作的代码编写工作。TweenLite属于gsap插件中的一个模块,详情参见官网GreenSock
实例代码:
<template>
<div class="svgDiv">
<h3>动态状态过渡</h3>
<svg width="200" height="200">
<polygon :points="points"></polygon>
<circle cx="100" cy="100" r="90"></circle>
</svg>
<label>Sides: {{ sides }}</label>
<input type="range" min="3" max="500" v-model.number="sides" />
<label>Minimum Radius: {{ minRadius }}%</label>
<input type="range" min="0" max="90" v-model.number="minRadius" />
<label>Update Interval: {{ updateInterval }} milliseconds</label>
<input type="range" min="10" max="2000" v-model.number="updateInterval" />
</div>
</template>
<script>
import TweenLite from 'gsap';
/** 数值转换坐标点*/
function valueToPoint(value, index, total) {
var x = 0;
var y = -value * 0.9;
var angle = ((Math.PI * 2) / total) * index;
var cos = Math.cos(angle);
var sin = Math.sin(angle);
var tx = x * cos - y * sin + 100;
var ty = x * sin + y * cos + 100;
return { x: tx, y: ty };
}
/**生成坐标点 */
function generatePoints(stats) {
var total = stats.length;
return stats.map(function(stat, index)
{
var point = valueToPoint(stat, index, total);
return point.x + "," + point.y;
}).join(" ");
}
export default {
//数据
data(){
var defaultSides = 10;//图形默认边数
var stats = Array.apply(null, { length: defaultSides }).map(function() {return 100;});
return {
stats: stats,
points:generatePoints(stats),
sides: defaultSides,//图形的边数
minRadius: 50,//最小直径长度
interval: null,
updateInterval: 500//图形变动时长
}
},
created: function () {
},
//注册局部组件
components:{
},
//方法
methods:{
//
randomizeStats() {
var vm = this;
this.stats = this.stats.map(function() {return vm.newRandomValue();});
},
//根据最小直径随机生成图形大小
newRandomValue() {
return Math.ceil(this.minRadius + Math.random() * (100 - this.minRadius));
},
//重置图形变动时间间隔
resetInterval() {
var vm = this;
clearInterval(this.interval);//清除定时器
this.randomizeStats();
this.interval = setInterval(function() {vm.randomizeStats();}, this.updateInterval);
}
},
//计算属性
computed:{
},
//侦听
watch:{
//图形显示边数
sides: function(newSides, oldSides) {
let sidesDifference = newSides - oldSides;
if (sidesDifference > 0)
{
for (let i = 1; i <= sidesDifference; i++)
{
this.stats.push(this.newRandomValue());
}
}
else
{
let absoluteSidesDifference = Math.abs(sidesDifference);
for (let i = 1; i <= absoluteSidesDifference; i++)
{
this.stats.shift();
}
}
},
//图形动画
stats: function(newStats) {
TweenLite.to(this.$data, this.updateInterval / 1000, {points: generatePoints(newStats)});
},
//更新图形变动时间间隔
updateInterval: function() {
this.resetInterval();
}
},
//渲染dom
mounted:function(){
this.resetInterval();
}
}
</script>
<style scoped>
svg {
display: block;
}
polygon {
fill: #41b883;
}
circle {
fill: transparent;
stroke: #35495e;
}
input[type="range"] {
display: block;
width: 100%;
margin-bottom: 15px;
}
.svgDiv{
width: 40%;
margin: auto;
}
</style>
2.2.3 把过渡放到组件里
管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。我们以数字文本输入改变动画为例,写一个通用组件
实例代码:
<template>
<div>
<h3>把过渡放到组件里</h3>
<input v-model.number="firstNumber" type="number" step="20"> +
<input v-model.number="secondNumber" type="number" step="20"> =
{{ result }}
<p>
<animated-integer v-bind:value="firstNumber"></animated-integer> +
<animated-integer v-bind:value="secondNumber"></animated-integer> =
<animated-integer v-bind:value="result"></animated-integer>
</p>
</div>
</template>
<script>
import TWEEN from 'tween';
//AnimateInteger子组件
var AnimateInteger={
data:function(){
return {
tweeningValue:0
}
},
props: {
value: {
type: Number,
required: true
}
},
methods:{
tween: function (startValue, endValue) {
var vm = this;
function animate () {
if (TWEEN.update()) {
requestAnimationFrame(animate);
}
}
new TWEEN.Tween({ tweeningValue: startValue }).to({ tweeningValue: endValue }, 500).onUpdate(function () {
vm.tweeningValue = this.tweeningValue.toFixed(0);
}).start();
animate();
}
},
//侦听
watch: {
value: function (newValue, oldValue) {
this.tween(oldValue, newValue)
}
},
//dom渲染
mounted: function () {
this.tween(0, this.value)
},
template:`<span>{{ tweeningValue }}</span>`
};
export default {
//数据
data(){
return {
firstNumber: 20,
secondNumber: 40
}
},
created: function () {
},
//注册局部组件
components:{
'animated-integer':AnimateInteger
},
//方法
methods:{
},
//计算属性
computed:{
//计算结果
result: function () {
return this.firstNumber + this.secondNumber;
}
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
3.可复用性与组合
3.1 混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
那也就是说,你可以单独写个逻辑文件,默认导出一个对象,对象里面可以包含data、created、mounted、methods
等vue模板文件中的逻辑对象。接着可以将这个对象引入到多个vue模板中进行功能复用,从而达到功能模块的逻辑封装,便于使用及后期维护。
3.1.1 基础
首先我们在项目src目录下创建mixins文件夹,用于存放混入组件的文件
创建basemixins.js文件,来实现一个基础混入,代码:
/**基础混入的使用 */
export default{
methods: {
hello: function () {
console.log('hello from mixin!')
}
},
created() {
this.hello();
}
}
vue页面中引入 basemixins.js,代码:
<template>
<div>
<h3>基础混入</h3>
</div>
</template>
<script>
import basemixin from '@/mixins/basemixins' //引入js
export default {
mixins: [basemixin], // 使用混入
//数据
data(){
return {
firstNumber: 20,
secondNumber: 40
}
},
created: function () {
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
运行项目,查看控制台的输出:
3.1.2 选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
3.1.2.1 数据对象合并
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
创建seelctmixin.js,代码:
export default{
data(){
return{
message:"hello",
foo:"abc"
}
}
}
vue页面中引入 seelctmixin.js,代码:
<template>
<div>
<h3>混入——选项合并之数据对象</h3>
<p>{{message}}</p>
<p>{{foo}}</p>
<p>{{bar}}</p>
</div>
</template>
<script>
import selectmixin from '@/mixins/selectmixin' //引入js
export default {
mixins: [selectmixin], // 使用混入
//数据
data(){
return {
message:"hello world",
foo:"abc1234",
bar:"def"
}
},
created: function () {
console.log(this.$data)
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
渲染效果:
3.1.2.2 同名钩子函数合并
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用
seelctmixin.js代码:
export default{
created: function () {
console.log('混入对象的钩子被调用')
}
}
vue页面代码:
<template>
<div>
<h3>混入——同名钩子函数合并</h3>
</div>
</template>
<script>
import selectmixin from '@/mixins/selectmixin' //引入js
export default {
mixins: [selectmixin], // 使用混入
//数据
data(){
return {
message:"hello world",
foo:"abc1234",
bar:"def"
}
},
created: function () {
console.log('组件钩子被调用')
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
运行项目,查看控制台的输出:
3.1.2.3 值为对象的选项合并
值为对象的选项,例如 methods
、components
和 directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
seelctmixin.js代码:
export default{
methods: {
foo() {
console.log('foo')
},
conflicting() {
console.log('from mixin')
}
}
}
vue页面代码:
<template>
<div>
<h3>混入——值为对象的选项合并</h3>
</div>
</template>
<script>
import selectmixin from '@/mixins/selectmixin' //引入js
export default {
mixins: [selectmixin], // 使用混入
//数据
data(){
return {
message:"hello world",
}
},
created: function () {
this.foo();
this.bar();
this.conflicting();
},
//注册局部组件
components:{
},
//方法
methods:{
bar() {
console.log('bar')
},
conflicting() {
console.log('from self')
}
},
//计算属性
computed:{
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
运行项目,查看控制台的输出:
3.1.3 全局混入
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。
在main.js中定义一个全局混入,代码:
import Vue from 'vue'
import App from './App.vue'//引入根组件
import router from './router/router.js'//引用router.js
import '@/assets/css/common.scss' // 全局样式
import animate from 'animate.css'
Vue.use(animate)
Vue.config.productionTip = false;
//实例化vue
new Vue({
router,
render: h => h(App),//导入根组件
}).$mount('#app');//mount挂载应用 根组件id
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created:function(){
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
});
vue页面代码:
<template>
<div>
<h3>全局混入</h3>
</div>
</template>
<script>
export default {
//数据
myOption: 'hello!',
data(){
return {
}
},
created: function () {
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
运行项目,查看控制台输出:
请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。
3.1.4 自定义选项合并策略
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies
添加一个函数:
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
对于多数值为对象的选项,可以使用与 methods
相同的合并策略:
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
可以在 Vuex 1.x 的混入策略里找到一个更高级的例子:
const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
if (!toVal) return fromVal
if (!fromVal) return toVal
return {
getters: merge(toVal.getters, fromVal.getters),
state: merge(toVal.state, fromVal.state),
actions: merge(toVal.actions, fromVal.actions)
}
}
3.2 自定义指令
3.2.1 简介
除了核心功能默认内置的指令 (v-model
和 v-show
),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
这里我们以文本框输入聚焦为示例:当页面加载时,该元素将获得焦点 (注意:autofocus
在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能:
全局指令注册方式(main.js),代码:
import Vue from 'vue'
import App from './App.vue'//引入根组件
import router from './router/router.js'//引用router.js
Vue.config.productionTip = false;
//实例化vue
new Vue({
router,
render: h => h(App),//导入根组件
}).$mount('#app');//mount挂载应用 根组件id
//注册全局v-focus指令
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
});
vue页面中局部注册 ,代码:
<template>
<div>
<h3>自定义指令——v-focus</h3>
<input v-focus v-model="message">
</div>
</template>
<script>
export default {
//注册局部指令v-focus
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
},
//数据
data(){
return {
message:""
}
}
}
</script>
<style scoped>
</style>
vue页面中,input输入框直接使用v-focus指令即可:
<template>
<div>
<h3>自定义指令——v-focus</h3>
<input v-focus v-model="message">
</div>
</template>
<script>
export default {
//数据
data(){
return {
message:""
}
}
}
</script>
<style scoped>
</style>
3.2.2 钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
3.2.2.1 钩子函数参数
指令钩子函数会被传入以下参数:
- el:指令所绑定的元素,可以用来直接操作 DOM。
- binding:一个对象,包含以下 property:
- name:指令名,不包括 v- 前缀。
- value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
- expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
- arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
- modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
- vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
除了 el
之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
现在我们使用钩子函数参数性质,来自定义一个钩子:
<template>
<div>
<h3>自定义指令——钩子函数自定义钩子</h3>
<p v-demo:foo.a.b="message"></p>
</div>
</template>
<script>
export default {
//自定义钩子
directives: {
demo: {
//钩子的定义
bind: function (el, binding, vnode) {
var s = JSON.stringify;
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ');
}
}
},
//数据
data(){
return {
message:"hello"
}
}
}
</script>
<style scoped>
</style>
3.2.2.2 动态指令参数
指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
这里我们通过用来通过固定布局将元素固定在页面上,定义一个指令,可以设定元素固定位置:
<template>
<div>
<h3>自定义指令——动态指令参数</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
</template>
<script>
export default {
//自定固定位置的动态指令
directives: {
pin: {
bind: function (el, binding, vnode) {
el.style.position = 'fixed';
var s = (binding.arg == 'left' ? 'left' : 'top');
el.style[s] = binding.value + 'px';
console.log(vnode);
}
}
},
//数据
data(){
return {
direction: 'left'
}
}
}
</script>
<style scoped>
</style>
3.2.3 函数简写
在很多时候,你可能想在 bind
和 update
时触发相同行为,而不关心其它的钩子。比如这样写:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
3.2.4 对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。
实例代码:
<template>
<div>
<h3>自定义指令——对象字面量</h3>
<p v-demo="{ color: 'white', text: 'hello!' }"></p>
</div>
</template>
<script>
export default {
//对象字面量传参
directives: {
demo: {
bind: function (el, binding) {
el.innerHTML=binding.value.color+" "+binding.value.text;
}
}
},
//数据
data(){
return {
direction: 'left'
}
},
created: function () {
},
//注册局部组件
components:{
},
//方法
methods:{
},
//计算属性
computed:{
},
//侦听
watch:{
},
//渲染dom
mounted:function(){
}
}
</script>
<style scoped>
</style>
3.3 渲染函数与JSX
3.3.1 基础
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
这里我们通过生成带锚点的标题的组件为示例,一个通过普通方式实现,一个通过render渲染函数实现,深入了解前后不同
普通方式实现:
<template>
<div>
<h3>渲染函数</h3>
<anchored-heading :level="1">h1标题</anchored-heading>
<anchored-heading :level="2">h2标题</anchored-heading>
</div>
</template>
<script>
//定义锚点标题组件——未使用渲染函数
var anchoredHeading={
data:function(){
return{
}
},
methods:{
},
props: {
level: {
type: Number,
required: true
}
},
template:`<div>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</div>`
};
export default {
//数据
data(){
return {
message: 'hello world'
}
},
//注册局部组件
components:{
"anchored-heading":anchoredHeading
}
}
</script>
<style scoped>
</style>
使用渲染函数实现:
<template>
<div>
<h3>渲染函数</h3>
<anchored-heading :level="1">h1标题</anchored-heading>
<anchored-heading :level="2">h2标题</anchored-heading>
<anchored-heading :level="3">h3标题</anchored-heading>
</div>
</template>
<script>
//定义锚点标题组件——使用渲染函数
var anchoredHeading={
data:function(){
return{
}
},
methods:{
},
props: {
level: {
type: Number,
required: true
}
},
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
template:`<div></div>`
};
export default {
//数据
data(){
return {
message: 'hello world'
}
},
//注册局部组件
components:{
"anchored-heading":anchoredHeading
}
}
</script>
<style scoped>
</style>
通过前后代码对比我们会发现,没有使用渲染函数的代码要比使用渲染函数的代码更显的冗长
3.3.2 节点、树以及虚拟DOM
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
不同的节点,就组成了树。
虚拟DOM:Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。我们可通过createElement创建虚拟DOM,比如:
return createElement('h1', this.blogTitle)
createElement
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription
,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
3.3.3 createElement参数
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
3.3.3.1 深入数据对象
有一点要注意:正如 v-bind:class
和 v-bind:style
在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML
这样的 DOM property (这会覆盖 v-html
指令)。
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
有了以上关于渲染函数等知识,我们现在写一个完整的示例:
<template>
<div>
<h3>渲染函数——深入数据对象</h3>
<anchored-heading :level="1">h1标题</anchored-heading>
<anchored-heading :level="2">h2标题</anchored-heading>
<anchored-heading :level="3">h3标题</anchored-heading>
</div>
</template>
<script>
//获取子节点文本内容
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children? getChildrenTextContent(node.children): node.text;
}).join('');
}
//定义锚点标题组件——使用渲染函数
var anchoredHeading={
data:function(){
return{
}
},
methods:{
},
props: {
level: {
type: Number,
required: true
}
},
render: function (createElement) {
// 创建 kebab-case 风格的 ID
let headingId = getChildrenTextContent(this.$slots.default).toLowerCase().replace(/\W+/g, '-').replace(/(^-|-$)/g, '');
return createElement(
'h' + this.level, // 标签名称
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
);
},
template:`<div></div>`
};
export default {
//数据
data(){
return {
direction: 'left'
}
},
//注册局部组件
components:{
"anchored-heading":anchoredHeading
}
}
</script>
<style scoped>
</style>
3.3.3.2 约束
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 错误 - 重复的 VNode
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
3.3.4 使用JavaScript代替模板功能
3.3.4.1 v-if 和v-for
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if
和 v-for
:
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
这些都可以在渲染函数中用 JavaScript 的 if
/else
和 map
来重写:
props: ['items'],
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}
3.3.4.2 v-model
渲染函数中没有与 v-model
的直接对应——你必须自己实现相应的逻辑:
这就是深入底层的代价,但与 v-model
相比,这可以让你更好地控制交互细节
props: ['value'],
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
}
3.3.4.3 事件与按键修饰符
对于 .passive
、.capture
和 .once
这些事件修饰符,Vue 提供了相应的前缀可以用于 on
:
修饰符 | 前缀 |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once 或.once.capture | ~! |
例如:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键:.enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码) |
修饰键:.ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey 、shiftKey 或者 metaKey ) |
这里是一个使用所有修饰符的例子:
on: {
keyup: function (event) {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果按下去的不是 enter 键或者
// 没有同时按下 shift 键
// 则返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
}
3.3.4.4 插槽
你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots
字段:
render: function (createElement) {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return createElement('div', [
createElement('child', {
// 在数据对象中传递 `scopedSlots`
// 格式为 { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
3.3.5 JSX
如果你写了很多 render
函数,可能会觉得下面这样的代码写起来很痛苦:
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
特别是对应的模板如此简单的情况下:
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
将 h
作为 createElement
的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement
,这样你就可以去掉 (h)
参数了。对于更早版本的插件,如果 h
在当前作用域中不可用,应用会抛错。
了解关于更多JSX,参见GitHub - vuejs/jsx-vue2: monorepo for Babel / Vue JSX related packages
3.3.6 函数式组件
之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。一个函数式组件就像这样:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则
props
选项是必须的。在 2.3.0 或以上的版本中,你可以省略props
选项,所有组件上的 attribute 都会被自动隐式解析为 prop。当使用函数式组件时,该引用将会是 HTMLElement,因为他们是无状态的也是无实例的。
在 2.5.0 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
<template functional>
</template>
组件需要的一切都是通过 context
参数传递,它是一个包括如下字段的对象:
props
:提供所有 prop 的对象children
:VNode 子节点的数组slots
:一个函数,返回了包含所有插槽的对象scopedSlots
:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,作为createElement
的第二个参数传入组件parent
:对父组件的引用listeners
:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是data.on
的一个别名。injections
:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。
在添加 functional: true
之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context
参数,并将 this.$slots.default
更新为 context.children
,然后将 this.level
更新为 context.props.level
。
因为函数式组件只是函数,所以渲染开销也低很多。
在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:
- 程序化地在多个组件中选择一个来代为渲染;
- 在将
children
、props
、data
传递给子组件之前操作它们。
下面是一个 smart-list
组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:
<template>
<div>
<h3>函数式组件</h3>
<smart-list v-bind="smartDatas">smart-list</smart-list>
</div>
</template>
<script>
//定义组件——使用渲染函数
var smartList={
functional: true,
data:function(){
return{
}
},
methods:{
},
props: {
level: {
type: String,
required: true
},
title: {
type: String,
required: true
}
},
render:function (createElement, context) {
// 子节点数组
console.log("上下文:",context);
let children = [];
// 属性获取的变化
const {title,level} = context.props;
children = children.concat(context.children);
return createElement(
'h'+level,
{ attrs: { title } },
children
)
}
};
export default {
//数据
data(){
return {
smartDatas:{
level:'1',
title:"hello world"
}
}
},
//注册局部组件
components:{
"smart-list":smartList
}
}
</script>
<style scoped>
</style>
3.3.6.1 向子元素或子组件传递 attribute 和事件
在普通组件中,没有被定义为 prop 的 attribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并。
然而函数式组件要求你显式定义该行为:
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 完全透传任何 attribute、事件监听器、子节点等。
return createElement('button', context.data, context.children)
}
})
通过向 createElement
传入 context.data
作为第二个参数,我们就把 my-functional-button
上面所有的 attribute 和事件监听器都传递下去了。事实上这是非常透明的,以至于那些事件甚至并不要求 .native
修饰符。
如果你使用基于模板的函数式组件,那么你还需要手动添加 attribute 和监听器。因为我们可以访问到其独立的上下文内容,所以我们可以使用 data.attrs
传递任何 HTML attribute,也可以使用 listeners
(即 data.on
的别名) 传递任何事件监听器。
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
3.3.6.2 slots() 和 children 对比
你可能想知道为什么同时需要 slots()
和 children
。slots().default
不是和 children
类似的吗?在一些场景中,是这样——但如果是如下的带有子节点的函数式组件呢?
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>
对于这个组件,children
会给你两个段落标签,而 slots().default
只会传递第二个匿名段落标签,slots().foo
会传递第一个具名段落标签。同时拥有 children
和 slots()
,因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children
,移交给其它组件去处理。