16. Render Functions & JSX(渲染功能& JSX)
渲染函数和JSX
基础知识
Vue建议在绝大多数情况下使用模板来构建HTML。但是,有些情况下,您确实需要JavaScript的全部程序化功能。这就是您可以使用渲染函数的地方
,它是模板的更接近编译器的替代品。
让我们深入一个简单的例子,其中一个render
函数是实用的。假设你想要生成锚定标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
对于上面的HTML,你决定你需要这个组件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
当你开始使用只生成基于level
道具的标题的组件时,你很快就会到达这个:
<script type="text/x-template" id="anchored-heading-template">
<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>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
该模板感觉不好。它不仅是详细的,而且我们正在复制<slot></slot>每个标题级别,并且在添加锚点元素时也必须执行相同的操作。
虽然模板适用于大多数组件,但很明显,这不是其中之一。所以让我们试着用一个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
属性的子元素传递到组件时,例如Hello world!
中的anchored-heading
,这些子元素将存储在组件实例中$slots.default
。
如果您还没有,建议
在深入渲染函数之前通读实例属性API
。
节点,树和虚拟DOM
在我们深入了解渲染函数之前,了解一些浏览器的工作方式很重要。以这个HTML为例:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
当浏览器读取此代码时,它会构建一个“DOM节点”树,以帮助它跟踪所有内容,就像您可能构建家族树来跟踪您的扩展系列一样。
上述HTML的DOM节点树如下所示:
每个元素都是一个节点。每一段文字都是一个节点。即使评论是节点!节点只是页面的一部分。和家谱一样,每个节点都可以有子节点(即每个节点可以包含其他片断)。
有效地更新所有这些节点可能很困难,但幸好,您不必手动执行。相反,您可以在模板中告诉Vue您想在页面上使用什么HTML:
<h1>{{ blogTitle }}</h1>
或者一个渲染函数:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
在这两种情况下,即使发生blogTitle
更改,Vue也会自动保持页面更新。
虚拟DOM
Vue通过构建虚拟DOM
来完成这一任务,以跟踪它需要对真实DOM做出的更改。仔细看一下这一行:
return createElement('h1', this.blogTitle)
createElement
实际返回的是什么?这不完全是
一个真正的DOM元素。它可以更准确地命名createNodeDescription
,因为它包含向Vue描述它应该在页面上呈现什么样的节点的信息,包括任何子节点的描述。我们称这个节点描述为“虚拟节点”,通常缩写为VNode
。“虚拟DOM”就是我们称之为由Vue组件树构建的整个VNode
s树。
createElement 参数
接下来你必须熟悉的是如何在createElement
函数中使用模板特征。以下是createElement
接受的论据:
// @returns {VNode}
createElement(
// {String | Object | Function}
// An HTML tag name, component options, or function
// returning 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 section 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属性以及DOM属性(如innerHTML
将替换该v-html
指令):
{
// Same API as `v-bind:class`
'class': {
foo: true,
bar: false
},
// Same API as `v-bind:style`
style: {
color: 'red',
fontSize: '14px'
},
// Normal HTML attributes
attrs: {
id: 'foo'
},
// Component props
props: {
myProp: 'bar'
},
// DOM properties
domProps: {
innerHTML: 'baz'
},
// Event handlers are nested under `on`, though
// modifiers such as in `v-on:keyup.enter` are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},
// Custom directives. Note that the binding's
// oldValue cannot be set, as Vue keeps track
// of it for you.
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Scoped slots in the form of
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// The name of the slot, if this component is the
// child of another component
slot: 'name-of-slot',
// Other special top-level properties
key: 'myKey',
ref: 'myRef'
}
完整的例子
有了这些知识,我们现在可以完成我们开始的组件:
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) {
// create kebabCase 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必须是唯一的
组件树中的所有VNodes必须是唯一的。这意味着以下渲染功能无效:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// Yikes - duplicate 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
在渲染函数中重写:
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
在渲染函数中没有直接的对应物 - 你必须自己实现逻辑:
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.value = event.target.value
self.$emit('input', event.target.value)
}
}
})
}
这是降低成本,但它也使您能够更好地控制与v-model
相比的交互细节。
事件和关键修饰符
对于.passive
,.capture
和.once
事件修饰符,Vue公司提供了可与使用前缀on
:
Modifier(s) | 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) {
// Abort if the element emitting the event is not
// the element the event is bound to
if (event.target !== event.currentTarget) return
// Abort if the key that went up is not the enter
// key (13) and the shift key was not held down
// at the same time
if (!event.shiftKey || event.keyCode !== 13) return
// Stop event propagation
event.stopPropagation()
// Prevent the default keyup handler for this element
event.preventDefault()
// ...
}
}
Slots
您可以从this.$slots
以下位置以静态插槽内容作为VNodes阵列访问:
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
并将作用域插槽作为从this.$scopedSlots
以下位置返回VNodes的函数:
render: function (createElement) {
// `<div><slot :text="msg"></slot></div>`
return createElement('div', [
this.$scopedSlots.default{
text: this.msg
})
])
}
要使用渲染函数将有限范围的插槽传递给子组件,请使用scopedSlots
VNode数据中的字段:
render (createElement) {
return createElement('div', [
createElement('child', {
// pass `scopedSlots` in the data object
// in the form of { 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插件在Vue中使用JSX,让我们回到更接近模板的语法:
import AnchoredHeading from './AnchoredHeading.vue'
new Vue{
el: '#demo',
render (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
Aliasing createElement
to h
是Vue生态系统中常见的惯例,实际上对于JSX是必需的。如果h
在范围内不可用,您的应用程序将抛出错误。
有关JSX如何映射到JavaScript的更多信息,请参阅用法文档。
功能组件
我们之前创建的锚定标题组件相对简单。它不管理任何状态,监视传递给它的状态,也没有生命周期方法。真的,这只是一些道具的功能。
在这种情况下,我们可以将组件标记为functional
,这意味着它们是无状态(无data
)和无实例(无this
环境)。一个功能组件
是这样的:
Vue.component('my-component', {
functional: true,
// To compensate for the lack of an instance,
// we are now provided a 2nd context argument.
render: function (createElement, context) {
// ...
},
// Props are optional
props: {
// ...
}
})
注意:在2.3.0之前的版本中,
props
如果您希望接受功能组件中的道具,则需要该选项。在2.3.0以上版本中,您可以省略该props
选项,并且在组件节点上找到的所有属性都将隐式提取为道具。
组件需要的所有内容都通过了context
,这是一个包含以下内容的对象:
props
:提供的道具的一个对象
children
:一组VNode子节点
slots
:返回一个slots
对象的函数
data
:传递给组件的整个数据对象
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组件树中。
它们作为包装组件也非常有用。例如,当你需要:
- 以编程方式选择要委派的其他组件之一
- 在将它们传递给子组件之前,操作子节点,道具或数据
以下是smart-list
根据传递给它的道具,委托给更多特定组件的组件示例:
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
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: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
}
})
slots() VS children
你可能想知道我们为什么需要slots()
和children
。slots().default
会不会和children
一样?在某些情况下,是的 - 但如果您有以下子功能组件,该怎么办?
<my-functional-component>
<p slot="foo">
first
</p>
<p>second</p>
</my-functional-component>
对于这个组件,children
会给你两个段落,slots().default
只会给你第二个,并且slots().foo
只会给你第一个。既有children
,slots()
也允许您选择此组件是否了解槽系统,或者可能通过传递将该责任委托给另一个组件children
。
模板编辑
你可能有兴趣知道Vue的模板实际上编译来渲染函数。这是您通常不需要知道的实现细节,但如果您想了解如何编译特定的模板功能,您可能会发现它很有趣。下面是一个Vue.compile
用于实时编译模板字符串的小示例: