跳轉到內容

渲染函式與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() 函式在每個元件中僅呼叫一次,而返回的渲染函式將被多次呼叫。

我們可以使用 render 選項來宣告渲染函式

js
import { h } from 'vue'

export default {
  data() {
    return {
      msg: 'hello'
    }
  },
  render() {
    return h('div', this.msg)
  }
}

render() 函式可以透過 this 訪問元件例項。

除了返回單個 vnode,您還可以返回字串或陣列

js
export default {
  render() {
    return 'hello world!'
  }
}
js
import { h } from 'vue'

export default {
  render() {
    // use an array to return multiple root nodes
    return [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

如果渲染函式元件不需要任何例項狀態,它們也可以直接宣告為函式以簡化程式碼

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 屬性(如 classfor)作為 props - 無需使用 classNamehtmlFor
  • 將子元素傳遞給元件(即插槽)工作方式不同

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>
js
h('div', [this.ok ? h('div', 'yes') : h('span', 'no')])
jsx
<div>{this.ok ? <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>
js
h(
  'ul',
  this.items.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)
jsx
<ul>
  {this.items.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>

在渲染函式中,可以從 this.$slots 中訪問插槽。

js
export default {
  props: ['message'],
  render() {
    return [
      // <div><slot /></div>
      h('div', this.$slots.default()),

      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        this.$slots.footer({
          text: this.message
        })
      )
    ]
  }
}

JSX 等價物

jsx
// <div><slot /></div>
<div>{this.$slots.default()}</div>

// <div><slot name="footer" :text="message" /></div>
<div>{this.$slots.footer({ text: this.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' }, /* ... */)
  }
}
js
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  render () {
    return h(Transition, { mode: 'out-in' }, /* ... */)
  }
}

v-model

在模板編譯期間,v-model指令被展開為modelValueonUpdate: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)
      })
  }
}
js
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  render() {
    return h(SomeComponent, {
      modelValue: this.modelValue,
      'onUpdate:modelValue': (value) => this.$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 })
  }
}

使用選項式API時,模板引用是透過將ref名稱作為字串傳遞給vnode的props來建立的。

js
export default {
  render() {
    // <div ref="divEl">
    return h('div', { ref: 'divEl' })
  }
}

函式式元件

函式式元件是沒有自己狀態的元件的另一種形式。它們像純函式一樣工作:輸入props,輸出vnodes。它們在沒有建立元件例項(即沒有this)的情況下渲染,也沒有通常的生命週期鉤子。

要建立函式式元件,我們使用一個普通函式,而不是選項物件。這個函式實際上是元件的render函式。

函式式元件的簽名與setup()鉤子的簽名相同。

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

由於函式式元件沒有this引用,Vue將把props作為第一個引數傳遞。

js
function MyComponent(props, context) {
  // ...
}

第二個引數,context,包含三個屬性:attrsemitslots。這些分別相當於例項屬性$attrs$emit$slots

對於函式式元件,大多數常規的配置選項都不可用。但是,可以透過將它們作為屬性新增來定義propsemits

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

如果沒有指定props選項,則傳遞給函式的props物件將包含所有屬性,與attrs相同。除非指定了props選項,否則不會將屬性名稱規範化為camelCase。

對於具有顯式props的函式式元件,與正常元件一樣,屬性穿透的工作方式幾乎相同。但是,對於沒有顯式指定props的函式式元件,預設情況下只會從attrs繼承classstyleonXxx事件監聽器。在兩種情況下,都可以將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'
}
渲染函式 & JSX 已載入