Render Functions & JSX,渲染函數不知道要幹嘛的?
JSX也沒很清楚他是什麼拉~ 快來看看
Vue 建議在大多數情況下使用模板來構建應用程式。然而,有時我們需要 JavaScript 的全程式化功能,這時我們可以使用渲染函數。
如果你對虛擬 DOM 和渲染函數的概念還不熟悉,請先閱讀《渲染機制》這一章。
Vue 提供了一個 h()
函數來創建 vnode:
import { h } from 'vue'
const vnode = h(
'div', // 類型
{ id: 'foo', class: 'bar' }, // 屬性
[
/* 子元素 */
]
)
h()
是 hyperscript 的簡寫,意思是“生成 HTML(超文本標記語言)的 JavaScript”。這個名字繼承了許多虛擬 DOM 實現的約定。一個更具描述性的名稱可以是 createVNode()
,但在渲染函數中多次調用這個函數時,較短的名稱會更有幫助。
h()
函數設計得非常靈活:
// 除了類型以外的所有參數都是可選的
h('div')
h('div', { id: 'foo' })
// 在屬性中可以使用屬性和屬性(props)
// Vue 會自動選擇正確的方式來分配它
h('div', { class: 'bar', innerHTML: 'hello' })
// 屬性修改器如 `.prop` 和 `.attr` 可以用 `.` 和 `^` 前綴分別添加
h('div', { '.name': 'some-name', '^width': '100' })
// class 和 style 有與模板中相同的對象/數組值支持
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件監聽器應以 onXxx 形式傳遞
h('div', { onClick: () => {} })
// 子元素可以是一個字符串
h('div', { id: 'foo' }, 'hello')
// 沒有屬性時可以省略屬性參數
h('div', 'hello')
h('div', [h('span', 'hello')])
// 子元素數組可以包含混合的 vnode 和字符串
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
注意:完整的 VNode 介面包含許多其他內部屬性,但強烈建議避免依賴除這裡列出的屬性之外的任何屬性。這可以避免內部屬性更改時意外中斷。
使用 Composition API 與模板時,setup()
鉤子的返回值用於向模板公開數據。然而,使用渲染函數時,我們可以直接返回渲染函數:
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// 返回渲染函數
return () => h('div', props.msg + count.value)
}
}
渲染函數在 setup()
中聲明,因此它自然可以訪問屬性和在相同作用域內聲明的任何響應性狀態。
除了返回單個 vnode 外,你還可以返回字符串或數組:
export default {
setup() {
return () => 'hello world!'
}
}
import { h } from 'vue'
export default {
setup() {
// 使用數組返回多個根節點
return () => [
h('div'),
h('div'),
h('div')
]
}
}
提示:確保返回一個函數,而不是直接返回值!setup()
函數每個組件只調用一次,而返回的渲染函數將被調用多次。
如果渲染函數組件不需要任何實例狀態,它們也可以直接聲明為一個函數以簡化:
function Hello() {
return 'hello world!'
}
沒錯,這是一個有效的 Vue 組件!有關此語法的更多詳細信息,請參見函數式組件。
組件樹中的所有 vnode 必須唯一。這意味著以下渲染函數是無效的:
function render() {
const p = h('p', 'hi')
return h('div', [
// 嚇人的 - 重複的 vnode!
p,
p
])
}
如果你真的想多次複製相同的元素/組件,可以使用工廠函數。例如,以下渲染函數是一種有效的方式來渲染 20 個相同的段落:
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
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 實際上並沒有定義運行時語義,可以編譯成各種不同的輸出。如果你以前使用過 JSX,請注意 Vue 的 JSX 轉換與 React 的不同,因此你不能在 Vue 應用中使用 React 的 JSX 轉換。以下是一些與 React JSX 的顯著不同點:
class
和 for
作為 props,無需使用 className
或 htmlFor
。Vue 的類型定義還提供了 TSX 使用的類型推斷。在使用 TSX 時,確保在 tsconfig.json
中指定 "jsx": "preserve"
,以便 TypeScript 保留 JSX 語法,讓 Vue JSX 轉換處理。
與轉換類似,Vue 的 JSX 也需要不同的類型定義。
從 Vue 3.4 開始,Vue 不再隱式註冊全局 JSX 命名空間。要指示 TypeScript 使用 Vue 的 JSX 類型定義,請確保在 tsconfig.json
中包含以下內容:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
// ...
}
}
你也可以通過在文件頂部添加 /* @jsxImportSource vue */
註釋來按文件選擇加入。
如果有依賴全局 JSX 命名空間存在的代碼,可以通過顯式導入或引用 vue/jsx
在你的專案中註冊全局 JSX 命名空間,從而保留完全與 3.4 之前一致的全局行為。
以下是一些將模板功能轉換為等效渲染函數/JSX 的常見實踐。
<template>
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
</template>
等效渲染函數/JSX:
// 渲染函數
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
// JSX
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>
<template>
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
</template>
等效渲染函數/JSX:
// 渲染函數
h(
'ul',
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>
以 "on" 開頭並接著大寫字母的屬性名被視為事件監聽器。例如,onClick 等同於模板中的 @click。
// 渲染函數
h(
'button',
{
onClick(event) {
/* ... */
}
},
'Click Me'
)
// JSX
<button onClick={(event) => { /* ... */ }}>
Click Me
</button>
對於 .passive、.capture 和 .once 事件修飾符,可以在事件名後使用駝峰式拼寫來連接。例如:
// 渲染函數
h('input', {
onClickCapture() { /* 捕獲模式的監聽器 */ },
onKeyupOnce() { /* 只觸發一次 */ },
onMouseoverOnceCapture() { /* 一次 + 捕獲 */ }
})
// JSX
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
對於其他事件和鍵盤修飾符,可以使用 withModifiers 助手:
import { withModifiers } from 'vue'
// 渲染函數
h('div', {
onClick: withModifiers(() => {}, ['self'])
})
// JSX
<div onClick={withModifiers(() => {}, ['self'])} />
要為組件創建 vnode,傳給 h() 的第一個參數應該是組件定義。這意味著在使用渲染函數時,不需要註冊組件 - 可以直接使用導入的組件:
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>
)
}
動態組件使用渲染函數也很簡單:
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 對象上的每個插槽都是一個返回 vnode 陣列的函數:
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 }))
]
}
}
JSX 等效:
// 默認
<div>{slots.default()}</div>
// 命名
<div>{slots.footer({ text: props.message })}</div>
將子元素傳遞給組件與將子元素傳遞給元素略有不同。我們需要傳遞一個插槽函數,或者一個插槽函數的對象。插槽函數可以返回任何正常渲染函數可以返回的內容,這些內容將在子組件中訪問時被規範化為 vnode 陣列。
// 單個默認插槽
h(MyComponent, () => 'hello')
// 命名插槽
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>
傳遞函數形式的插槽允許它們由子組件懶加載,這使得插槽的依賴項由子組件而不是父組件進行跟踪,從而使更新更準確高效。
在父組件中渲染作用域插槽時,將插槽傳遞給子組件。注意插槽現在有一個參數 text。插槽將在子組件中被調用,並將子組件的數據傳遞給父組件。
// 父組件
export default {
setup() {
return () => h(MyComp, null, {
default: ({ text }) => h('p', text)
})
}
}
記得傳遞 null 以避免將插槽視為道具。
// 子組件
export default {
setup(props, { slots }) {
const text = ref('hi')
return () => h('div', null, slots.default({ text: text.value }))
}
}
JSX 等效:
<MyComponent>{{
default: ({ text }) => <p>{ text }</p>
}}</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 指令在模板編譯期間被擴展為 modelValue 和 onUpdate
道具 - 我們需要自己提供這些道具:
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() 本身作為道具傳給 vnode 來創建:
import { h, ref } from 'vue'
export default {
setup() {
const divEl = ref()
// <div ref="divEl">
return () => h('div', { ref: divEl })
}
}
函數型元件是一種不具有自己狀態的元件形式。它們像純粹的函數一樣:傳入 props
,傳出 vnodes
。它們在渲染時不會創建元件實例(即沒有 this
),也不會有常規的元件生命周期鉤子。
要創建函數型元件,我們使用一個普通的函數,而不是選項物件。這個函數實際上就是該元件的渲染函數。
函數型元件的函數簽名與 setup()
鉤子相同:
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
大多數常規的元件配置選項在函數型元件中不可用。不過,我們可以通過將 props
和 emits
當作屬性來定義它們:
MyComponent.props = ['value']
MyComponent.emits = ['click']
如果沒有指定 props
選項,則傳遞給函數的 props
物件將包含所有屬性,這些屬性將與 attrs
相同。除非指定 props
選項,否則屬性名稱不會被標準化為駝峰式命名。
對於有明確 props
的函數型元件,屬性繼承與常規元件的行為相似。然而,對於沒有明確指定 props
的函數型元件,只有 class
、style
和 onXxx
事件監聽器會從 attrs
繼承。無論如何,可以將 inheritAttrs
設為 false
來禁用屬性繼承:
MyComponent.inheritAttrs = false
函數型元件可以像常規元件一樣註冊和使用。如果將一個函數作為 h()
的第一個參數傳遞,它將被視為函數型元件。
函數型元件可以根據是否有命名來進行打字。Vue 的官方擴展也支持對正確打字的函數型元件進行類型檢查,當它們在 SFC 模板中被消費時。
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'
}
所以現在除了模板還有渲染函數跟JSX的寫法啊!
差異不知道是什麼?
在 Vue.js 中,現在可以使用模板(template)、渲染函數(render function)和 JSX 來定義組件的視圖。這三種方法各有優缺點,適用於不同的場景。下面是它們的差異和適用場合:
特點:
適用場合:
例子:
<template>
<div class="container">
<h1>Hello, Vue!</h1>
<p>This is a paragraph.</p>
</div>
</template>
<script>
export default {
name: 'MyComponent'
}
</script>
<style scoped>
.container {
padding: 20px;
}
</style>
特點:
適用場合:
例子:
export default {
render(h) {
return h('div', { class: 'container' }, [
h('h1', 'Hello, Vue!'),
h('p', 'This is a paragraph.')
]);
}
}
特點:
適用場合:
例子:
import { defineComponent } from 'vue';
export default defineComponent({
render() {
return (
<div class="container">
<h1>Hello, Vue!</h1>
<p>This is a paragraph.</p>
</div>
);
}
});
總之,根據項目的需求和開發者的習慣來選擇最合適的方式,三者都是 Vue.js 提供的強大工具,合理使用可以使開發更加高效和靈活。