# 【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' } ```