渲染函数 & JSX
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数。
基本用法
创建 Vnodes
Vue 提供了一个 h()
函数用于创建 vnodes:
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
h()
是 hyperscript 的简称——意思是"能生成 HTML (超文本标记语言) 的 JavaScript"。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVnode()
,但当你需要多次使用渲染函数时,一个简短的名字会更省力。
声明渲染函数
当使用组合式 API 时,setup()
钩子的返回值是用来给模板提供数据的。然而当我们使用渲染函数时,我们可以直接把渲染函数返回:
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// 返回渲染函数
return () => h('div', props.msg + count.value)
}
}
在 <script setup>
中,我们可以直接导出一个渲染函数:
<script setup>
import { ref, h } from 'vue'
const count = ref(1)
function render() {
return h('div', count.value)
}
</script>
<template>
<render />
</template>
Vnodes 必须唯一
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
function render() {
const p = h('p', 'hi')
return h('div', [
// 错误 - 重复的 vnodes
p,
p
])
}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
JSX / TSX
JSX 是 JavaScript 的一个类似 XML 的扩展,它允许我们像这样编写代码:
const vnode = <div>hello</div>
在 JSX 表达式中,使用大括号来嵌入动态值:
const vnode = <div id={dynamicId}>hello, {userName}</div>
create-vue
和 Vue CLI 都有预配置的 JSX 语法支持。如果你想手动配置 JSX,请参阅 @vue/babel-plugin-jsx
文档来了解更多细节。
虽然最早是由 React 引入,但实际上 JSX 语法并没有定义运行时语义,并且能被编译成各种不同的输出形式。如果你之前使用过 JSX 语法,那么请注意 Vue 的 JSX 转换方式与 React 中 JSX 的转换方式不同,因此你不能在 Vue 应用中使用 React 的 JSX 转换。与 React JSX 语法的一些明显差异包括:
- 可以使用 HTML attributes 比如
class
和for
作为 props - 不需要使用className
或htmlFor
。 - 传递子元素给组件 (比如 slots) 的方式不同。
Vue 的类型定义也提供了 TSX 语法的类型推导支持。当使用 TSX 语法时,确保在 tsconfig.json
中配置了 "jsx": "preserve"
,这样的话 TypeScript 就能保证 Vue JSX 语法转换过程中的类型正确性。
JSX 类型推导
与转换类似,Vue 的 JSX 也需要不同的类型定义。
从 Vue 3.4 开始,Vue 不再隐式注册全局 JSX
命名空间。要指示 TypeScript 使用 Vue 的 JSX 类型定义,请确保在你的 tsconfig.json
中包含以下内容:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
// ...
}
}
你也可以通过在文件顶部添加 /* @jsxImportSource vue */
注释来选择性地启用。
如果有代码依赖于全局 JSX
命名空间的存在,你可以通过在你的项目中显式导入或引用 vue/jsx
来保留 3.4 之前的全局行为,它注册了全局 JSX
命名空间。
渲染函数案例
下面我们提供一些等价于模板功能的渲染函数案例。
v-if
模板:
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
等价的渲染函数:
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
v-for
模板:
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
等价的渲染函数:
h(
'ul',
// 假设 `items` 是一个 ref
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
v-on
以 on
开头,并跟着大写字母的 props 会被当作事件监听器。比如,onClick
相当于模板中的 @click
。
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
事件修饰符
对于 .passive
、.capture
和 .once
这些事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
实例:
h('input', {
onClickCapture() {
/* 捕获模式中的监听器 */
},
onKeyupOnce() {
/* 只触发一次 */
},
onMouseoverOnceCapture() {
/* 单次 + 捕获 */
}
})
对于所有其他的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键:.enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码) |
修饰键:.ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey 、shiftKey 或者 metaKey ) |
这里是一个使用所有修饰符的例子:
h('input', {
onKeyUp(event) {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果向上键不是回车键,则中止
// 没有同时按下按键 (13) 和 shift 键
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件传播
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
})
插槽
在渲染函数中,插槽可以通过 setup()
的上下文来访问。每个 slots
对象中的插槽都是一个返回 vnodes 数组的函数:
export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// 默认插槽:
// <div><slot /></div>
h('div', slots.default()),
// 具名插槽:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
传递插槽
向组件传递子元素的方式与向元素传递子元素的方式有所不同。我们需要传递一个插槽函数或者是一个包含插槽函数的对象而非是数组,插槽函数的返回值同一个正常的渲染函数的返回值一样——在子组件中被访问时总是会被规范为 vnodes 数组。
// 单个默认插槽
h(MyComponent, () => 'hello')
// 具名插槽
// 注意 `null` 是必需的
// 以避免插槽对象被当作是 props
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
等价的 JSX 语法:
// 默认插槽
<MyComponent>{() => 'hello'}</MyComponent>
// 具名插槽
<MyComponent>
{{
default: () => 'default slot',
foo: () => <div>foo</div>,
bar: () => [<span>one</span>, <span>two</span>]
}}
</MyComponent>
传递插槽为函数使得它们可以被子组件懒调用。这能确保它被注册为子组件的依赖关系,而不是父组件。这使得更新更加准确及有效。
内置组件
诸如 <KeepAlive>
、<Transition>
、<TransitionGroup>
、<Teleport>
和 <Suspense>
等内置组件在渲染函数中必须导入才能使用:
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'
export default {
setup () {
return () => h(Transition, { mode: 'out-in' }, /* ... */)
}
}
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些 props:
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 上:
import { h, withDirectives } from 'vue'
// 自定义指令
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 来创建:
import { h, ref } from 'vue'
export default {
setup() {
const divEl = ref()
return () =>
h('div', {
ref: divEl
})
}
}
函数式组件
函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。
我们用一个普通的函数而不是一个选项对象来创建函数式组件。该函数实际上就是该组件的渲染函数。
函数式组件的签名与 setup()
钩子相同:
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 props
和 emits
作为属性加入,以达到定义它们的目的:
MyComponent.props = ['value']
MyComponent.emits = ['click']
如果这个 props
选项没有被定义,那么被传入函数的 props
对象就会像 attrs
一样包含所有 attribute。除非指定了 props
选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。
对于有明确 props
的函数式组件,attribute 透传的工作方式与普通组件基本相同。但是,对于没有明确指定 props
的函数式组件,只有 class
、style
和 onXxx
事件监听器将从 attrs
中继承。在这两种情况下,都可以将 inheritAttrs
设置为 false
来禁用 attribute 继承:
MyComponent.inheritAttrs = false
函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h()
,它将会被当作一个函数式组件来对待。