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组件树构建的整个VNodes树。

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:classv-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-ifv-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/ elsemap在渲染函数中重写:

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
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif (event.target !== event.currentTarget) return
Keys:.enter, .13if (event.keyCode !== 13) return (change 13 to another key code for other key modifiers)
Modifiers Keys:.ctrl, .alt, .shift, .metaif (!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 }) ]) }

要使用渲染函数将有限范围的插槽传递给子组件,请使用scopedSlotsVNode数据中的字段:

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 createElementto 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.defaultcontext.children然后更新this.levelcontext.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()childrenslots().default会不会和children一样?在某些情况下,是的 - 但如果您有以下子功能组件,该怎么办?

<my-functional-component> <p slot="foo"> first </p> <p>second</p> </my-functional-component>

对于这个组件,children会给你两个段落,slots().default只会给你第二个,并且slots().foo只会给你第一个。既有childrenslots()也允许您选择此组件是否了解槽系统,或者可能通过传递将该责任委托给另一个组件children

模板编辑

你可能有兴趣知道Vue的模板实际上编译来渲染函数。这是您通常不需要知道的实现细节,但如果您想了解如何编译特定的模板功能,您可能会发现它很有趣。下面是一个Vue.compile用于实时编译模板字符串的小示例: