目录
1.组件之处理边界情况
1.1 子组件访问根组件数据
1.2 子组件访问父组件数据
1.3 父组件访问子组件
1.4 依赖注入
1.5 程序化的事件侦听器
1.6 递归组件
1.7 内联模板
1.8 X-Template
1.9 强制更新
1.10 v-once
2.过渡效果与状态
2.1 过渡效果
2.1.1 单元素/组件的过渡
2.1.1.1 过渡的CSS类名
2.1.1.2 CSS过渡
2.1.1.3 CSS动画
2.1.1.4 自定义过渡类名
2.1.1.5 同时使用过渡和动画
2.1.1.6 显性的过渡时间
2.1.1.7 JavaScript钩子
2.1.2 初始渲染的过渡
2.1.3 多个元素的过渡
2.1.3.1 过渡模式
2.1.4 多个组件的过渡
2.1.5 列表过渡
2.1.5.1 列表的进入/离开过渡
2.1.5.2 列表的排序过渡
2.1.5.3 列表的交错过渡
2.1.6 可复用的过渡
2.1.7 动态过渡
2.2 过渡状态
2.2.1 状态动画与侦听器
2.2.2 动态状态过渡
2.2.3 把过渡放到组件里
3.可复用性与组合
3.1 混入
3.1.1 基础
3.1.2 选项合并
3.1.2.1 数据对象合并
3.1.2.2 同名钩子函数合并
3.1.2.3 值为对象的选项合并
3.1.3 全局混入
3.1.4 自定义选项合并策略
3.2 自定义指令
3.2.1 简介
3.2.2 钩子函数
3.2.2.1 钩子函数参数
3.2.2.2 动态指令参数
3.2.3 函数简写
3.2.4 对象字面量
3.3 渲染函数与JSX
3.3.1 基础
3.3.2 节点、树以及虚拟DOM
3.3.3 createElement参数
3.3.3.1 深入数据对象
3.3.3.2 约束
3.3.4 使用JavaScript代替模板功能
3.3.4.1 v-if 和v-for
3.3.4.2 v-model
3.3.4.3 事件与按键修饰符
3.3.4.4 插槽
3.3.5 JSX
3.3.6 函数式组件
3.3.6.1 向子元素或子组件传递 attribute 和事件
3.3.6.2 slots() 和 children 对比
在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问
代码实例:
在根组件main.js中定义data数据,methods及computed属性
页面子组件中调用根组件数据实现
子组件访问根组件数据
注意:对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。
使用 $parent property 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。
var map = this.$parent.map || this.$parent.$parent.map
在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。
此时我们建议使用依赖注入的方式
尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref
这个 attribute 为子组件赋予一个 ID 引用
父组件访问子组件
$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
当我们从子组件访问父组件,其中一层的父组件发生了改变的时候,使用 $parent property 无法很好的扩展到更深层级的嵌套组件上,这个时候就要用到依赖注入,它用到了两个新的实例选项:provide 和 inject
provide 选项允许我们指定我们想要提供给后代组件的数据/方法,该选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。
inject 选项来接收指定的我们想要添加在这个实例上的 property,该选项应该是:
一个字符串数组,或
一个对象,对象的 key 是本地的绑定名,value 是:
在可用的注入内容中搜索用的 key (字符串或 Symbol),或
一个对象,该对象的:
from property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)
default property 是降级情况下使用的 value
provide 和 inject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中
实例代码:
组件的依赖注入
在前面的学习中我们知道$emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法:
当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。
$on、$once、$off实例代码:
$on侦听事件
this.$on('add', eventHandler) 定义一个监听的(add)事件并指定他的执行对象/执行函数(eventHandler)
this.$emit('add', 'my params')消费add事件
使用on和emit后,我们可以把事件的定义和消费实现逻辑的解耦,可以在父组件监听($on)子组件中直接调用事件($emit)
要强调的一点是:$on和$emit事件必须是在一个公共的实例上才能触发!!!
组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:
name: 'unique-name-of-my-component'
当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。
Vue.component('unique-name-of-my-component', {
// ...
})
稍有不慎,递归组件就可能导致无限循环:
name: 'stack-overflow',
template: ''
类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。
递归组件基础实例代码:
递归组件基础使用
当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。
内联模板实例代码
内联模板
hello world
内联模板会把子组件中的内容替换为内联模板的内容,比如上述实例的最终渲染结果为:
X-Template是在 JavaScript 标签里使用 text/x-template 类型,并且指定一个id来定义模板的一种方式,例如:
Vue.component('hello-world', {
template: '#hello-world-template'
})
x-template 需要定义在 Vue 所属的 DOM 元素外。
这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。
在vue的开发过程中,数据的绑定通常来说都不用我们操心,例如在data中有一个msg的变量,只要修改它,那么在页面上,msg的内容就会自动发生变化。但是如果对于一个复杂的对象,例如一个对象数组,直接去给数组上某一个元素增加属性,或者直接把数组的length变成0,vue可能就无法知道发生了改变。这个其实就是考验对于双向绑定的更进一步的理解应用了。
在Vue中,双向绑定属于自动档,然而在特定的情况下,需要手动触发“刷新”操作,目前有四种方案可以选择:
使用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()
只渲染元素和组件一次,随后的渲染,使用了此指令的元素/组件及其所有的子节点,都会当作静态内容并跳过,这可以用于优化更新性能。
常见用法实例代码
v-once
{{msg}}
当msg值改变时,input文本框中的内容不会变化,而p元素的值会随之变化
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
元素封装成过渡组件之后,在遇到插入或删除时,Vue 将
在进入/离开的过渡中,会有 6 个 class 切换。
对于这些在过渡中切换的类名来说,如果你使用一个没有名字的
v-enter-active 和 v-leave-active 可以控制进入/离开过渡的不同的缓和曲线
实例代码
CSS过渡
Hello World
CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter 类名在节点插入 DOM 后不会立即删除,而是在 animationend 事件触发时删除
实例代码
CSS动画
Hello World
我们可以通过以下属性来自定义过渡类名:
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。
实例代码:
自定义过渡的类名
Hello World
animate的引用说明:
1、npm i animate.css--save 安装
2、main.js中全局引入
import animate from 'animate.css'
Vue.use(animate)
3、animate css样式引入,注意需要添加前缀animate__
Hello World
Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 transitionend 或 animationend,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。
但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type attribute 并设置 animation 或 transition 来明确声明你需要 Vue 监听的类型。
实例代码:
同时使用过渡和动画
Hello World
在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend 或 animationend 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。
在这种情况下你可以用
...
你也可以定制进入和移出的持续时间:
...
可以在属性中声明 JavaScript 钩子,包含
实例代码:
JavaScript钩子
Hello World
关于Velocity插件
Velocity是一个简单易用,高性能且功能丰富的轻量级JavaScript动画库,它拥有颜色动画,转换动画(transforms)、循环、缓动、SVG动画、和滚动动画等特色功能
支持Chaining链式动画,当一个元素连续应用多个velocity()时,动画以队列的方式执行
1、执行npm install velocity-animate命令,进行安装
2、页面引入 import Velocity from 'velocity-animate';
通过 appear 属性设置节点在初始渲染的过渡
appear与进入/离开过渡一样,可以自定义过渡CSS列名,可以设置JavaScript钩子
自定义过渡类名的属性:
JavaScript钩子:
appear-cancelled的触发条件就是before-appear/appear两个钩子函数中有不显示该节点的操作v-if/ v-show都会触发appear-cancelled钩子函数。实际场景不多,一般在动画执行过程中被用户取消后,可以执行一些操作。
实例代码:
初始渲染的过渡
Hello World
我们平时在多个不同元素上会使用v-if/v-show来控制元素是否显示,这也是控制多个元素过渡的一种方式,但是这样使用时,需要注意:
当有相同标签名的元素切换时,需要通过 key
attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在
实例代码:
多个元素的过渡
在上面的实例中,我们会发现,当我们改变buttonState值的时候,会发现两个按钮每次都会刷新,一个离开过渡的时候另一个开始进入过渡,这是
的默认行为 - 进入和离开同时发生。
同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了过渡模式:
我们在transition中设置mode属性为out-in,就可已解决这个问题
多个组件的过渡我们不需要使用key属性,只需要使用动态组件
实例代码:
多个组件的过渡
列表过渡我们需要用到
实例代码:
列表的进入/离开过渡
{{item}}
在列表的进入/离开实例中,代码运行时,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,这时我们需要用到排序过渡
要实现排序过渡,我们这里需要使用lodash插件,
1、执行 npm i lodash --save 命令安装插件
2、页面中引入lodash, import Lodash from 'lodash';
实例代码:
列表的排序过渡
{{item}}
排序过渡的实现,是Vue 使用了一个叫 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡新的位置
需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline
。作为替代方案,可以设置为 display: inline-block
或者放置于 flex 中
FLIP 动画不仅可以实现单列过渡,多维网格也同样可以过渡
通过 data 属性 与 JavaScript 通信,就可以实现列表的交错过渡
实例代码:
列表的交错过渡
{{ item.msg }}
要创建一个可复用过渡组件,你需要做的就是将
或者
作为根组件,然后将任何子组件放置在其中就可以了。
简单组件代码示例:
Vue.component('my-special-transition', {
template: '\
\
\
\
',
methods: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
})
动态过渡最基本的例子是通过 name
属性 来绑定动态值。
当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用。
所有过渡属性都可以动态绑定,但我们不仅仅只有属性可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现。
实例代码:
动态过渡
Fade In:
Fade Out:
hello
Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如:
这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。
通过侦听,我们能监听到任何数值属性的数值更新,首先我们先看一下通过监听数字文本框中数值变化,从而实现数字动态变化的实例:
首先执行npm i [email protected] 命令安装gsap插件(动画库与图片预览插件)
实例代码:
状态动画与侦听器
{{ animatedNumber }}
对于不能直接像数字一样存储的值,比如 CSS 中的 color 的值,使用tween和color-js插件实现
1)执行命令npm i tween 安装tween插件
2)执行命令 npm i color-js 安装color插件
实例代码:
状态动画与侦听器
Preview:
{{ tweenedCSSColor }}
就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。这里我们使用TweenLite实现一个缓动作的动态效果:
1)执行npm i gsap 安装插件
注:TweenLite最初是GreenSock公司推出的一款基于ActionScript的免费开源的缓动引擎,是webgame开发人员比较常用的一个动画库,使用TweenLite能够简化动画制作的代码编写工作。TweenLite属于gsap插件中的一个模块,详情参见官网GreenSock
实例代码:
动态状态过渡
管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。我们以数字文本输入改变动画为例,写一个通用组件
实例代码:
把过渡放到组件里
+
=
{{ result }}
+
=
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
那也就是说,你可以单独写个逻辑文件,默认导出一个对象,对象里面可以包含data、created、mounted、methods
等vue模板文件中的逻辑对象。接着可以将这个对象引入到多个vue模板中进行功能复用,从而达到功能模块的逻辑封装,便于使用及后期维护。
首先我们在项目src目录下创建mixins文件夹,用于存放混入组件的文件
创建basemixins.js文件,来实现一个基础混入,代码:
/**基础混入的使用 */
export default{
methods: {
hello: function () {
console.log('hello from mixin!')
}
},
created() {
this.hello();
}
}
vue页面中引入 basemixins.js,代码:
基础混入
运行项目,查看控制台的输出:
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
创建seelctmixin.js,代码:
export default{
data(){
return{
message:"hello",
foo:"abc"
}
}
}
vue页面中引入 seelctmixin.js,代码:
混入——选项合并之数据对象
{{message}}
{{foo}}
{{bar}}
渲染效果:
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用
seelctmixin.js代码:
export default{
created: function () {
console.log('混入对象的钩子被调用')
}
}
vue页面代码:
混入——同名钩子函数合并
运行项目,查看控制台的输出:
值为对象的选项,例如 methods
、components
和 directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
seelctmixin.js代码:
export default{
methods: {
foo() {
console.log('foo')
},
conflicting() {
console.log('from mixin')
}
}
}
vue页面代码:
混入——值为对象的选项合并
运行项目,查看控制台的输出:
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 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页面代码:
全局混入
运行项目,查看控制台输出:
请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 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)
}
}
除了核心功能默认内置的指令 (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页面中局部注册 ,代码:
自定义指令——v-focus
vue页面中,input输入框直接使用v-focus指令即可:
自定义指令——v-focus
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
指令钩子函数会被传入以下参数:
除了 el
之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
现在我们使用钩子函数参数性质,来自定义一个钩子:
自定义指令——钩子函数自定义钩子
指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
这里我们通过用来通过固定布局将元素固定在页面上,定义一个指令,可以设定元素固定位置:
自定义指令——动态指令参数
I am pinned onto the page at 200px to the left.
在很多时候,你可能想在 bind
和 update
时触发相同行为,而不关心其它的钩子。比如这样写:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。
实例代码:
自定义指令——对象字面量
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
这里我们通过生成带锚点的标题的组件为示例,一个通过普通方式实现,一个通过render渲染函数实现,深入了解前后不同
普通方式实现:
渲染函数
h1标题
h2标题
使用渲染函数实现:
渲染函数
h1标题
h2标题
h3标题
通过前后代码对比我们会发现,没有使用渲染函数的代码要比使用渲染函数的代码更显的冗长
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
不同的节点,就组成了树。
虚拟DOM:Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。我们可通过createElement创建虚拟DOM,比如:
return createElement('h1', this.blogTitle)
createElement
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription
,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
有一点要注意:正如 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 }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
有了以上关于渲染函数等知识,我们现在写一个完整的示例:
渲染函数——深入数据对象
h1标题
h2标题
h3标题
组件树中的所有 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')
})
)
}
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if
和 v-for
:
- {{ item.name }}
No items found.
这些都可以在渲染函数中用 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.')
}
}
渲染函数中没有与 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)
}
}
})
}
对于 .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()
// ...
}
}
你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render: function (createElement) {
// ` `
return createElement('div', this.$slots.default)
}
也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:
props: ['message'],
render: function (createElement) {
// ` `
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots
字段:
render: function (createElement) {
// `{{ props.text }} `
return createElement('div', [
createElement('child', {
// 在数据对象中传递 `scopedSlots`
// 格式为 { name: props => VNode | Array }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
如果你写了很多 render
函数,可能会觉得下面这样的代码写起来很痛苦:
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
特别是对应的模板如此简单的情况下:
Hello world!
这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
Hello world!
)
}
})
将 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
之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 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 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
组件需要的一切都是通过 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 的值来代为渲染更具体的组件:
函数式组件
smart-list
在普通组件中,没有被定义为 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
的别名) 传递任何事件监听器。
你可能想知道为什么同时需要 slots()
和 children
。slots().default
不是和 children
类似的吗?在一些场景中,是这样——但如果是如下的带有子节点的函数式组件呢?
first
second
对于这个组件,children
会给你两个段落标签,而 slots().default
只会传递第二个匿名段落标签,slots().foo
会传递第一个具名段落标签。同时拥有 children
和 slots()
,因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children
,移交给其它组件去处理。