渲染函数与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()
函数在每个组件中仅调用一次,而返回的渲染函数将被多次调用。
如果渲染函数组件不需要任何实例状态,它们也可以直接声明为函数以简化代码
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 属性(如
class
和for
)作为 props - 无需使用className
或htmlFor
。 - 将子元素传递给组件(即插槽)工作方式不同。
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>
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>
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>
传递插槽
将子元素传递给组件的工作方式与传递给元素的方式略有不同。我们需要传递一个槽函数,或者一个槽函数的对象。槽函数可以返回普通渲染函数可以返回的任何内容 - 当在子组件中访问时,这些内容将始终规范化为 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' }, /* ... */)
}
}
v-model
在模板编译期间,v-model
指令被展开为modelValue
和onUpdate: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)
})
}
}
自定义指令
可以使用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 })
}
}
函数式组件
函数式组件是没有自己状态的组件的另一种形式。它们像纯函数一样工作:输入props,输出vnodes。它们在没有创建组件实例(即没有this
)的情况下渲染,也没有通常的生命周期钩子。
要创建函数式组件,我们使用一个普通函数,而不是选项对象。这个函数实际上是组件的render
函数。
函数式组件的签名与setup()
钩子的签名相同。
js
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
对于函数式组件,大多数常规的配置选项都不可用。但是,可以通过将它们作为属性添加来定义props
和emits
。
js
MyComponent.props = ['value']
MyComponent.emits = ['click']
如果没有指定props
选项,则传递给函数的props
对象将包含所有属性,与attrs
相同。除非指定了props
选项,否则不会将属性名称规范化为camelCase。
对于具有显式props
的函数式组件,与正常组件一样,属性穿透的工作方式几乎相同。但是,对于没有显式指定props
的函数式组件,默认情况下只会从attrs
继承class
、style
和onXxx
事件监听器。在两种情况下,都可以将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'
}