Skip to content

渲染函数

渲染函数为你提供了完全的 JavaScript 编程能力来构建虚拟 DOM。在大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,你真的需要用到 JavaScript 完全的编程能力,这时渲染函数就派上用场了。

基本用法

渲染函数通过 h() 函数创建虚拟节点 (VNode):

javascript
import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h() 函数

h() 函数的名字来源于 hyperscript,意思是"能生成 HTML (超文本标记语言) 的 JavaScript"。这个名字来源于许多虚拟 DOM 实现默认形成的约定。

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

声明渲染函数

组合式 API

在组合式 API 中,setup() 钩子可以返回一个渲染函数:

javascript
import { ref, h } from 'vue'

export default {
  setup() {
    const count = ref(1)

    // 返回渲染函数
    return () => h('div', count.value)
  }
}

选项式 API

在选项式 API 中,我们可以使用 render 选项:

javascript
import { h } from 'vue'

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

虚拟 DOM 树

Vue 组件树中的每个组件都有一个对应的 VNode 实例,该实例负责描述如何渲染这个组件。

javascript
const vnode = h('div', {
  id: 'app'
}, [
  h('header', [
    h('h1', 'My App')
  ]),
  h('main', [
    h('p', 'Hello World!')
  ])
])

完整示例

让我们看一个完整的示例,创建一个动态标题组件:

javascript
import { h } from 'vue'

export default {
  name: 'DynamicHeading',
  props: {
    level: {
      type: Number,
      required: true
    },
    text: {
      type: String,
      required: true
    }
  },
  render() {
    return h(
      `h${this.level}`, // 标签名
      {}, // props
      this.text // children
    )
  }
}

使用这个组件:

vue
<template>
  <DynamicHeading :level="1" text="Hello World!" />
  <DynamicHeading :level="3" text="Subtitle" />
</template>

渲染插槽

在渲染函数中访问插槽内容可以通过 this.$slots 来实现:

javascript
export default {
  render() {
    // `<div><slot></slot></div>`
    return h('div', this.$slots.default())
  }
}

具名插槽:

javascript
export default {
  render() {
    // `<div><slot name="header"></slot><slot></slot><slot name="footer"></slot></div>`
    return h('div', [
      this.$slots.header(),
      this.$slots.default(),
      this.$slots.footer()
    ])
  }
}

作用域插槽:

javascript
export default {
  render() {
    return h('div', [
      this.$slots.default({
        text: 'hello'
      })
    ])
  }
}

事件处理

在渲染函数中,事件监听器应该以 onXxx 的形式命名:

javascript
export default {
  render() {
    return h('button', {
      onClick: this.handleClick
    }, 'Click me!')
  },
  methods: {
    handleClick() {
      console.log('Button clicked!')
    }
  }
}

事件修饰符:

javascript
export default {
  render() {
    return h('input', {
      onKeyup: {
        handler: this.handleKeyup,
        modifiers: { enter: true }
      }
    })
  }
}

组件和 Props

渲染其他组件:

javascript
import MyComponent from './MyComponent.vue'

export default {
  render() {
    return h(MyComponent, {
      // 传递 props
      someProp: 'hello'
    })
  }
}

条件渲染

在渲染函数中,你可以使用完整的 JavaScript 语法:

javascript
export default {
  props: ['items'],
  render() {
    if (this.items.length) {
      return h('ul', this.items.map(item => {
        return h('li', { key: item.id }, item.name)
      }))
    } else {
      return h('p', 'No items found.')
    }
  }
}

v-model

在渲染函数中实现 v-model:

javascript
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  render() {
    return h('input', {
      value: this.modelValue,
      onInput: (event) => {
        this.$emit('update:modelValue', event.target.value)
      }
    })
  }
}

JSX

如果你更喜欢类似 HTML 的语法,可以使用 JSX:

jsx
export default {
  render() {
    const { level, text } = this
    const Tag = `h${level}`
    
    return <Tag>{text}</Tag>
  }
}

要使用 JSX,你需要安装 @vitejs/plugin-vue-jsx 插件:

bash
npm install @vitejs/plugin-vue-jsx -D

然后在 vite.config.js 中配置:

javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
  plugins: [vue(), vueJsx()]
})

函数式组件

函数式组件是一种定义自身没有任何状态的组件的方式:

javascript
function MyComponent(props, { slots, emit, attrs }) {
  return h('div', `Hello ${props.name}`)
}

// 添加 props
MyComponent.props = ['name']

性能优化

静态提升

对于静态内容,Vue 编译器会自动进行静态提升:

javascript
// 编译器会将这个静态节点提升到渲染函数外部
const hoisted = h('div', 'Static content')

export default {
  render() {
    return h('div', [
      hoisted, // 静态节点
      h('div', this.dynamicContent) // 动态节点
    ])
  }
}

内联组件 Props

避免在每次渲染时创建新的对象:

javascript
// 不好
export default {
  render() {
    return h(MyComponent, {
      style: { color: 'red' } // 每次都创建新对象
    })
  }
}

// 好
const staticStyle = { color: 'red' }

export default {
  render() {
    return h(MyComponent, {
      style: staticStyle // 复用对象
    })
  }
}

何时使用渲染函数

渲染函数适用于以下场景:

  1. 需要完全的编程能力:复杂的条件逻辑
  2. 动态组件名:根据数据动态选择组件
  3. 性能关键场景:避免模板编译开销
  4. 库和框架开发:提供更大的灵活性

下一步

vue study guide - 专业的 Vue.js 学习平台