首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

16. Render Functions & 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-ifv-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

.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
    })
  ])
}

要使用渲染函数将有限范围的插槽传递给子组件,请使用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用于实时编译模板字符串的小示例:

扫码关注腾讯云开发者

领取腾讯云代金券