# 【Vue】渲染函式 & JSX
一般情況都是使用 template 來做渲染,但有時候就是需要 javascript ,所以這時候就需要渲染函式。
## 基本用法
創建 vnode
Vue 提供了 `h()` 函式來創建 vnode
```js=
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
```
h 是 hyperscript 的簡稱,意思是能生成 HTML 的 javascript,這個名字來源於實作虛擬 DOM 的默認規則,更正確的名稱應該是 `createVNode()`,但一個常用的函式名字越簡短越有利。
`h()` 的使用方式:
```js=
// 除了类型必填以外,其他的参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })
// 像 `.prop` 和 `.attr` 这样的的属性修饰符
// 可以分别通过 `.` 和 `^` 前缀来添加
h('div', { '.name': 'some-name', '^width': '100' })
// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 props 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])
```
舉個例子:
```js=
const vnode = h('div', { id: 'foo' }, [])
vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null
```
:::warning
vnode 內部其實包含很多屬性,但強烈建議不要使用這裡沒列出的屬性,以避免變更屬性導致內部程式碼無法運行的情況
:::
## 宣告渲染函式
在使用 Composition API 時,`setup()` hook 的回傳值是用來給予 template 資料,不過在使用渲染函式的時候可以直接回傳渲染函式就好。
```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 或是響應式狀態。
除了回傳渲染函式,你也可以回傳字串或陣列
```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')
]
}
}
```
:::info
確保`setup()`都是回傳函式,而不是直接回傳值,因為`setup()`只會被呼叫一次,而你回傳的函式可以被叫很多次
:::
如果渲染函式的元件不需要任何的 instance state,方便起見你也可以直接把他們宣告成一般的函式
```js=
function Hello() {
return 'hello world!'
}
```
其實這樣就是合法的 Vue 元件了
## Vnode 必須唯一
```js=
function render() {
const p = h('p', 'hi')
return h('div', [
// 啊哦,重復的 vnodes 是無效的
p,
p
])
}
```
如果你真的很想這樣做,你可以用工廠函式來達成
```js=
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
```
## JSX/TSX
JSX 是 javascript 類似 XML 的一個拓展
```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](https://github.com/vuejs/babel-plugin-jsx)
雖然最早 jsx 是由 React 引入,但實際上 jsx 沒有定義運行上的語意,而且也能被編譯成不同類型的輸出,如果你之前使用過 jsx,請注意 **Vue 的 JSX 轉換方式與 React 不同**,因此你不能在 Vue 中使用 React 的 jsx 轉換法,以下是一些明顯的區別:
* 可以使用 HTML attritube (例如class、for) 作為 props ,不需使用 `className` 和 `htmlFor`
* 傳遞子素件給元件(例如slot)的方式不同
Vue 也支援 TSX,使用的時候要在 `tsconfig.json` 中設置 `"jsx": "preserve"`
### JSX Type Inference
與轉換類似,Vue 與 jsx 也需要不同的類型定義,目前 Vue 會在全域範圍自動註冊 Vue 的 JSX 類型。
全域註冊的 jsx 與其他同樣需要 jsx 的 library 一起使用時可能會引起衝突(特別是 React)。從 3.3 開始,Vue 利用 [jsxImportSource](https://www.typescriptlang.org/tsconfig#jsxImportSource)選項指定 jsx 命名空間,並計畫會在 3.4 移除全域註冊。
對於 tsx 用戶建議在升級到3.3之後把 tsconfig.json 中的 `jsxImportSource` 設置為 `vue`,或是針對單個文件加入 `/* @jsxImportSource vue */`,這可以讓你使用區域註冊,並在3.4無痛升級。
如果你還有程式碼會依賴全域的 jsx,你可以通過 explicitly referencing ,把路徑改成 `vue/jsx`來保留 3.4 之前的全域註冊。
## 渲染函式案例
### v-if
template:
```html=
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
```
h():
```js=
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
```
jsx:
```jsx=
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>
```
### v-for
template:
```vue=
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
```
h():
```js=
h(
'ul',
// assuming `items` is a ref with array value
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
```
jsx:
```jsx=
<ul>
{items.value.map(({ id, text }) => {
return <li key={id}>{text}</li>
})}
</ul>
```
### v-on
h():
```js=
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
```
jsx:
```jsx=
<button
onClick={(event) => {
/* ... */
}}
>
click me
</button>
```
### 事件修飾符
`.passive`、`.capture` 和 `.once`,可以用駝峰式命名將他們接在事件名後面
h():
```js=
h('input', {
onClickCapture() {
/* 捕捉模式中的监听器 */
},
onKeyupOnce() {
/* 只触发一次 */
},
onMouseoverOnceCapture() {
/* 单次 + 捕捉 */
}
})
```
jsx:
```jsx=
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
```
其他的事件或是按鍵修飾符,可以用 `withModifiers` 函式
h():
```js=
import { withModifiers } from 'vue'
h('div', {
onClick: withModifiers(() => {}, ['self'])
})
```
jsx:
```jsx=
<div onClick={withModifiers(() => {}, ['self'])} />
```
### 元件
在為元件創建 vnode 時,第一個傳給 h() 的參數就是元件的定義,這代表你使用渲染函式就不用再註冊元件了
h():
```js=
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return h('div', [h(Foo), h(Bar)])
}
```
jsx:
```jsx=
function render() {
return (
<div>
<Foo />
<Bar />
</div>
)
}
```
不論是什麼類型的文件,只要你導入的是有效的 Vue 元件,`h` 就能正常運作。
動態函式也可以直接使用:
h():
```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`對象中的插槽都是一個**返回 `vnode` 陣列的渲染函式**
```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=
// 默认插槽
<div>{slots.default()}</div>
// 具名插槽
<div>{slots.footer({ text: props.message })}</div>
```
### 傳遞插槽
向元件傳遞子元素和項元素傳遞子元素的方式其實不盡相同,我們需要傳遞的是 slot 函式或是一個包含 slot 函式的物件而非陣列,普通的渲染函式可以回傳的 slot 函式都能回傳,並且都會在子元件被拜訪時被轉換成 vnode 陣列。
h():
```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>
```
用函式的方式傳遞可以使得他們以 lazy invoke 的方式被子元件使用,這能夠確保 slot 的依賴關係是存在於子元件而非父元件,使得更新更準確與有效。
## 內置元件
`<KeepAlive>`、`<Transition>`、`<TransitionGroup>`、`<Teleport>` 和 `<Suspense>` 等要 import 才能用
h():
```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`,在模板編譯的過程中我們要自己提供這些 props
```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'
// 自定义指令
const pin = {
mounted() { /* ... */ },
updated() { /* ... */ }
}
// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
```
如果是名稱註冊一樣可以使用 `resolveDirective` 來解決
## 模板引用
在 Composition API 中,模板引用通過 `ref()` 本身作為一個屬性傳遞給 vnode 來創建
```js=
import { h, ref } from 'vue'
export default {
setup() {
const divEl = ref()
// <div ref="divEl">
return () => h('div', { ref: divEl })
}
}
```
## 函式式元件
函式式元件是元件的替代形式,它们不具有自己的狀態。它们的行為類似於纯函式:接收 props,輸出虚擬節點 (vnode)。它们在渲染时不會創建組件實例(即没有 this),也不會使用一般的元件生命周期勾子。
要創建函式式元件,我们使用普通的函式,而不是 [options object](https://www.codereadability.com/what-are-javascript-options-objects/)。這個函式實際上就是元件的渲染函式。
基本上就和 setup() hook 相同:
```js=
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
```
常見的元件配置在這裡都不能使用,不過可以使用 props 和 emits
```js=
MyComponent.props = ['value']
MyComponent.emits = ['click']
```
如果這個 props 沒被定義就會像 attrs 一樣包含所有的 attritube,除非有指定 props 選項,不然他們不會被處理成駝峰式命名。
對於有明確 props 的 函式式元件,屬性傳透的原理和一般的元件一樣,但是對於沒有明確指定 props 的 函式式元件只會有 `class`、`style`、`onXXX`默認會從中繼承,在這兩種情況下都可以將 `inheritAttrs` 設成 false 來禁用屬性繼承。
### 為他們標註類型(TS)
函式式元件可以根據他們是否有命名來標註類型,在 SFC 中 Volar 還支持型別檢查
```js=
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'
}
```
匿名函式式組件:
```js=
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'
}
```