插值 文本插值 格式{{expression}} 使用v-once指令执行一次插值,后续不再更新 html 插值双大括号将数据解释为普通文本而不是html代码,如果需要渲染为html,则需要使用v-html指令
指令 参数 一些指令能够接收一个参数,在指令名称之后以冒号表示
1 2 <a v-bind:href ="url" > 跳转</a > <a v-on:click ="todo" > 点击</a >
动态参数 动态参数会转换为字符串,异常情况下为null,将会移除绑定 1 2 3 4 5 6 7 8 9 10 11 12 <a v-bind: [attributeName ]="url" > 跳转</a > <a v-on: [eventName ]="todo" > 事件</a > <script > var app = new Vue({ el: '#app' , data: { attributeName: 'href' , url: 'www.baidu.com' , eventName: 'click' } }) </script >
计算属性和侦听器 计算属性 对于复杂的逻辑,使用计算属性比在插值逻辑或函数中效率更高
计算属性默认只有getter,也可以在定义计算属性的同时指定setter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 computed: { fullName: { get: function ( ) { return this .firstName + ' ' + this .lastName }, set: function (newValue ) { var names = newValue.split(' ' ) this .firstName = names[0 ] this .lastName = names[names.length - 1 ] } } }
侦听器 vue通过watch来响应数据的变化,当侦听的数据发生变化时,执行相应的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 new Vue({ el: '#app' , data: { oldMessage: 'message' , message: 'message' }, watch: { oldMessage: function (newValue, oldValue ) { console .log(newValue + '' + oldValue); this .message = newValue + oldValue; } }, mounted ( ) { setTimeout (() => this .oldMessage = 'suxi' , 1000 ); } })
class 和 style 绑定绑定 class 传入对象,动态切换class
1 2 3 4 5 6 7 8 9 10 11 12 13 <div v-bind:class ="{ active: isActive }" class ="static" > </div > <script > new Vue({ el: '#app' , data: { isActive: true } }) </script > <div class ="static active" > </div >
传入数组,动态应用数组中的class
1 2 3 4 <div v-bind:class ="['static', 'active']" > </div > <div class ="static active" > </div >
自定义组件,在组件上绑定的类名,将会添加在组件的根元素上面,这个元素上存在的类名不会被覆盖
1 2 3 4 5 6 7 8 9 <my-component class ="baz boo" > </my-component > <script > Vue.component('my-component' , { template: '<p class ="foo bar" > Hi</p > ' }) </script > <p class ="foo bar baz boo" > Hi</p >
绑定 style 传入对象,类型css的对象
1 <div v-bind:style ="{color: 'red', fontSize: '14px'}" > </div >
传入对象数组,将多个样式对象应用于同一个元素上
一个样式声明允许提供多个值,这样只会渲染数组中最后一个被浏览器支持的值
1 <div :style ="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }" > </div >
条件渲染 v-if / v-else-if / v-elsev-if / v-else-if指令可以条件性的渲染一块内容,v-else当不满足条件时渲染某块
1 2 3 <div v-if ="total > 100" > 111</div > <div v-else-if ="total > 80" > 222</div > <div v-else > 333</div >
vue会高效的渲染元素,通常会复用已有的元素而不是从头渲染,如果在一个业务中,两个元素逻辑上是相互独立的,共用一个元素显然是不合理的。vue采用添加唯一的key来避免复用
v-showv-show不支持template和v-else,v-show渲染的元素会被保留在dom中,只切换元素css中的display属性,所以v-if有更高的切换开销,而v-show有更高的初始化渲染开销,切换频次少使用v-if,反之使用v-show
列表渲染 数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <ul id ="example-1" > <li v-for ="(item, index) in items" :key ="item.message" > {{item.message}} -- {{index}} </li > </ul > <script > new Vue({ el: '#example-1' , data: { items: [ {message : 'foo' }, {message : 'bar' } ] } }) </script >
对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <ul id ="v-for-object" class ="demo" > <li v-for ="(value, name, index) in object" > {{value}} -- {{name}} -- {{index}} </li > </ul > <script > new Vue({ el: '#v-for-object' , data: { object: { title: 'How to do lists in Vue' , author: 'Jane Doe' } } }) </script >
维护状态 vue根据每项提供的唯一的key来追踪每一个节点的身份,使用v-for时,建议为每一项提供唯一的key,在组件中使用v-for,key是必须的
数组更新检测 vue重写了数组的一些方法,调用以下方法将会触发视图更新pushpopshiftunshiftsplicesortreverse数组的一些方法不改变原数组而是返回一个新数组,用新数组替换原数组也会触发视图更新 数组过滤 / 排序(使用计算属性) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <ul > <li v-for ="item in filter" :key ="item" > {{item}}</li > </ul > <script > new Vue({ el: 'ul' , data: { nums: [1, 2, 3, 4, 5, 6], }, computed: { filter: function ( ) { return this .nums.filter(item => item % 2 === 0 ) } } }) </script >
v-for 和 v-if 一起使用不推荐在同一个元素中同时使用 v-for 和 v-if,v-for 的优先级高于v-if,当只渲染部分节点时,可以同时使用~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <ul > <li v-for ="item in todos" v-if ="item.isShow" :key ="item.key" > {{item.todo}}</li > </ul > <script > new Vue({ el: 'ul' , data: { todos: [ {isShow : true , todo : '起床' , key : 'one' }, {isShow : false , todo : '早餐' , key : 'two' }, {isShow : true , todo : '午饭' , key : 'three' } ], } }) </script
事件绑定 使用 v-on 指令监听 dom事件,例如 v-on:click="function",可以使用简写 @,例如 @click="function"
事件处理方法 在 v-on 指令中直接书写 js 代码
1 2 3 4 5 6 7 8 9 10 11 12 <div class ="app" > <button @click ="sum += 2" > 点击</button > <p > sum: {{sum}}</p > </div > <script > new Vue({ el: '.app' , data: { sum: 0, }, }) </script >
事件处理流程写入方法中(默认将event对象传入)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div class ="app" > <button @click ="add" > 点击</button > <p > sum: {{sum}}</p > </div > <script > new Vue({ el: '.app' , data: { sum: 0, }, methods: { add (event ) { console .log(event); this .sum += 2 ; } } }) </script >
内联处理器中的方法 除了直接绑定方法,还可以在指令中直接调用方法,并将参数传入,如果需要在内联语句中访问dom事件,可以用特殊变量 $event 传入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div class ="app" > <button @click ="add(2, $event)" > 点击</button > <p > sum: {{sum}}</p > </div > <script > new Vue({ el: '.app' , data: { sum: 0, }, methods: { add (num, event ) { console .log(event); this .sum += num; } } }) </script >
事件修饰符 在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求,vue 提供了事件修饰符来处理 dom 事件细节
.stop .prevent .capture .self .once .passive passive 这个修饰符会执行默认的方法,但明明是默认执行为什么要设置这个修饰符呢?因为浏览器只有等内核线程执行到事件监听器对应的 JavaScript 代码时,才能知道内部是否会调用 preventDefault 函数来阻止事件的默认行为,所以浏览器本身是没有办法对这种场景进行优化的。这种场景下,用户的手势事件无法快速产生,会导致页面无法快速执行滑动逻辑,从而让用户感觉到页面卡顿。通俗点说就是每次事件产生,浏览器都会去查询一下是否有 preventDefault 阻止该次事件的默认动作。我们加上 passive 就是为了告诉浏览器,不用查询了,我们没用 preventDefault 阻止默认动作, 这里一般用在滚动监听,@scoll,@touchmove。因为滚动监听过程中,移动每个像素都会产生一次事件,每次都使用内核线程查询 prevent 会使滑动卡顿。我们通过 passive 将内核线程查询跳过,可以大大提升滑动的流畅度
使用修饰符时,顺序很重要,相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。passive 和 prevent 冲突,不能同时绑定在一个监听器上
按键修饰符 监听键盘事件时,v-on在监听键盘事件时添加按键修饰符
1 2 <input v-on:keyup.enter ="submit" >
按键码 keyCode 的事件用法已经被废弃了并可能不会被最新的浏览器支持
1 2 <input v-on:keyup.13 ="submit" >
vue 提供了绝大多数常用的按键码的别名,并且有一些按键 (.esc 以及所有的方向键) 在 IE9 中有不同的 key 值, 如果你想支持 IE9,这些内置的别名应该是首选
.enter.tab.delete.esc.space.up.down.left.right1 2 3 Vue.config.keyCodes.f1 = 112;
系统修饰符 用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器 .exact修饰符.exact 修饰符允许控制由精确的系统修饰符组合触发的事件请注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件。如果你想要这样的行为,请为 ctrl 换用 keyCode:keyup.17
1 2 3 4 5 6 7 8 <button v-on:click.ctrl ="onClick" > A</button > <button v-on:click.ctrl.exact ="onCtrlClick" > A</button > <button v-on:click.exact ="onClick" > A</button >
鼠标按钮修饰符 .left.right.middle表单输入绑定 v-model 指令在表单 input、textarea、select 元素上创建双向数据绑定。根据控件类型自动选取正确的方法来更新元素。v-model 会忽略所有表单元素的 value、checked、selected 属性的初始值而总是将vue 实例的数据作为数据来源
v-model在内部为不同的输入元素使用不同的property并抛出不同的事件text和textarea元素使用value属性和input事件checkbox和radio使用checked属性和change事件select元素将value作为prop并将change作为事件修饰符 .lazy,在默认情况下 v-model 在每次 input 事件触发后将输入框的值与数据进行同步,添加 .lazy 修饰符,从而转为在 change 事件之后进行同步.number,自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符,如果这个值无法被 parseFloat() 解析,则会返回原始的值.trim,自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符组件 1 2 3 4 5 6 7 8 9 10 11 12 Vue.component('button-counter' , { data: function ( ) { return { count: 0 } }, template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>' }) <button-counter></button-counter>
data在组件中,data必须是一个函数,因为每一个实例需要维护被返回对象的独立的拷贝
prop 传递数据1 2 3 4 Vue.component('blog-post' , { props: ['title' ], template: '<h3>{{title}}</h3>' })
监听子组件事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <div id ="blog-posts-events-demo" > <div :style ="{ fontSize: postFontSize + 'em' }" > <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" v-on:enlarge-text="postFontSize += 0.1" ></blog-post > </div > </div > <script > Vue.component('blog-post' , { props: ['post' ], template: ` <div class ="blog-post" > <h3 > {{ post.title }} </h3 > <button v-on:click ="$emit('enlarge-text')" > Enlarge text </button > <div v-html ="post.content" > </div > </div > ` }) new Vue({ el: '#blog-posts-events-demo' , data: { posts: [], postFontSize: 1 } }) </script >
使用事件抛出一个值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <button v-on:click ="$emit('enlarge-text', 0.1)" > Enlarge text </button > <blog-post v-on:enlarge-text="postFontSize += $event" ></blog-post > <blog-post v-on:enlarge-text="changeFontSize" ></blog-post > <script > methods: { changeFontSize (size ) { this .postFontSize += size; } } </script >
组件中使用 v-model 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <input v-model ="searchText" /> <input :value ="searchText" @input ="searchText = $event.target.value" /> <my-component v-model="searchText" ></my-component > <my-conponent :value="searchText" @input="searchText = $event" ></my-component > <script > Vue.component('my-component' , { props: ['value' ], template: ` <input :value="value" @input="$emit('input', $event.target.value)" /> ` }) </script >
动态组件 vue 的 component 元素加一个特殊的 is 属性来实现动态组件,is后面的值可以是一个组件的名字或者是一个组件的选项对象
组件名 使用 kebab-case 当使用 kebab-case 定义一个组件时,也必须在引用这个自定义元素时使用kebab-case
1 2 3 4 Vue.component('my-component-name' , { }) <my-component-name></my-component-name>
使用 PascalCase 当使用 PascalCase 定义一个组件时,引用这个自定义元素时两种命名法都可以使用
1 2 3 4 5 Vue.component('MyComponentName' , { }) <my-component-name></my-component-name> <MyComponentName></MyComponentName>
解析 dom 模板时的注意事项 有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部
1 2 3 4 5 6 7 8 9 <table > <item > </item > </table > <script > Vue.component('item' , { template: '<tr > <td > 1</td > </tr > <tr > <td > 2</td > </tr > ' }) </script >
上例子中,自定义组件会被提升到外部,并导致最终渲染失败,可以通过 is 属性来避免
1 2 3 <table > <tr is ="item" > </tr > </table >
当然如果使用单文件组件和 x-template 以及模板字符串定义的模板,那么这条限制是不存在的
全局注册 通过 Vue.component 来创建的组件都是全局注册的组件,注册以后就可以用在任何新创建的 vue 根实例
1 2 3 4 5 Vue.component('component-a' , { }) Vue.component('component-b' , { }) Vue.component('component-c' , { }) new Vue({ el : '#app' })
局部注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 var ComponentA = { }var ComponentB = { }var ComponentC = { }new Vue({ el: '#app' , components: { 'component-a' : ComponentA, 'component-b' : ComponentB } }) import ComponentA from './ComponentA.vue' export default { components: { ComponentA }, } var ComponentA = { }var ComponentB = { components: { 'component-a' : ComponentA }, }
基础组件的自动化全局注册 使用 webpack 中的方法 require.context 自动化导入组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 require .context(directory: String , useSubdirectories : Boolean , regExp : RegExp ): Object require .context('./components' , true , /\.js$/ );var map = { "./A.js" : "./src/components/test/components/A.js" , "./B.js" : "./src/components/test/components/B.js" , "./C.js" : "./src/components/test/components/C.js" , "./D.js" : "./src/components/test/components/D.js" }; function webpackContext (req ) { var id = webpackContextResolve(req); return __webpack_require__(id); } function webpackContextResolve (req ) { var id = map[req]; if (!(id + 1 )) { var e = new Error ("Cannot find module '" + req + "'" ); e.code = 'MODULE_NOT_FOUND' ; throw e; } return id; } webpackContext.keys = function webpackContextKeys ( ) { return Object .keys(map); }; webpackContext.resolve = webpackContextResolve; module .exports = webpackContext; webpackContext.id = "./src/components/test/components sync recursive \\.js$" ; const webpack = require .context('./components' , true , /\.js$/ );const map = {};for (const key of webpack.keys()){ map[key] = webpack(key); } console .log(map);
require.context 执行后,返回一个方法 webpackContext,这个方法又返回一个 __webpack_require__ ,这个__webpack_require__ 就相当于 require 或者 import。同时webpackContext 还有二个静态方法 keys 与 resolve,一个 id 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import Vue from 'vue' import upperFirst from 'lodash/upperFirst' import camelCase from 'lodash/camelCase' const requireComponent = require .context( './components' , false , /Base[A-Z]\w+\.(vue|js)$/ ) requireComponent.keys().forEach(fileName => { const componentConfig = requireComponent(fileName) const componentName = upperFirst( camelCase( fileName .split('/' ) .pop() .replace(/\.\w+$/ , '' ) ) ) Vue.component( componentName, componentConfig.default || componentConfig ) })
全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生
propprop 的大小写HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当使用 DOM 中的模板时,camelCase 的 prop 名需要使用其等价的 kebab-case 命名,如果使用字符串模板,那么这个限制就不存在了
1 2 3 4 5 6 7 8 9 Vue.component('blog-post' , { props: ['postTitle' ], template: '<h3>{{ postTitle }}</h3>' }) <blog-post post-title="hello!" ></blog-post>
prop 类型1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 props: ['title' , 'likes' , 'isPublished' , 'commentIds' , 'author' ]; props: { title: String , likes: Number , isPublished: Boolean , commentIds: Array , author: Object , callback: Function , contactsPromise: Promise } <blog-post title="My journey with Vue" ></blog-post> <blog-post v-bind:title="post.title" ></blog-post> <blog-post v-bind:likes="42" ></blog-post> <blog-post is-published></blog-post> <blog-post v-bind:is-published="false" ></blog-post> <blog-post v-bind:is-published="post.isPublished" ></blog-post> <blog-post v-bind:comment-ids="[234, 266, 273]" ></blog-post> <blog-post v-bind:comment-ids="post.commentIds" ></blog-post> <blog-post v-bind:author="{ name: 'Veronica', company: 'Veridian Dynamics' }" ></blog-post> <blog-post v-bind:author="post.author" ></blog-post> post: { id: 1 , title: 'My Journey with Vue' } <blog-post v-bind="post" ></blog-post> <blog-post v-bind:id="post.id" v-bind:title="post.title" ></blog-post>
单向数据流 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 props: ['initialCounter' ], data: function ( ) { return { counter: this .initialCounter } } props: ['size' ], computed: { normalizedSize: function ( ) { return this .size.trim().toLowerCase() } }
prop 验证可以为组件的 prop 指定验证要求,如果有一个需求没有被满足,则 Vue 会在浏览器控制台中发出警告
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Vue.component('my-component' , { props: { propA: Number , propB: [String , Number ], propC: { type: String , required: true }, propD: { type: Number , default : 100 }, propE: { type: Object , default : function ( ) { return { message : 'hello' } } }, propF: { validator: function (value ) { return ['success' , 'warning' , 'danger' ].indexOf(value) !== -1 } } } })
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的
type 可以是下面原生的构造函数或其他自定义构造函数,并且通过 instanceof 来进行检查确认StringNumberBooleanArrayObjectDateFunctionSymbol1 2 3 4 5 6 7 8 9 10 function Person (firstName, lastName ) { this .firstName = firstName; this .lastName = lastName; } Vue.component('blog-post' , { props: { author: Person, } })
非 prop 的 Attribute 一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute,因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上 ,例如组件的 class 属性会被添加到组件的根元素上
替换 / 合并已有的 Attribute 1 2 3 4 5 6 7 8 Vue.component('myInput' , { template: `<input type="date" class="form-control" />` }) <my-input type="text" class ="abc" /> <input type="text" class ="form-control abc" />
对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值,所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏 !庆幸的是,class 和 style attribute 会稍微智能一些,即两边的值会被合并起来
禁止 Attribute 继承 如果不希望组件的根元素 继承 attribute,在组件选项中设置 inheritAttrs: false
1 2 3 Vue.component('my-component' , { inheritAttrs: false })
禁止组件根元素继承属性以后,依然可以使用 $attrs 手动决定这些属性将会赋予哪个元素,$attrs 会将没有声明的属性合并为一个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Vue.component('base-input' , { props: ['label' , 'value' ], template: ` <label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > </label> ` }) Vue.component('base-input' , { inheritAttrs: false , props: ['label' , 'value' ], template: ` <label> {{ label }} <input v-bind:placeholder="$attrs.placeholder" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > </label> ` }) <base-input label="姓名" value="苏西" type="text" placeholder="请输入姓名~" ></base-input>
inheritAttrs: false 不会影响 style 和 class 的绑定
自定义事件 事件名 1 2 3 4 5 <my-component v-on:my-event="doSomething" ></my-component> this .$emit('myEvent' );
不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到,因此,我们推荐始终使用 kebab-case 的事件名
自定义组件的 v-model 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框 等类型的输入控件可能会将 value attribute 用于不同的目的,model 选项可以用来避免这样的冲突
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Vue.component('base-checkbox' , { model: { prop: 'checked' , event: 'change' }, props: { checked: Boolean }, template: ` <input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)" > ` }) <base-checkbox v-model="lovingVue" ></base-checkbox>
将原生事件绑定到组件 如果需要在组件的根元素 上直接监听一个原生事件 ,可以使用 v-on 的 .native 修饰符
1 <base-input v-on:focus.native ="onFocus" > </base-input >
如果尝试监听一个类似 <input> 的非常特定的元素时,这可能不是一个好主意,比如上方的 base-input 组件中如果 <input> 并不是它的根元素 ,那么组件中使用 v-on:focus.native 监听器将静默失败,虽然不会产生任何报错,但是也不会触发 onFocus 的事件,为了解决这个问题,vue 提供了 $listeners 属性,它是一个 Object,里面包含了作用在这个组件的所有监听器,$listeners 属性,可以配合 v-on="$listeners" 将所有事件监听器值向这个组件的某个特定的子元素,当然也可以绑定特定的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { focus: function (event ) {}, input: function (value ) {} } Vue.component('base-input' , { props: ['label' , 'value' ], template: ` <label> {{label}} <input type="text" v-bind:value="value" v-on="$listeners" > </label> ` }) new Vue({ el: '#app' , methods: { onChange (e ) { console .log('change' ); console .log(e); }, onFocus (e ) { console .log('focus' ); console .log(e); } } }) <base-input @change="onChange" @focus="onFocus" ></base-input>
对于 input 类型的如果需要配合 v-model 工作的组件来说,为这些监听器创建一个计算属性通常是有用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Vue.component('base-input' , { inheritAttrs: false , props: ['label' , 'value' ], computed: { inputListeners: function ( ) { var vm = this return Object .assign({}, this .$listeners, { input: function (event ) { vm.$emit('input' , event.target.value) } } ) } }, template: ` <label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on="inputListeners" > </label> ` })
.sync 修饰符在有些情况下,我们可能需要对一个 prop 进行双向绑定 ,不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源 。vue 推荐以 update:myPropName 的模式触发事件取而代之
1 this .$emit('update:title' , newTitle)
父组件监听这个事件,并根据需要变更本地的而数据属性 1 2 3 4 <text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document >
vue 为了方便将这种模式提供了一个缩写,即 .sync 修饰符 1 <text-document v-bind:title.sync ="doc.title" > </text-document >
带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync="doc.title + '!'" 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model,所以将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }",是无法正常工作的
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用
1 2 3 4 5 6 7 8 data ( ) { return { doc: { id: 'xx' , title: 'yy' } } }
1 2 3 4 5 6 7 8 9 10 11 <text-document v-bind.sync ="doc" > </text-document > <text-document v-bind:title.sync ="doc.title" v-bind:id.sync ="doc.id" > </text-document > <text-document v-bind:title="doc.title" v-bind:id="doc.id" v-on:update:title="doc.title = $event" v-on:update:id="doc.id = $event" ></text-document >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <div class ="app" > <span > {{title}}</span > <base-input v-bind:title.sync ="title" > </base-input > </div > <script > Vue.component('base-input' , { inheritAttrs: false , props: ['title' ], template: ` <div class ="text" @click="$emit('update:title', '居庙堂之高则忧其民,处江湖之远而忧其君')" > {{title }} </div > ` }) new Vue({ el: '.app' , data: { title: '不以物喜,不以己悲' } }) </script >
插槽 插槽内容 vue 实现了一套内容分发的 API,将 <slot> 元素作为承载分发内容的出口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <navigation-link url ="/profile" > Your Profile </navigation-link > <a v-bind:href="url" class="nav-link" > <slot > </slot > </a >
插槽作用域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <div class ="app" > <navigation-link url ="/profile" > Your Profile {{title}} {{url}} </navigation-link > </div > <script > Vue.component('navigation-link' , { template: ` <div > <span > 插槽</span > <slot > </slot > </div > ` }) new Vue({ el: '.app' , data: { title: '不以物喜,不以己悲' } }) </script >
父级模板里的所有内容都是在父级作用域中编译的,子模板里的所有内容都是在子作用域中编译的
后备内容 有时为一个插槽设置具体的后备 内容是很有用的,它只会在没有提供内容的时候被渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <button type ="submit" > <slot > </slot > </button > <button type ="submit" > <slot > Submit</slot > </button > <submit-button > </submit-button > <submit-button > save</submit-button > <submit-button > <div style ="width: 10px; height: 10px; background: red; border-radius: 50%;" > </div > </submit-button >
具名插槽 1 2 3 4 5 6 7 8 9 10 11 12 13 <div class ="container" > <header > <slot name ="header" > </slot > </header > <main > <slot > </slot > </main > <footer > <slot name ="footer" > </slot > </footer > </div >
slot 的一个特殊的属性 name,这个属性就是用来定义额外的插槽的 ,其中一个不带 name 属性的 slot 带有隐含的名字 default ,在向具名插槽提供内容时,在 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <base-layout > <template v-slot:header > <h1 > Here might be a page title</h1 > </template > <template v-slot:default > <p > A paragraph for the main content.</p > <p > And another one.</p > </template > <template v-slot:footer > <p > Here's some contact info</p > </template > </base-layout >
v-slot 一般只能添加在 <template> 上
作用域插槽 有时候需要插槽中的内容 能够访问到子组件的数据,那么就需要作用域插槽,将子组件 slot 元素中的数据通过 v-bind 暴露给插槽,插槽通过 v-slot:[param]="slotProps" 获取到一个包含子组件的 slot 中 v-bind 参数的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <div class ="app" > <div > <current-user > <template > {{user.firstName}} </template > </current-user > </div > <div > <current-user > <template v-slot:default ="slotProps" > {{slotProps.user.firstName}} </template > </current-user > </div > </div > <script > Vue.component('current-user' , { data ( ) { return { user: { firstName: '苏西' , lastName: '佩奇' } } }, template: ` <span > <slot v-bind:user ="user" > {{user.lastName }} </slot > </span > ` }) new Vue({ el: '.app' }) </script >
独占默认插槽的缩写语法 在上面情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用 ,这样我们就可以把 v-slot 直接用在组件上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <current-user v-slot:default ="slotProps" > {{ slotProps.user.firstName }} </current-user > <current-user v-slot ="slotProps" > {{ slotProps.user.firstName }} </current-user > <current-user v-slot ="slotProps" > {{ slotProps.user.firstName }} <template v-slot:other ="otherSlotProps" > slotProps is NOT available here </template > </current-user > <current-user > <template v-slot:default ="slotProps" > {{ slotProps.user.firstName }} </template > <template v-slot:other ="otherSlotProps" > </template > </current-user > <div class ="app" > <div > <current-user > <template v-slot:default ="slotProps" > {{slotProps.user.firstOne}} </template > <template v-slot:slotone ="slotProps" > {{slotProps.brother.firstOne}} </template > </current-user > </div > </div > <script > Vue.component('current-user' , { data ( ) { return { user: { firstOne: '苏西' , lastTwo: '佩奇' }, brother: { firstOne: '乔治1' , lastTwo: '乔治2' } } }, template: ` <span > <slot v-bind:user ="user" > {{user.lastTwo }} </slot > <slot v-bind:brother ="brother" name ="slotone" > {{brother.lastTwo }} </slot > </span > ` }) new Vue({ el: '.app' }) </script >
解构插槽 Prop 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <current-user > <template v-slot:default ="{user}" > {{user.firstName}} </template > </current-user > <current-user v-slot ="{ user: person }" > {{ person.firstName }} </current-user > <current-user v-slot ="{ user = { firstName: 'Guest' } }" > {{ user.firstName }} </current-user >
动态插槽名 1 2 3 4 <base-layout > <template v-slot: [dynamicSlotName ]> </template > </base-layout >
具名插槽的缩写 跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot) 替换为字符 #。例如 v-slot:header 可以被重写为 #header
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <base-layout > <template #header > <h1 > Here might be a page title</h1 > </template > <p > A paragraph for the main content.</p > <p > And another one.</p > <template #footer > <p > Here's some contact info</p > </template > </base-layout > <base-layout > <template v-slot:header > <h1 > Here might be a page title</h1 > </template > <p > A paragraph for the main content.</p > <p > And another one.</p > <template v-slot:footer > <p > Here's some contact info</p > </template > </base-layout >
1 2 3 4 5 6 7 8 9 <current-user #="{ user }" > {{ user.firstName }} </current-user > <current-user #default ="{ user }" > {{ user.firstName }} </current-user >
作用域插槽示例 当需要根据子组件的某些条件来确认是否需要渲染某些内容时,作用域插槽是可用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <div class ="app" > <div > <todo-list > <template v-slot:todo ="{todo}" > <span v-if ="todo.isComplete" > ✓</span > {{todo.text}} </template > </todo-list > </div > </div > <script > Vue.component('todo-list' , { data ( ) { return { filteredTodos: [ {id : '1' , text : '111' , isComplete : true }, {id : '2' , text : '222' , isComplete : false }, {id : '3' , text : '333' , isComplete : true } ] } }, template: ` <ul > <li v-for ="todo in filteredTodos" v-bind:key="todo.id" > <slot name ="todo" v-bind:todo ="todo" > {{ todo.text }} </slot > </li > </ul > ` }) new Vue({ el: '.app' }) </script >
插槽废弃的语法 v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slot 和 slot-scope attribute 的 API 替代方案。虽然在所有的 2.x 版本中 slot 和 slot-scope attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3 中
带有 slot 属性的具名插槽 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <div class ="container" > <header > <slot name ="header" > </slot > </header > <main > <slot > </slot > </main > <footer > <slot name ="footer" > </slot > </footer > </div > <base-layout > <template slot ="header" > <h1 > Here might be a page title</h1 > </template > <p > A paragraph for the main content.</p > <p > And another one.</p > <template slot ="footer" > <p > Here's some contact info</p > </template > </base-layout > <base-layout > <h1 slot ="header" > Here might be a page title</h1 > <p > A paragraph for the main content.</p > <p > And another one.</p > <p slot ="footer" > Here's some contact info</p > </base-layout > <div class ="container" > <header > <h1 > Here might be a page title</h1 > </header > <main > <p > A paragraph for the main content.</p > <p > And another one.</p > </main > <footer > <p > Here's some contact info</p > </footer > </div >
带有 slot-scope 属性的具名插槽 和 v-slot:default="slotProp" 一样可以接收传递给插槽的 slotProp,这个 slotProp 声明了被接收的 prop 对象会作为 slotProps 变量存在于 template 作用域中,slotProp 可以随意命名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <slot-example > <template slot ="default" slot-scope ="slotProps" > {{ slotProps.msg }} </template > </slot-example > <slot-example > <template slot-scope ="slotProps" > {{ slotProps.msg }} </template > </slot-example > <slot-example > <span slot-scope ="slotProps" > {{ slotProps.msg }} </span > </slot-example > <slot-example > <span slot-scope ="{ msg }" > {{ msg }} </span > </slot-example > <todo-list > <template slot ="todo" slot-scope ="{ todo }" > <span v-if ="todo.isComplete" > ✓</span > {{ todo.text }} </template > </todo-list >
动态组件 & 异步组件 在动态组件上使用 keep-alive 1 <component v-bind:is ="currentTabComponent" > </component >
当在这些组件之间切换的时候,有时会想保持这些组件的状态 ,以避免反复重渲染 导致的性能问题
可以看到,选择一篇文章,切换到 archive 之后再切回 posts,不会展示之前显示的文章,这是因为每次切换标签的时候,vue 都创建了一个新的 currentTabComponent 实例
重新创建动态组件的行为通常是非常有用的,但是有时更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来 。为了解决这个问题,可以用一个 <keep-alive> 元素将其动态组件包裹起来
1 2 3 4 <keep-alive > <component v-bind:is ="currentTabComponent" > </component > </keep-alive >
注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册
异步组件 在大型应用中,可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义组件,这个工厂函数会异步解析 组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数 ,且会把结果缓存 起来供未来渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Vue.component('async-example' , function (resolve, reject ) { setTimeout (function ( ) { resolve({ template: '<div>I am async!</div>' }) }, 1000 ) }) Vue.component('async-webpack-example' , () => import ('./my-async-componet' )); new Vue({ components: { 'my-component' : () => import ('./my-async-component' ) } })
处理加载状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const AsyncComponent = () => ({ component: import ('./MyComponent.vue' ), loading: LoadingComponent, error: ErrorComponent, delay: 200 , timeout: 3000 })
处理边界情况 访问元素 & 组件 访问根实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 new Vue({ data: { foo: 1 }, computed: { bar: function ( ) { } }, methods: { baz: function ( ) { } } }) this .$root.foothis .$root.foo = 2 this .$root.barthis .$root.baz()Vue.component('todo-list' , { data ( ) { return { filteredTodos: [ {id : '1' , text : '111' , isComplete : true }, {id : '2' , text : '222' , isComplete : false }, {id : '3' , text : '333' , isComplete : true } ] } }, mounted ( ) { console .log(this .$root.foo); }, template: ` <ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id" > <slot name="todo" v-bind:todo="todo"> <!-- 后备内容 --> {{ todo.text }} </slot> </li> </ul> ` }) new Vue({ el: '.app' , data: { foo: 1 } })
访问父级组件实例 和 $root 类似,$parent 属性可以用来从一个子组件访问父组件的实例 ,它提供了一种机会,可以在后期随时触达父级组件,来替代将数据以 prop 的方式传入子组件的方式
在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Vue.component('todo-list' , { data ( ) { return { filteredTodos: [ {id : '1' , text : '111' , isComplete : true }, {id : '2' , text : '222' , isComplete : false }, {id : '3' , text : '333' , isComplete : true } ] } }, mounted ( ) { console .log(this .$parent.foo); }, template: ` <ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id" > <slot name="todo" v-bind:todo="todo"> <!-- 后备内容 --> {{ todo.text }} </slot> </li> </ul> ` }) new Vue({ el: '.app' , data: { foo: 1 } })
访问子组件或子组件实例和方法 尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件 。为了达到这个目的,可以通过 ref 这个属性为子组件赋予一个 ID 引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <div class ="app" > <div > <todo-list ref ="todolist" > <template v-slot:todo ="{todo}" > <span v-if ="todo.isComplete" > ✓</span > {{todo.text}} </template > </todo-list > </div > </div > <script > Vue.component('todo-list' , { data ( ) { return { filteredTodos: [ {id : '1' , text : '111' , isComplete : true }, {id : '2' , text : '222' , isComplete : false }, {id : '3' , text : '333' , isComplete : true } ] } }, mounted ( ) { console .log(this .$parent.foo); }, methods: { test ( ) { console .log(this .filteredTodos); } }, template: ` <ul > <li v-for ="todo in filteredTodos" v-bind:key="todo.id" > <slot name ="todo" v-bind:todo ="todo" > {{ todo.text }} </slot > </li > </ul > ` }) new Vue({ el: '.app' , data: { foo: 1 }, mounted ( ) { this .$refs.todolist.test() } }) </script >
当 ref 和 v-for 一起使用的时候,得到的 ref 将会是一个包含了对应数据源的这些子组件的数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <div class ="app" > <div > <todo-list ref ="todolist" v-for ="item in list" :key ="item.id" > <template v-slot:todo ="{todo}" > <span v-if ="todo.isComplete" > ✓</span > {{todo.text}} </template > </todo-list > </div > </div > <script > Vue.component('todo-list' , { data ( ) { return { filteredTodos: [ {id : '1' , text : '111' , isComplete : true }, {id : '2' , text : '222' , isComplete : false }, {id : '3' , text : '333' , isComplete : true } ] } }, mounted ( ) { console .log(this .$parent.foo); }, methods: { test ( ) { console .log(this .filteredTodos); } }, template: ` <ul > <li v-for ="todo in filteredTodos" v-bind:key="todo.id" > <slot name ="todo" v-bind:todo ="todo" > {{ todo.text }} </slot > </li > </ul > ` }) new Vue({ el: '.app' , data: { foo: 1, list: [ {id: 1}, {id: 2} ] }, mounted ( ) { console .log(this .$refs.todolist) } }) </script >
$refs 只会在组件渲染完成 之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的逃生舱——应该避免在模板或计算属性中访问 $refs
依赖注入 使用两个新的实例选项:provide 和 inject,provide 选项允许提供给任意后代组件数据和方法 ,在后代组件中使用 inject 选项来接收传给后代组件的数据和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 Vue.component('todo-list' , { data ( ) { return { filteredTodos: [ {id : '1' , text : '111' , isComplete : true }, {id : '2' , text : '222' , isComplete : false }, {id : '3' , text : '333' , isComplete : true } ] } }, inject: ['one' , 'foo' ], mounted ( ) { this .one(); console .log(this .foo); } template: ` <ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id" > <slot name="todo" v-bind:todo="todo"> <!-- 后备内容 --> {{ todo.text }} </slot> </li> </ul> ` }) new Vue({ el: '.app' , data: { foo: 1 }, provide ( ) { return { one: this .one, foo: this .foo } }, methods: { one ( ) { console .log('one' + this .foo); } } })
实际上,可以把依赖注入看作一部分大范围有效的 prop,除了祖先组件不需要哪些后代组件使用了它提供的 property,后代组件不需要知道被注入的 property 来自哪里
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root 做这件事都是不够好的
程序化的事件侦听器 通过 $on(eventName, eventHandler) 侦听一个事件 通过 $once(eventName, eventHandler) 一次性侦听一个事件 通过 $off(eventName, eventHandler) 停止侦听一个事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 mounted: function ( ) { this .picker = new Pikaday({ field: this .$refs.input, format: 'YYYY-MM-DD' }) }, beforeDestroy: function ( ) { this .picker.destroy() }
这里存在了两个问题
它需要在这个组件实例中保存这个 picker,如果可以的话最好只有 生命周期钩子可以访问到它,这并不算严重的问题,但是它可以被视为杂物 建立代码独立 于我们的清理代码,这使得我们比较难于难于程序化地清理我们建立的所有东西 1 2 3 4 5 6 7 8 9 10 11 mounted: function ( ) { var picker = new Pikaday({ field: this .$refs.input, format: 'YYYY-MM-DD' }) this .$once('hook:beforeDestroy' , function ( ) { picker.destroy() }) }
使用这个逻辑,甚至可以让多个输入框同时使用不同的 pikaday,每个实例都程序化的在后期清理它们
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mounted: function ( ) { this .attachDatepicker('startDateInput' ) this .attachDatepicker('endDateInput' ) }, methods: { attachDatepicker: function (refName ) { var picker = new Pikaday({ field: this .$refs[refName], format: 'YYYY-MM-DD' }) this .$once('hook:beforeDestroy' , function ( ) { picker.destroy() }) } }
注意 vue 的事件系统不同于浏览器的 EventTarget API,尽管它们工作起来是相似的,但是 $emit、$on, 和 $off 并不是 dispatchEvent、addEventListener 和 removeEventListener 的别名
循环引用 递归组件 组件是可以在它们自己的模板中调用自身的,不过它们只能通过 name 选项来做这件事。当全局注册一个组件时 ,这个全局的 ID 会自动设置为该组件的 name 选项
1 2 3 4 5 Vue.component('unique-name-of-my-component' , { })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <div class ="app" > <list :list ="list" > </list > </div > <script > Vue.component('list' , { props: ['list' ], template: ` <ul > <li v-for ="item in list" :key ="item.id" > <div > {{item.content }} </div > <list :list ="item.children" v-if ="item.children !== null" > </list > </li > </ul > ` }) new Vue({ el: '.app' , data: { list: [ {id : '1' , content : 'list1' , children : [{id : '1-1' , content : 'list1-1' }, {id : '1-2' , content : 'list1-2' }]}, {id : '2' , content : 'list2' }, {id : '3' , content : 'list3' } ] } }) </script >
组件之间的循环引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <p > <span > {{ folder.name }}</span > <tree-folder-contents :children ="folder.children" /> </p > <ul > <li v-for ="child in children" > <tree-folder v-if ="child.children" :folder ="child" /> <span v-else > {{ child.name }}</span > </li > </ul > <script > beforeCreate: function ( ) { this .$options.components.TreeFolderContents = require ('./tree-folder-contents.vue' ).default } components: { TreeFolderContents: () => import ('./tree-folder-contents.vue' ) } </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <div class ="app" > <tree-folder :folder ="folder" > </tree-folder > </div > <script > Vue.component('tree-folder' , { props: ['folder' ], template: ` <p > <span > {{folder.name }} </span > <tree-folder-contents :children ="folder.children" /> </p > ` }) Vue.component('tree-folder-contents' , { props: ['children' ], template: ` <ul > <li v-for ="child in children" > <tree-folder v-if ="child.children" :folder ="child" /> <span v-else > {{child.name }} </span > </li > </ul > ` }) new Vue({ el: '.app' , data: { folder: {name : '我的电脑' , children : [ {name : '用户' , id : 'user' , children : [{name : '默认' , id : 'defalut' }, {name : '公用' , id : 'public' }]}, {name : 'System32' , id : 'system32' }, {name : 'System64' , id : 'system64' } ]} } }) </script >
模板定义的替代品 内联模板 当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板 ,而不是将其作为被分发的内容 ,这使得模板的撰写工作更加灵活
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <my-component inline-template > <div > <p > These are compiled as the component's own template.</p > <p > Not parent's transclusion content.</p > </div > </my-component > <script > Vue.component('my-component' , { template: '<span > 111</span > ' }) </script >
inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板
X-Template另一个定义模板的方式是在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去,但这些模板一般用于极小的应用,其他情况下避免使用,这会将模板和组件的其他定义分开来
1 2 3 4 5 6 7 8 9 10 11 <hello-world > </hello-world > <script type ="text/x-template" id ="hello-world-template" > <p > Hello hello hello</p > </script > <script > Vue.component('hello-world' , { template: '#hello-world-template' }) </script >
控制更新 强制更新 通过 $forceUpdate 来强制更新,对于数组和对象的变更检测,视图是无法及时更新的,需要强制更新( $forceUpdate )
通过 v-once 创建低开销的静态组件 渲染普通的 HTML 元素在 vue 中是非常快速的,但有的时候可能有一个组件,这个组件包含了大量静态内容,在这种情况下,可以在根元素上添加 v-once 属性以确保这些内容只计算一次然后缓存起来
1 2 3 4 5 6 7 8 Vue.component('terms-of-service' , { template: ` <div v-once> <h1>Terms of Service</h1> ... a lot of static content ... </div> ` })
进入 / 离开 & 列表过渡 vue 在插入、更新或移除 dom 时,提供多种不同方式的应用过渡效果
在 css 过渡和动画中自动应用 class 可以配合使用第三方 css 动画库,如 animate.css 在过渡钩子函数中使用 javascript 直接操作 dom 可以配合使用第三方 javascript 动画库,如 velocity.js 单元素 / 组件的过渡 vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素或组件添加进入 / 离开过渡
条件渲染(v-if 或 v-show) 动态组件(<component :is="xx"></component>) 组件根节点 当插入或删除包含在 transition 组件中的元素时,vue 将会做出以下处理
自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行 过渡的类名 在进入 / 离开的过渡中,会有 6 个 class 切换
v-enter:定义进入过渡的开始状态,在元素被插入之前生效,在元素被插入之后的下一帧移除v-enter-active:定义进入过渡生效时的状态,在整个进入过渡阶段中应用,在元素被插入之前生效,在过渡 / 动画完成之后移除,这个类被用来定义进入过渡的过程时间,延迟和曲线函数 v-enter-to:定义进入过渡的结束状态,在元素被插入之后下一帧生效(与此同时 v-enter 被移除),在过渡 / 动画完成之后移除v-leave:定义离开过渡的开始状态,在离开过渡被触发时立刻生效,下一帧被移除v-leave-active:定义离开过渡生效时的状态,在整个离开过渡阶段中应用,在离开过渡被触发时立刻生效,在过渡 / 动画完成之后移除,这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数 v-leave-to:定义离开过渡的结束状态,在离开过渡被触发之后下一帧生效(与此同时 v-leave 被删除),在过渡 / 动画完成之后移除对于这些在过渡中切换的类名来说,如果使用了一个没有名字的 <transition>,则 v- 是这些类名的默认前缀,如果使用了 name 属性 <transition name="my-transition">,则 v-enter 会替换为 my-transition。其中 v-enter-active 和 v-leave-active 可以控制进入 / 离开过渡的不同的缓和曲线
CSS 过渡1 2 3 4 5 6 7 8 <div id ="example-1" > <button @click ="show = !show" > Toggle render </button > <transition name ="slide-fade" > <p v-if ="show" > hello</p > </transition > </div >
1 2 3 4 5 6 new Vue({ el: '#example-1' , data: { show: true } })
1 2 3 4 5 6 7 8 9 10 11 12 13 .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 { transform : translateX (10px ); opacity : 0 ; }
CSS 动画css 动画用法同 css 过渡,区别是在动画中 v-enter 类名在节点插入 dom 后不会立即删除,而是在 animationend 事件触发时删除
自定义过渡的类名 可以通过以下的属性来自定义过渡类名enter-classenter-active-classenter-to-classleave-classleave-active-classleave-to-class 它们的优先级高于普通的类名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <link href ="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel ="stylesheet" type ="text/css" > <div id ="app" > <button @click ="show = !show" > 切换</button > <transition name="custom-classes-transition" enter-active-class="animated tada" leave-active-class="animated bounceOutRight" > <p v-if ="show" > hello</p > </transition > </div > <script > new Vue({ el: '#app' , data: { show: true } }) </script >
同时使用过渡和动画 vue 为了知道过渡的完成,必须设置相应的事件监听器,它可以是 transitionend 或 animationend,这取决于给元素应用的 css 规则,如果使用其中任何一种,vue 能自动识别类型并设置监听,但是,假设需要给同一个元素设置两种过渡动效,如果 animation 很快的被触发完成了,而 transition 效果还没结束,在这种情况下,需要使用 type 属性并设置 animation 或 transition 来明确声明需要 vue 监听的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <link href ="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel ="stylesheet" type ="text/css" > <div id ="app" > <button @click ="show = !show" > 切换</button > <transition name="fade" enter-active-class="animated tada duration-1s" leave-active-class="animated bounceOutRight duration-1s" type="transition" > <p v-if ="show" > hello</p > </transition > </div > <script > new Vue({ el: '#app' , data: { show: true } }) </script >
显性的过渡持续时间 在很多情况下,vue 可以自动得出过渡效果的完成时机。默认情况下,vue 会等待其在过渡效果的根元素的第一个 transitionend 或 animationend 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果,在这种情况下可以用 <transition> 组件上的 duration prop定制一个显性的过渡持续时间
1 2 3 4 5 <transition :duration ="1000" > ...</transition > <transition :duration ="{ enter: 500, leave: 800 }" > ...</transition >
javascript 钩子1 2 3 4 5 6 7 8 9 10 11 12 13 <transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:after-enter="afterEnter" v-on:enter-cancelled="enterCancelled" v-on:before-leave="beforeLeave" v-on:leave="leave" v-on:after-leave="afterLeave" v-on:leave-cancelled="leaveCancelled" > </transition >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 methods: { beforeEnter: function (el ) { }, enter: function (el, done ) { done() }, afterEnter: function (el ) { }, enterCancelled: function (el ) { }, beforeLeave: function (el ) { }, leave: function (el, done ) { done() }, afterLeave: function (el ) { }, leaveCancelled: function (el ) { } }
可以在属性中声明 javascript 钩子before-enter:进入过渡运行前enter:进入过渡运行时after-enter:进入过渡运行后enter-cancelled:进入过渡被打断时before-leave:离开过渡运行前leave:离开过渡运行时after-leave:离开过渡运行后leave-cancelled:离开过渡被打断时 这些钩子函数可以结合 transitions / animations 使用,也可以单独使用 当只有 javascript 过渡的时候,在 enter 和 leave 中必须使用 done 进行回调,否则,它们将被同步调用,过渡会立即完成 推荐对于使用 javascript 过渡的元素添加 v-bind:css="false",vue 会跳过 css 的检测,也可以避免过渡过程中 css 的影响 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <div id ="app" > <button @click ="show = !show" > 切换 </button > <transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:leave="leave" v-bind:css="false" > <p v-if ="show" > Hello </p > </transition > </div > <script src ="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js" > </script > <script > new Vue({ el: '#example-4' , data: { show: false }, methods: { beforeEnter: function (el ) { el.style.opacity = 0 el.style.transformOrigin = 'left' }, enter: function (el, done ) { Velocity(el, { opacity : 1 , fontSize : '1.4em' }, { duration : 300 }) Velocity(el, { fontSize : '1em' }, { complete : done }) }, leave: function (el, done ) { Velocity(el, { translateX : '15px' , rotateZ : '50deg' }, { duration : 600 }) Velocity(el, { rotateZ : '100deg' }, { loop : 2 }) Velocity(el, { rotateZ: '45deg' , translateY: '30px' , translateX: '30px' , opacity: 0 }, { complete: done }) } } }) </script >
初始渲染的过渡 通过 appear 属性来设置节点在初始渲染的过渡
1 2 3 <transition appear > </transition >
自定义 css 类名
1 2 3 4 5 6 7 8 <transition appear appear-class="custom-appear-class" appear-to-class="custom-appear-to-class" appear-active-class="custom-appear-active-class" > </transition >
自定义 javascript 钩子
1 2 3 4 5 6 7 8 9 <transition appear v-on:before-appear="customBeforeAppearHook" v-on:appear="customAppearHook" v-on:after-appear="customAfterAppearHook" v-on:appear-cancelled="customAppearCancelledHook" > </transition >
多个元素的过渡 一个 transition 标签中含有多个过渡元素,但是当有多个相同标签名 的元素切换时,需要通过 key 属性设置唯一的值来标记以让 vue 区分它们,否则 vue 为了效率只会替换相同标签内部的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <transition > <button v-if ="isEditing" key ="save" > Save </button > <button v-else key ="edit" > Edit </button > </transition > <transition > <button v-bind:key ="isEditing" > {{ isEditing ? 'Save' : 'Edit' }} </button > </transition >
过渡模式 点击下面的按钮,在 one 和 two 按钮的过渡中,两个按钮都被重绘了,一个离开过渡的时候另一个开始进入过渡,这是 <transition>的默认行为 。接下来点击切换定位为绝对定位在彼此的上面运行正常,然后滑动进度条运动变为滑动过渡
同时生效的进入和离开的过渡不能满足所有要求,所以 vue 提供了过渡模式
in-out:新元素先过渡,完成之后当前元素过渡离开out-in:当前元素先进行过渡,完成之后新元素过渡进入(点击切换模式查看效果)多个组件的过渡 多个组件的过渡需要使用动态组件
1 2 3 <transition fade mode ="out-in" > <component v-bind:is ="view" > </component > </transition >
1 2 3 4 5 6 .fade-enter-active , .fade-leave-active { transition : opacity .3s ease; } .fade-enter , .fade-leave-to { opacity : 0 ; }
列表过渡 对于列表过渡,我们采用 transition-group 组件,该组件的特点如下:
不同于 transition,它会以一个真实元素呈现:默认为一个 span,可以通过 tag 属性更换为其他元素 过渡模式不可用,因为不需要相互切换特有的元素 内部元素总是需要提供唯一的 key css 过渡的类将会应用在内部的元素中,而不是这个组 / 容器本身列表的进入 / 离开过渡 上面的过渡并不平滑,当添加或移除元素时 ,周围的元素会瞬间移动到它们的新布局的位置
列表的排序过渡 transition-group 组件还有一个特殊之处,不仅可以进入和离开动画,还可以改变定位 ,要使用这个功能需要 v-move 这个类,它会在元素的改变定位的过程中应用,像之前的类名一样,可以通过 name 属性来自定义前缀,也可以通过 move-class 属性手动设置,v-move 对于设置过渡的切换时机和过渡曲线非常有用
1 2 3 4 5 6 7 8 9 <div id ="app" class ="demo" > <button v-on:click ="shuffle" > Shuffle</button > <transition-group name ="flip-list" tag ="ul" > <li v-for ="item in items" v-bind:key ="item" > {{ item }} </li > </transition-group > </div > <script src ="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js" > </script >
1 2 3 4 5 6 7 8 9 10 11 12 new Vue({ el: '#app' , data: { items: [1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ] }, methods: { shuffle: function ( ) { this .items = _.shuffle(this .items) } } })
1 2 3 .flip-list-move { transition : transform 1s ; }
使用 v-move时,vue 内部使用了一个 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡到新的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div id ="app" class ="demo" > <button v-on:click ="shuffle" > Shuffle</button > <button v-on:click ="add" > Add</button > <button v-on:click ="remove" > Remove</button > <transition-group name ="list-complete" tag ="p" > <span v-for="item in items" v-bind:key="item" class="list-complete-item" > {{ item }} </span > </transition-group > </div > <script src ="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js" > </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 new Vue({ el: '#app' , data: { items: [1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ], nextNum: 10 }, 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 ) }, shuffle: function ( ) { this .items = _.shuffle(this .items) } } })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .list-complete-item { transition : all 1s ; display : inline-block; margin-right : 10px ; } .list-complete-enter , .list-complete-leave-to { opacity : 0 ; transform : translateY (30px ); } .list-complete-leave-active { position : absolute; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .list-complete-item { display : inline-block; margin-right : 10px ; } .list-complete-enter , .list-complete-leave-to { opacity : 0 ; transform : translateY (30px ); } .list-complete-enter-active ,.list-complete-leave-active { transition : all 1s ; } .list-complete-move { transition : all 1s ; } .list-complete-leave-active { position : absolute; }
需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline 。作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中
flip 动画不仅可以实现单列过渡,多维网格也可以过渡
列表的交错过渡 传统的交错过渡需要使用到大量的 css,vue 提供了钩子函数配合 setTimeout来实现交错过渡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 .list-enter-active , .list-leave-active { transition : all 1s ; } .list-enter , .list-leave-to { opacity : 0 ; transform : translateY (100% ); } .list-css-enter-active , .list-css-leave-active { transition : all 1s ; } .list-css-enter , .list-css-leave-to { opacity : 0 ; transform : translateY (100% ); } .list-css-enter-active :nth-child (5 n+2 ){ transition-delay : .3s ; } .list-css-enter-active :nth-child (5 n+3 ){ transition-delay : .6s ; } .list-css-enter-active :nth-child (5 n+4 ){ transition-delay : .9s ; } .list-css-enter-active :nth-child (5 n+5 ){ transition-delay : 1.2s ; } .list-css-leave-active :nth-child (5 n+1 ){ transition-delay : 1.2s ; } .list-css-leave-active :nth-child (5 n+2 ){ transition-delay : .9s ; } .list-css-leave-active :nth-child (5 n+3 ){ transition-delay : .6s ; } .list-css-leave-active :nth-child (5 n+4 ){ transition-delay : .3s ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 Vue.component('tab-c' , { props: ['num' ], template: ` <transition-group :css="false" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" > <item v-for="item in num" :key="item" :data-enter="item * 300" :data-leave="(num - item) * 300"></item> </transition-group> ` , methods: { beforeEnter (el ) { el.classList.add('list-enter' , 'list-enter-active' ); }, enter (el, done ) { let delay = el.dataset.enter; setTimeout (() => { el.classList.remove('list-enter' ); el.classList.add('list-enter-to' ); el.addEventListener('transitionend' , function onEnd ( ) { el.removeEventListener('transitionend' , onEnd); done(); }) }, delay) }, afterEnter (el ) { el.classList.remove('list-enter-to' , 'list-enter-active' ); }, beforeLeave (el ) { el.classList.add('list-leave' , 'list-leave-active' ); }, leave (el, done ) { let delay = el.dataset.leave; setTimeout (() => { el.classList.remove('list-leave' ); el.classList.add('list-leave-to' ); el.addEventListener('transitionend' , function onEnd ( ) { el.removeEventListener('transitionend' , onEnd); done(); }) }, delay) }, afterLeave (el ) { el.classList.remove('list-leave-active' , 'list-leave-to' ); } } })
可复用的过渡 过渡可以通过 vue 的组件系统来实现复用,创建一个可复用的组件,需要将 transition 或 transition-group 作为根组件 ,然后将任何子组件放置在其中就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ) { } } })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Vue.component('my-special-transition' , { functional: true , render: function (createElement, context ) { var data = { props: { name: 'very-special-transition' , mode: 'out-in' }, on: { beforeEnter: function (el ) { }, afterEnter: function (el ) { } } } return createElement('transition' , data, context.children) } })
动态过渡 1 2 3 4 <transition v-bind:name ="transitionName" > </transition >
状态过渡 状态动画和侦听器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <script src ="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js" > </script > <div id ="animated-number-demo" > <input v-model.number ="number" type ="number" step ="20" > <p > {{ animatedNumber }}</p > </div > <script > new Vue({ el: '#animated-number-demo' , data: { number: 0, tweenedNumber: 0 }, computed: { animatedNumber: function ( ) { return this .tweenedNumber.toFixed(0 ); } }, watch: { number: function (newValue ) { gsap.to(this .$data, { duration : 0.5 , tweenedNumber : newValue }); } } }) </script >
把过渡放在组件里 1 2 3 4 5 6 7 8 9 10 11 12 <script src ="https://cdn.jsdelivr.net/npm/tween.js@16.3.4" > </script > <div id ="example-8" > <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 >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 Vue.component('animated-integer' , { template: '<span>{{ tweeningValue }}</span>' , props: { value: { type: Number , required: true } }, data: function ( ) { return { tweeningValue: 0 } }, watch: { value: function (newValue, oldValue ) { this .tween(oldValue, newValue) } }, mounted: function ( ) { this .tween(0 , this .value) }, 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() } } }) new Vue({ el: '#example-8' , data: { firstNumber: 20 , secondNumber: 40 }, computed: { result: function ( ) { return this .firstNumber + this .secondNumber } } })