内容提要:
- 渲染函数基本用法
- 节点、树和虚拟DOM
createElement
参数详解- 使用普通的JavaScript代替模板特性
- JSX插件介绍
- 函数式组件的用法:传递属性和事件给子元素或子组件,
slots()
vschildren
- 模板编译的demo演示
基础
在大部分情况下我们推荐使用template去构建HTML页面。然而在某些情况下,你需要JavaScript的全部编程能力。这就是你需要使用render function
,一个更接近编译器的模板替代方案。
让我们深入研究一个简单的例子,关于一个render
函数的实践。假设你想要生成锚定标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
对于以上HTML,你决定这样定义组件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
当你开始使用一个组件的时候,它仅仅基于level
prop生成一个头,你可以很快使用以下这个方式达成:
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-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">
</h6>
</script>
Vue.component('anchored-heading',{
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
模板感觉不太好。它不仅啰嗦,而且我们重复使用<slot><slot>
为每一个heading level,并且当我们追加相同的anchor 元素的时候做相同的事情。
虽然模板对大多数组件有用,但很明显这不是其中之一。所以试着让我们以一个render
函数重写它。
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name
this.$slots.default // array of children
)
},
props: {
level: {
type: Number,
required: true
}
}
})
更简单的!稍稍。这个代码更短,但是这要求更熟悉Vue属性。在这个例子中,你必须知道在组件中传递没写携带slot属性的子元素时,比如anchored-heading
中的 Hello world!
,这些子组件被存储在组件实例中的$slots.default
中。如果你还没有了解,建议你在继续深入了解渲染函数之前读instance properties API 。
节点,树,和虚拟DOM
在我们深入渲染函数之前,理解一点浏览器如何工作是重要的。以这个HTML为例:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
当一个浏览器渲染这段代码的时候,它创建了一个tree of “DOM nodes” 去帮助浏览器追踪一切,就好像你创建了一个家谱来记录你的大家庭一样。
DOM的节点数看起来像这样:
每一个元素是一个节点。每一个文本是一个节点。每一个评论也是一个节点!一个节点是一页的一部分。类似于家谱,每一个节点可以有子节点(例如:每一个部分可以包含其他部分)。
高效的更新所有这些节点是困难的,但是还好,你不用手动这么做。相反,你告诉Vue页面需要什么HTML,在一个模板中:
<h1>{{ blogTitle }}</h1>
或一个渲染函数:
render: function (createElement) {
retrun createElement('h1', this.blogTitle)
}
在这些例子中,Vue自动更新页面,甚至当blogTitle
改变的时候。
虚拟DOM
Vue通过创建虚拟DOM来实现对真实DOM改变的追踪。仔细看看这行:
return createElement('h1', this.blogTitle)
createElement实际返回什么?确实没有返回成真实的DOM元素。可能称为createNodeDescription
更准确,因为它包含Vue应该在页面上呈现何种节点的信息,包括对所有子节点的描述,我们称这个节点描述为一个“虚拟节点”,简称VNode。“Virtual DOM” 就是我们所说的VNodes树,通过Vue组件树创建。
createElement
参数
下一件事是你必须熟悉如何在createElement
函数中使用模板特性。下面是createElement
接受的参数:
// @returns {VNode}
createElement(
// {Stirng | Object | Function}
// An HTML tag name, component options, or async
// funciton resolving to one of these. Required.
'div',
// {Object}
// A data object corresponding to the attributes
// you would use in a template. Optional.
{
// (see details in the next seciton below)
},
// {String | Array}
// Children VNodes, built using `createElement()`,
// or using strings to get 'text VNodes'. Optional.
[
'Some text comes first.',
createElement('h1', 'A headline'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
深入数据对象
一件事情需要注意:类似于v-bind:class
和v-bind:style
在模板中被特殊对待,他们在自己的VNode数据对象中有自己的顶层字段。这个对象也允许你绑定正常的HTML属性以及innerHTML
(这将替换v-html
指令)等DOM属性。
// 和‘v-bind:class’ 一样的API,也接受一个字符串,对象,或字符串和对象组成的数组
class: {
foo: true,
bar: false
},
// 与‘v-bind:style’一样的API,也接收一个字符串,对象或对象组成的数组
style:{
color: 'red',
fontSize: '14px'
},
// 正常的Html属性
attrs: {
id: 'foo'
},
// 组件 props
props:{
myProp: 'bar'
},
// DOM 属性
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
}
}
],
// slots作用域格式
// {name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// slot的名字,如果这个组件是另一个组件的子组件
slot: 'name-of-slot',
key: 'myKey',
ref: 'myRef',
// 在渲染函数中如果你使用了多个同名元素。这将是“¥refs.myRef”变成一个数组
refInFor: true
完整用例
用这些知识,我们可以开始完成这些组件:
var getChildrenTextContent = function (children) {
return children.map(function (node){
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
// 创建连字符id
var 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)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
})
约束
组件树中的所有VNodes必须唯一。这将意味着以下渲染函数是无效的:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 呀 - 重复的 VNodes!
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重复相同的元素/组件多次,你可以用一个工厂函数实现这个。例如,以下渲染函数是渲染20个完全相同段落的有效方法:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
用JavaScript替换模板特性
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.')
}
}
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)
}
}
})
}
这就是深入底层的成本,但是相比于v-model
,它给了你更多而交互细节控制。
事件&键修饰符
对于.passive
,.capture
和.once
事件修饰符,Vue提供了前缀能够被用于on
:
Mdoifiers | Prefix |
---|---|
.passive |
& |
.capture |
! |
.once |
~ |
.capture .once or .once.capture |
~! |
例如:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
对于所有的其它事件和键修饰符,前缀是没有必要的,因为你可以在处理函数中使用事件方法:
Modifier(s) | Equivalent in Handler |
---|---|
.stop |
event.stopPropagation() |
.prevent |
event.preventDefault() |
.self |
if (event.target !== event.currentTarget) return |
Keys:.enter , .13 |
if (event.keyCode !== 13) return (change 13 to another key code)for other key modifiers |
Modifiers Keys:.ctrl , .alt ,.shift , .meta |
if (!event.ctrlKey) return (change ctrlKey to altKey , shiftKey , or metaKey , respectively) |
这里有一个使用所有这些修饰符的例子:
on: {
keyup: function (event) {
// 如果发出事件元素的没有事件绑定终止操作
if (event.target !== event.currentTarget) return
// 如果没有按下13键或没有同时按下shift键终止操作
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件传播
event.stopPropagation()
// 阻止该元素的默认keyup事件
event.preventDefault()
// ...
}
}
Slots
你能够通过this.$slots
访问静态的slot内容,作为VNodes的数组:
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
你也能够使用this.$scopedSlots
访问作用域插槽,得到一个返回VNodes的函数:
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
为了使用渲染函数传递scoped slots给一个子组件,在VNode 数据对象里使用scopedSlots
域。
render: function (createElement) {
return createElement('div', [
createElement('child', {
// 传递`scopedSlots` 在data Object
// 格式 { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
JSX
如果你正在写很多的render
函数,你可能会比较头疼像这样写东西:
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
尤其是模板的版本相对比较简单:
<anchored-heading: level="1">
<span>Hello</span> world!
</anchored-heading>
这就是为什么会有一个 Babel plugin插件,在JSX中使用Vue语法,它让我们回到更接近模板的语法上:
import AnchoredHeading form './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
你将看到在Vue的生态系统中将
h
作为createElement
别名是通用的惯例,实际上也是JSX所要求的。如果在作用域中h
失去作用,你的app将抛出一个错误。
对于更多的JSX如何映射JavaScript的用例,查看 usage docs.
函数式组件
之前我们创建的锚点标题组件相当简单。它没有管理和监听传递给它的任何状态,它没有生命周期方法,它仅仅是一个拥有一些props的一个函数。
在这个例子中,我们标记组件为functional
,那意味着他们无状态(没有 reactive data响应式数据)和无实例(没有this
上下文)。一个函数式组件看起来像这样:
Vue.component('my-component', {
functional: true,
// Props是可选的
props: {
// ...
},
// 作为缺少实例的补偿,我们现在提供第二个上下文参数
render: function (createElement, context) {
// ...
}
})
注意:在2.3.0版本之前,如果你希望在函数式组件里接受props,那么
props
操作符是需要的。在2.3.0+你能够忽略props
操作符,在组件上的所有属性都会被当做props隐式取出。
在2.5.0+,如果你使用single-file components,模板基于函数式组件可以这样声明:
<template functional>
</template>
组件的所有东西都需要通过context
传递。一个对象包含:
props
:一个被提供给props的对象children
:一个VNode子节点数组slots
:一个函数返回一个slots对象data
:整个 data object,被传递给组件作为createElement
的第二个参数parent
:一个父组件的引用listeners
: (2.3.0+)一个对象包含父节点注册的时间监听器,这是一个指向data.on的别名injections
:(2.3.0+)如果使用inject
选项,則该选项应该包含被注入的属性。
增加了functional:true
之后,我们的锚点标题组件将要求context
参数更新渲染函数,this.$slots.default
更新为context.children
,然后this.level
更新为context.props.level
.
由于函数式组件仅仅是函数,渲染开销很低。然而,对于持久化实例的缺乏也意味着他们不会出现在 Vue devtools 组件树。
作为封装组件时他们也非常有用,例如,当你需要:
- 程式化的选择要委托的几个组件之一
- 在传递他们给一个子组件之前操作children、props或data
这是一个smart-list
组件的例子,依赖于传入props的值,可以代表更多的具体组件。
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered:Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
传递属性和事件给子元素/子组件
在普通的组件里,没有被定义为props的特性会自动添加到组件的根元素、将现有的同名特性替换或智能合并 。
然而,函数式组件要求你显式的定义这个行为:
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 显式的传递任何属性,事件监听者,子节点,等等。
return createElement('button', context.data, context.children)
}
})
通过传递context.data
作为给createElement
的第二个参数。我们就把my-functional-button
上面的所有属性和事件监听者都传递下去了。它是如此的透明,实际上,事件甚至不要求.native
修饰符。
如果你使用基于模板的函数式组件 ,你也必须手动添加属性和监听器。由于我们可以访问独立的上下文内容,我们能使用data.attrs
传递任何HTML属性,也可以使用listeners
(data.on
的别名)传递任何事件监听器。
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners">
<slot/>
</button>
</template>
slots()
vs children
你可能想知道为什么我们slots()
和children
都需要。slots().default
不是和children
相同吗?在一些情况下,是的 - 如果你用一个具有以下子节点的函数式组件,怎么办呢?
<my-functional-component>
<p slot="foo">
first
</p>
<p>second</p>
</my-functional-component>
对于这个组件,子节点都是paragraphs,slots().default
将只给你第二个参数,slots().foo
将给第一个参数。children
和slots()
都有的情况下,因此允许你去选择是否这个组件知道这个slot系统或通过传递children
将该责任委托给另一个组件。
模板编译
你可能对于知道Vue的模版实际编译为渲染函数感兴趣。这个实现细节你通常不需要知道,但是如果你想看看模板的功能是怎么被编译的,你可能对它感兴趣。下面是一个使用Vue.compile来实时编译模板字符串的简单demo:
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>
渲染:
function anonymous(
) {
with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}
staticRenderFns:
_m(0): function anonymous(
) {
with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}