为了提高英语水平和保持技术成长,开始按计划翻译一些短篇和博客,有问题欢迎讨论👻
原文:Render Functions & JSX
正文
Vue 推荐大多数情况下使用模板来构建应用。不过,在某些情况下,我们需要使用JavaScript的全部编程能力。这时,我们就需要使用渲染函数。
Basic Usage
Creating Vnodes
Vue提供了h()
函数用于创建vnode
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
h()
是 hyperscript 的缩写,意思是”生成HTML(超文本标记语言)的JavaScript”。这个名字来自于许多虚拟DOM实现时的共同约定。一个描述性更强的名称可以为createVnode()
,但当你需要在渲染函数中多次调用时,一个更简短的名称会有所帮助。
这个h()
函数设计的非常灵活
// 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结构如下:
const vnode = h('div', { id: 'foo' }, [])
vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null
Note
完整的VNode结构还包含许多其他内部属性,当强烈建议避免依赖以上所列的以外其他属性。这样可以避免在内部属性发生变化时出现意外中断。
Declaring Render Functions
在通过 Composition API 使用模版时,setup()
的返回值是用于向模版暴露数据。但在使用渲染函数时,我们可以直接返回渲染函数:
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外,还可以返回字符串或者数组:
export default {
setup() {
return () => 'hello world!'
}
}
import { h } from 'vue'
export default {
setup() {
// use an array to return multiple root nodes
return () => [
h('div'),
h('div'),
h('div')
]
}
}
TIP
确保返回的是一个函数,而不是直接返回值!因为每个组件的setup()
只会调用一次,而返回的渲染函数会被调用多次。
如果渲染函数不需要任何实例状态,你也可以直接声明一个函数:
function Hello() {
return 'hello world!'
}
没错,这是一个有效的Vue组件!有关此语法的更多详情,可以查看 Functional Components
Vnodes Must Be Unique
组件树中的所有vnode必须是唯一的。下面的渲染函数就意味着是无效的:
function render() {
const p = h('p', 'hi')
return h('div', [
// Yikes - duplicate 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文档了解详情。
虽然JSX最初是由React引入的,但它实际上并没有定义运行时语义,可以编译成各种不同的输出。如果你以前使用过JSX,需注意Vue的JSX转换和React的JSX转换是不同的,所以你不能在Vue应用中使用React的JSX转换,具体与React JSX的一些明显区别包括:
- 你可以使用
class
和for
等HTML属性作为props,无需使用className
或者htmlFor
。 - 将子元素传递给组件(即插槽)的工作方式有所不同。
Vue的类型定义还为TSX的使用提供了类型推断。使用TSX时,确保在tsconfig.json中指定"jsx": "preserve"
,以便在TypeScript保留JSX语法,供Vue JSX转换处理。
JSX Type Inference
与transform一样,Vue的JSX也需要不同的类型定义。目前,Vue会自动全局注册Vue的JSX类型。这意味着,当Vue的类型可用时,TSX就能开箱即用。
Vue的全局JSX类型可能会与其他同样需要JSX类型推导的库发生冲突,特别是React。从3.3版本开始,Vue支持通过TypeScript的jsxImportSource选项指定JSX命名空间。我们计划在3.4版本中移除默认的全局JSX命名空间注册。
对于TSX用户,建议在升级到3.3之后在 tsconfig.json 中将 jsxImportSource 选项设置为 “vue”,或者在每个文件中使用 /* @jsxImportSource vue */
。这样,你现在就可以选择加入新行为,并在 3.4 发布时无缝升级。
如果有代码依赖于全局JSX命名空间,你可以通过显式引用 vue/jsx(它会注册全局 JSX 命名空间)来保留完全相同的 3.4 之前的全局行为
Render Function Recipes
下面我们会提供一些用等价的渲染函数/JSX实现模版功能的常用方法。
v-if
template:
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
等价 渲染函数/JSX:
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
<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:
h(
'ul',
// assuming `items` is a ref with array value
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
<ul>
{items.value.map(({ id, text }) => {
return <li key={id}>{text}</li>
})}
</ul>
v-on
props名称以on
开头且后面跟大写字母的被视为事件监听器。比如,onClick
相当于模版的@click
。
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
<button
onClick={(event) => {
/* ... */
}}
>
click me
</button>
Event Modifiers
对于.passive
,.capture
和.once
事件修饰符,可以驼峰写法在事件名称后面进行连接。
例如:
h('input', {
onClickCapture() {
/* listener in capture mode */
},
onKeyupOnce() {
/* triggers only once */
},
onMouseoverOnceCapture() {
/* once + capture */
}
})
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
然后其余的事件和按键修饰符,可以使用withModifiers
协助使用:
import { withModifiers } from 'vue'
h('div', {
onClick: withModifiers(() => {}, ['self'])
})
<div onClick={withModifiers(() => {}, ['self'])} />
Components
要为组件创建vnode,传递给h()
的第一个参数应该是组件定义。这就意味着使用渲染函数时,无需注册组件,只是直接使用导入的组件即可:
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return h('div', [h(Foo), h(Bar)])
}
function render() {
return (
<div>
<Foo />
<Bar />
</div>
)
}
正如我们看到的,只要是有效的Vue组件,h函数就能处理任何文件格式导入的组件。
动态组件在渲染组件中可直接使用:
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return ok.value ? h(Foo) : h(Bar)
}
function render() {
return ok.value ? <Foo /> : <Bar />
}
如果组件是用名称注册的,无法直接导入(例如,有库进行的全局注册),这种情况可以使用resolveComponent
来解析该组件。
Rendering Slots
在渲染函数中,可以通过setup()
的上下文访问slot。然后slots对象上的每一个slot都是一个函数,会返回一个vnodes数组:
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:
// default
<div>{slots.default()}</div>
// named
<div>{slots.footer({ text: props.message })}</div>
Passing Slots
将子元素传递给组件和子元素传递给元素的方式有些不同。我们不需要传递数组,而是传递slot函数或者slot函数对象。slot函数可以返回与普通渲染函数的返回值一样的内容,并且在子组件访问时,这些内容将始终会转换为一个vnodes数组。
// 单个默认插槽
h(MyComponent, () => 'hello')
// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
等价的JSX:
// default
<MyComponent>{() => 'hello'}</MyComponent>
// named
<MyComponent>{{
default: () => 'default slot',
foo: () => <div>foo</div>,
bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>
将slot作为函数传递给子组件,可以让子组件懒散地调用slot。这样,子组件而不是父组件就能跟踪slot的依赖关系,从而实现更准确、更有效的更新。
Built-in Components
内置组件如<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,在渲染函数中就必须自己提供这些props:
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(SomeComponent, {
modelValue: props.modelValue,
'onUpdate:modelValue': (value) => emit('update:modelValue', value)
})
}
}
Custom Directives
自定义指令可以通过withDirectives
应用到vnode:
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
对其进行解析。
Template Refs
在使用组合式API时,模版ref是通过ref()
本身作为props传递给vnode创建的
import { h, ref } from 'vue'
export default {
setup() {
const divEl = ref()
// <div ref="divEl">
return () => h('div', { ref: divEl })
}
}
Functional Components
函数组件是组件的另一种形式,自身没有任何状态。执行的内容就像纯函数,props输入,vnode输出。在渲染时不创建组件实例(即没有this
),也没有一些组件声明周期钩子。
我们会通过一个普通函数来创建一个函数组件,而不是一个选项对象。该函数实际上就是组件的渲染函数。
函数组件的签名与setup()
hook相同:
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
组件的大多数常规配置选项都不适用于函数组件。不过,可以通过添加属性来定义props
和emits
:
MyComponent.props = ['value']
MyComponent.emits = ['click']
如果未指定props选项,那么传递给函数的props对象将包含所有属性,与attrs相同。除非指定来props选项,否则props名称不会规范为驼峰。
对于没有明确props的函数组件,属性穿透的工作原理与普通组件大致相同。但是,对于没有明确指定props的函数组件,默认情况下将会从attrs
继承class
、style
和onXxx
事件侦听器。无论是哪种情况,都可以将inheritAttrs
设为false
以禁用属性继承:
MyComponent.inheritAttrs = false
函数组件可以像普通组件一样注册和使用。如果将函数作为h()
的第一个参数,它会被视为函数组件。
Typing Functional Components
函数组件可根据其命名或匿名类型来进行标注类型。在SFC模板中使用时,Volar还支持对其进行类型检查。
具名函数组件
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'
}
匿名函数组件
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'
}