跳转到内容

渲染函数与JSX

Vue推荐在大多数情况下使用模板来构建应用程序。然而,在某些情况下,我们需要JavaScript的全部编程能力。这就是我们可以使用渲染函数的地方。

如果您对虚拟DOM和渲染函数的概念还不熟悉,请确保首先阅读渲染机制章节。

基本用法

创建Vnodes

Vue提供了一个h()函数用于创建Vnodes

js
import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h()超脚本 的简称,意为“生成 HTML(超文本标记语言)的 JavaScript”。这个名称是从许多虚拟 DOM 实现共享的约定中继承而来的。一个更具体的名称可以是 createVNode(),但较短的名称在需要在渲染函数中多次调用此函数时更有帮助。

h() 函数设计得非常灵活

js
// all arguments except the type are optional
h('div')
h('div', { id: 'foo' })

// both attributes and properties can be used in props
// Vue automatically picks the right way to assign it
h('div', { class: 'bar', innerHTML: 'hello' })

// props modifiers such as `.prop` and `.attr` can be added
// with `.` and `^` prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })

// class and style have the same object / array
// value support that they have in templates
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// event listeners should be passed as onXxx
h('div', { onClick: () => {} })

// children can be a string
h('div', { id: 'foo' }, 'hello')

// props can be omitted when there are no props
h('div', 'hello')
h('div', [h('span', 'hello')])

// children array can contain mixed vnodes and strings
h('div', ['hello', h('span', 'hello')])

生成的 vnode 具有以下形状

js
const vnode = h('div', { id: 'foo' }, [])

vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null

注意

完整的 VNode 接口包含许多其他内部属性,但强烈建议只依赖这里列出的属性。这样可以避免在内部属性更改时意外破坏。

声明渲染函数

当使用带有组合 API 的模板时,setup() 钩子的返回值用于将数据暴露给模板。然而,当使用渲染函数时,我们可以直接返回渲染函数

js
import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // return the render function
    return () => h('div', props.msg + count.value)
  }
}

渲染函数在 setup() 中声明,因此它自然可以访问同一作用域中声明的 props 和任何响应式状态。

除了返回单个 vnode,您还可以返回字符串或数组

js
export default {
  setup() {
    return () => 'hello world!'
  }
}
js
import { h } from 'vue'

export default {
  setup() {
    // use an array to return multiple root nodes
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

提示

请确保返回一个函数而不是直接返回值!setup() 函数在每个组件中仅调用一次,而返回的渲染函数将被多次调用。

我们可以使用 render 选项来声明渲染函数

js
import { h } from 'vue'

export default {
  data() {
    return {
      msg: 'hello'
    }
  },
  render() {
    return h('div', this.msg)
  }
}

render() 函数可以通过 this 访问组件实例。

除了返回单个 vnode,您还可以返回字符串或数组

js
export default {
  render() {
    return 'hello world!'
  }
}
js
import { h } from 'vue'

export default {
  render() {
    // use an array to return multiple root nodes
    return [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

如果渲染函数组件不需要任何实例状态,它们也可以直接声明为函数以简化代码

js
function Hello() {
  return 'hello world!'
}

没错,这是一个有效的 Vue 组件!有关此语法的更多详细信息,请参阅 功能组件

VNode 必须唯一

组件树中的所有 vnodes 都必须是唯一的。这意味着以下渲染函数是无效的

js
function render() {
  const p = h('p', 'hi')
  return h('div', [
    // Yikes - duplicate vnodes!
    p,
    p
  ])
}

如果您真的想多次重复相同的元素/组件,可以使用工厂函数来实现。例如,以下渲染函数是渲染 20 个相同段落的完全有效的方法

js
function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

JSX / TSX

JSX 是一种类似于 XML 的 JavaScript 扩展,允许我们编写如下代码

jsx
const vnode = <div>hello</div>

在 JSX 表达式中,使用花括号嵌入动态值

jsx
const vnode = <div id={dynamicId}>hello, {userName}</div>

create-vue 和 Vue CLI 都提供了用于构建具有预配置 JSX 支持的项目选项。如果您正在手动配置 JSX,请参阅 @vue/babel-plugin-jsx 的文档以获取详细信息。

尽管 JSX 首先由 React 提出,但实际上 JSX 没有定义的运行时语义,可以编译成不同的输出。如果您之前使用过 JSX,请注意,Vue JSX 转换与 React 的 JSX 转换不同,因此您不能在 Vue 应用程序中使用 React 的 JSX 转换。与 React JSX 的明显差异包括

  • 您可以使用 HTML 属性(如 classfor)作为 props - 无需使用 classNamehtmlFor
  • 将子元素传递给组件(即插槽)工作方式不同

Vue 的类型定义还提供了对 TSX 使用的类型推断。当使用 TSX 时,请确保在 tsconfig.json 中指定 "jsx": "preserve",以便 TypeScript 保留 JSX 语法以供 Vue JSX 转换处理。

JSX 类型推断

类似于转换,Vue 的 JSX 也需要不同的类型定义。

从 Vue 3.4 开始,Vue 不再隐式注册全局 JSX 命名空间。为了指示 TypeScript 使用 Vue 的 JSX 类型定义,请确保在您的 tsconfig.json 中包含以下内容

json
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "vue"
    // ...
  }
}

您还可以通过在文件顶部添加注释 /* @jsxImportSource vue */ 来为每个文件启用。

如果有代码依赖于全局 JSX 命名空间的存在,您可以通过在项目中显式导入或引用 vue/jsx 来保留精确的 3.4 版本之前的全局行为,从而注册全局 JSX 命名空间。

渲染函数食谱

以下我们将提供一些将模板功能实现为其等效渲染函数/JSX的常见食谱。

v-if

模板

template
<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>

等效的渲染函数/JSX

js
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
jsx
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>
js
h('div', [this.ok ? h('div', 'yes') : h('span', 'no')])
jsx
<div>{this.ok ? <div>yes</div> : <span>no</span>}</div>

v-for

模板

template
<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>

等效的渲染函数/JSX

js
h(
  'ul',
  // assuming `items` is a ref with array value
  items.value.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)
jsx
<ul>
  {items.value.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>
js
h(
  'ul',
  this.items.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)
jsx
<ul>
  {this.items.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>

v-on

on 开头,后跟一个大写字母的属性名的 Props 被视为事件监听器。例如,onClick 是模板中 @click 的等效。

js
h(
  'button',
  {
    onClick(event) {
      /* ... */
    }
  },
  'Click Me'
)
jsx
<button
  onClick={(event) => {
    /* ... */
  }}
>
  Click Me
</button>

事件修饰符

对于 .passive.capture.once 事件修饰符,它们可以用驼峰式连接到事件名称之后。

例如

js
h('input', {
  onClickCapture() {
    /* listener in capture mode */
  },
  onKeyupOnce() {
    /* triggers only once */
  },
  onMouseoverOnceCapture() {
    /* once + capture */
  }
})
jsx
<input
  onClickCapture={() => {}}
  onKeyupOnce={() => {}}
  onMouseoverOnceCapture={() => {}}
/>

对于其他事件和键修饰符,可以使用 withModifiers 助手函数。

js
import { withModifiers } from 'vue'

h('div', {
  onClick: withModifiers(() => {}, ['self'])
})
jsx
<div onClick={withModifiers(() => {}, ['self'])} />

组件

为了为组件创建一个 vnode,传递给 h() 的第一个参数应该是组件定义。这意味着在使用渲染函数时,无需注册组件 - 您可以直接使用导入的组件。

js
import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return h('div', [h(Foo), h(Bar)])
}
jsx
function render() {
  return (
    <div>
      <Foo />
      <Bar />
    </div>
  )
}

正如我们所看到的,h 可以与从任何文件格式导入的有效 Vue 组件一起工作。

动态组件与渲染函数一样简单。

js
import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return ok.value ? h(Foo) : h(Bar)
}
jsx
function render() {
  return ok.value ? <Foo /> : <Bar />
}

如果一个组件通过名称注册且不能直接导入(例如,通过库全局注册),可以使用 resolveComponent() 助手函数来编程式地解决。

渲染插槽

在渲染函数中,可以从 setup() 上下文中访问插槽。每个 slots 对象上的插槽都是一个 返回 vnodes 数组的函数

js
export default {
  props: ['message'],
  setup(props, { slots }) {
    return () => [
      // default slot:
      // <div><slot /></div>
      h('div', slots.default()),

      // named slot:
      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        slots.footer({
          text: props.message
        })
      )
    ]
  }
}

JSX 等价物

jsx
// default
<div>{slots.default()}</div>

// named
<div>{slots.footer({ text: props.message })}</div>

在渲染函数中,可以从 this.$slots 中访问插槽。

js
export default {
  props: ['message'],
  render() {
    return [
      // <div><slot /></div>
      h('div', this.$slots.default()),

      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        this.$slots.footer({
          text: this.message
        })
      )
    ]
  }
}

JSX 等价物

jsx
// <div><slot /></div>
<div>{this.$slots.default()}</div>

// <div><slot name="footer" :text="message" /></div>
<div>{this.$slots.footer({ text: this.message })}</div>

传递插槽

将子元素传递给组件的工作方式与传递给元素的方式略有不同。我们需要传递一个槽函数,或者一个槽函数的对象。槽函数可以返回普通渲染函数可以返回的任何内容 - 当在子组件中访问时,这些内容将始终规范化为 vnodes 数组。

js
// single default slot
h(MyComponent, () => 'hello')

// named slots
// notice the `null` is required to avoid
// the slots object being treated as props
h(MyComponent, null, {
  default: () => 'default slot',
  foo: () => h('div', 'foo'),
  bar: () => [h('span', 'one'), h('span', 'two')]
})

JSX 等价物

jsx
// default
<MyComponent>{() => 'hello'}</MyComponent>

// named
<MyComponent>{{
  default: () => 'default slot',
  foo: () => <div>foo</div>,
  bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>

将插槽作为函数传递可以让子组件延迟调用它们。这导致子组件跟踪插槽的依赖项,而不是父组件,从而实现更准确和高效的更新。

作用域插槽

要在父组件中渲染作用域插槽,需要将插槽传递给子组件。注意,插槽现在有一个参数text。该插槽将在子组件中调用,并将子组件的数据传递给父组件。

js
// parent component
export default {
  setup() {
    return () => h(MyComp, null, {
      default: ({ text }) => h('p', text)
    })
  }
}

请记得传递null,这样插槽就不会被视为属性。

js
// child component
export default {
  setup(props, { slots }) {
    const text = ref('hi')
    return () => h('div', null, slots.default({ text: text.value }))
  }
}

JSX 等价物

jsx
<MyComponent>{{
  default: ({ text }) => <p>{ text }</p>  
}}</MyComponent>

内置组件

内置组件,如<KeepAlive><Transition><TransitionGroup><Teleport><Suspense>,必须在渲染函数中使用之前导入。

js
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  setup () {
    return () => h(Transition, { mode: 'out-in' }, /* ... */)
  }
}
js
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  render () {
    return h(Transition, { mode: 'out-in' }, /* ... */)
  }
}

v-model

在模板编译期间,v-model指令被展开为modelValueonUpdate:modelValue属性——我们将不得不自己提供这些属性。

js
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    return () =>
      h(SomeComponent, {
        modelValue: props.modelValue,
        'onUpdate:modelValue': (value) => emit('update:modelValue', value)
      })
  }
}
js
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  render() {
    return h(SomeComponent, {
      modelValue: this.modelValue,
      'onUpdate:modelValue': (value) => this.$emit('update:modelValue', value)
    })
  }
}

自定义指令

可以使用withDirectives来将自定义指令应用于vnode。

js
import { h, withDirectives } from 'vue'

// a custom directive
const pin = {
  mounted() { /* ... */ },
  updated() { /* ... */ }
}

// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
  [pin, 200, 'top', { animate: true }]
])

如果指令通过名称注册且无法直接导入,则可以使用resolveDirective辅助函数来解析。

模板引用

使用组合式API时,模板引用是通过将ref()本身作为prop传递给vnode来创建的。

js
import { h, ref } from 'vue'

export default {
  setup() {
    const divEl = ref()

    // <div ref="divEl">
    return () => h('div', { ref: divEl })
  }
}

使用选项式API时,模板引用是通过将ref名称作为字符串传递给vnode的props来创建的。

js
export default {
  render() {
    // <div ref="divEl">
    return h('div', { ref: 'divEl' })
  }
}

函数式组件

函数式组件是没有自己状态的组件的另一种形式。它们像纯函数一样工作:输入props,输出vnodes。它们在没有创建组件实例(即没有this)的情况下渲染,也没有通常的生命周期钩子。

要创建函数式组件,我们使用一个普通函数,而不是选项对象。这个函数实际上是组件的render函数。

函数式组件的签名与setup()钩子的签名相同。

js
function MyComponent(props, { slots, emit, attrs }) {
  // ...
}

由于函数式组件没有this引用,Vue将把props作为第一个参数传递。

js
function MyComponent(props, context) {
  // ...
}

第二个参数,context,包含三个属性:attrsemitslots。这些分别相当于实例属性$attrs$emit$slots

对于函数式组件,大多数常规的配置选项都不可用。但是,可以通过将它们作为属性添加来定义propsemits

js
MyComponent.props = ['value']
MyComponent.emits = ['click']

如果没有指定props选项,则传递给函数的props对象将包含所有属性,与attrs相同。除非指定了props选项,否则不会将属性名称规范化为camelCase。

对于具有显式props的函数式组件,与正常组件一样,属性穿透的工作方式几乎相同。但是,对于没有显式指定props的函数式组件,默认情况下只会从attrs继承classstyleonXxx事件监听器。在两种情况下,都可以将inheritAttrs设置为false来禁用属性继承。

js
MyComponent.inheritAttrs = false

功能组件可以像普通组件一样进行注册和使用。如果你将一个函数作为第一个参数传递给 h(),它将被视为一个功能组件。

编写功能组件

功能组件可以根据其命名或匿名进行类型化。《a href="https://github.com/vuejs/language-tools" target="_blank" rel="noreferrer">Vue - Official extension 也支持在 SFC 模板中消费时对正确类型化的功能组件进行类型检查。

命名功能组件

tsx
import type { SetupContext } from 'vue'
type FComponentProps = {
  message: string
}

type Events = {
  sendMessage(message: string): void
}

function FComponent(
  props: FComponentProps,
  context: SetupContext<Events>
) {
  return (
    <button onClick={() => context.emit('sendMessage', props.message)}>
        {props.message} {' '}
    </button>
  )
}

FComponent.props = {
  message: {
    type: String,
    required: true
  }
}

FComponent.emits = {
  sendMessage: (value: unknown) => typeof value === 'string'
}

匿名功能组件

tsx
import type { FunctionalComponent } from 'vue'

type FComponentProps = {
  message: string
}

type Events = {
  sendMessage(message: string): void
}

const FComponent: FunctionalComponent<FComponentProps, Events> = (
  props,
  context
) => {
  return (
    <button onClick={() => context.emit('sendMessage', props.message)}>
        {props.message} {' '}
    </button>
  )
}

FComponent.props = {
  message: {
    type: String,
    required: true
  }
}

FComponent.emits = {
  sendMessage: (value) => typeof value === 'string'
}
渲染函数 & JSX 已加载