Try  HackMD Logo HackMD

【Vue】渲染函式 & JSX

一般情況都是使用 template 來做渲染,但有時候就是需要 javascript ,所以這時候就需要渲染函式。

基本用法

創建 vnode
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() 的使用方式:

// 除了类型必填以外,其他的参数都是可选的 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')])

舉個例子:

const vnode = h('div', { id: 'foo' }, []) vnode.type // 'div' vnode.props // { id: 'foo' } vnode.children // [] vnode.key // null

vnode 內部其實包含很多屬性,但強烈建議不要使用這裡沒列出的屬性,以避免變更屬性導致內部程式碼無法運行的情況

宣告渲染函式

在使用 Composition API 時,setup() hook 的回傳值是用來給予 template 資料,不過在使用渲染函式的時候可以直接回傳渲染函式就好。

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 或是響應式狀態。
除了回傳渲染函式,你也可以回傳字串或陣列

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') ] } }

確保setup()都是回傳函式,而不是直接回傳值,因為setup()只會被呼叫一次,而你回傳的函式可以被叫很多次

如果渲染函式的元件不需要任何的 instance state,方便起見你也可以直接把他們宣告成一般的函式

function Hello() { return 'hello world!' }

其實這樣就是合法的 Vue 元件了

Vnode 必須唯一

function render() { const p = h('p', 'hi') return h('div', [ // 啊哦,重復的 vnodes 是無效的 p, p ]) }

如果你真的很想這樣做,你可以用工廠函式來達成

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 沒有定義運行上的語意,而且也能被編譯成不同類型的輸出,如果你之前使用過 jsx,請注意 Vue 的 JSX 轉換方式與 React 不同,因此你不能在 Vue 中使用 React 的 jsx 轉換法,以下是一些明顯的區別:

  • 可以使用 HTML attritube (例如class、for) 作為 props ,不需使用 classNamehtmlFor
  • 傳遞子素件給元件(例如slot)的方式不同

Vue 也支援 TSX,使用的時候要在 tsconfig.json 中設置 "jsx": "preserve"

JSX Type Inference

與轉換類似,Vue 與 jsx 也需要不同的類型定義,目前 Vue 會在全域範圍自動註冊 Vue 的 JSX 類型。
全域註冊的 jsx 與其他同樣需要 jsx 的 library 一起使用時可能會引起衝突(特別是 React)。從 3.3 開始,Vue 利用 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:

<div> <div v-if="ok">yes</div> <span v-else>no</span> </div>

h():

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>

h():

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

h():

h( 'button', { onClick(event) { /* ... */ } }, 'click me' )

jsx:

<button onClick={(event) => { /* ... */ }} > click me </button>

事件修飾符

.passive.capture.once,可以用駝峰式命名將他們接在事件名後面
h():

h('input', { onClickCapture() { /* 捕捉模式中的监听器 */ }, onKeyupOnce() { /* 只触发一次 */ }, onMouseoverOnceCapture() { /* 单次 + 捕捉 */ } })

jsx:

<input onClickCapture={() => {}} onKeyupOnce={() => {}} onMouseoverOnceCapture={() => {}} />

其他的事件或是按鍵修飾符,可以用 withModifiers 函式
h():

import { withModifiers } from 'vue' h('div', { onClick: withModifiers(() => {}, ['self']) })

jsx:

<div onClick={withModifiers(() => {}, ['self'])} />

元件

在為元件創建 vnode 時,第一個傳給 h() 的參數就是元件的定義,這代表你使用渲染函式就不用再註冊元件了
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> ) }

不論是什麼類型的文件,只要你導入的是有效的 Vue 元件,h 就能正常運作。

動態函式也可以直接使用:
h():

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 () => [ // 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:

// 默认插槽 <div>{slots.default()}</div> // 具名插槽 <div>{slots.footer({ text: props.message })}</div>

傳遞插槽

向元件傳遞子元素和項元素傳遞子元素的方式其實不盡相同,我們需要傳遞的是 slot 函式或是一個包含 slot 函式的物件而非陣列,普通的渲染函式可以回傳的 slot 函式都能回傳,並且都會在子元件被拜訪時被轉換成 vnode 陣列。

h():

// 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:

// 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():

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 來解決

模板引用

在 Composition API 中,模板引用通過 ref() 本身作為一個屬性傳遞給 vnode 來創建

import { h, ref } from 'vue' export default { setup() { const divEl = ref() // <div ref="divEl"> return () => h('div', { ref: divEl }) } }

函式式元件

函式式元件是元件的替代形式,它们不具有自己的狀態。它们的行為類似於纯函式:接收 props,輸出虚擬節點 (vnode)。它们在渲染时不會創建組件實例(即没有 this),也不會使用一般的元件生命周期勾子。

要創建函式式元件,我们使用普通的函式,而不是 options object。這個函式實際上就是元件的渲染函式。

基本上就和 setup() hook 相同:

function MyComponent(props, { slots, emit, attrs }) { // ... }

常見的元件配置在這裡都不能使用,不過可以使用 props 和 emits

MyComponent.props = ['value'] MyComponent.emits = ['click']

如果這個 props 沒被定義就會像 attrs 一樣包含所有的 attritube,除非有指定 props 選項,不然他們不會被處理成駝峰式命名。

對於有明確 props 的 函式式元件,屬性傳透的原理和一般的元件一樣,但是對於沒有明確指定 props 的 函式式元件只會有 classstyleonXXX默認會從中繼承,在這兩種情況下都可以將 inheritAttrs 設成 false 來禁用屬性繼承。

為他們標註類型(TS)

函式式元件可以根據他們是否有命名來標註類型,在 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)}> &nbsp; {props.message}&nbsp;{' '} </button> ) } FComponent.props = { message: { type: String, required: true } } FComponent.emits = { sendMessage: (value) => typeof value === 'string' }