渲染函式與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'
}