入门Vue3使用渲染函数、h函数

3,224 阅读5分钟

为什么要学习渲染函数和jsx?

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

确实,在开发工作中,我们绝大多数情况都会使用template来书写组件页面代码,只有极少的情况会用到渲染函数,比如具有高度动态的逻辑(可参考vue2的示例:v2.cn.vuejs.org/v2/guide/re… ),或是我之前遇到的——需要在自指令中动态使用Element Plus的组件。

所以,学会了渲染函数,我们在碰到问题时又多了一种解决途径。

Vue3的渲染机制

虚拟DOM

虚拟DOM的概念,各位估计已经倒背如流😄,咱们虚拟DOM效率虽然不是最高,但是能减轻开发者的工作呀~ 🐟

记住下面两个概念,后面还会提到

挂载 mount

一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。

更新 patch

如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。

渲染管线

从高层面的视角看,Vue 组件挂载时会发生如下几件事:

  1. 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
  2. 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
  3. 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

image.png

理解了上图,就可以开始看渲染函数了。

手写渲染函数相当于上图黄色框框,因为可以完全利用js的编程能力,所以它会比模板template更灵活。

渲染函数

h()函数

渲染函数的关键就是h()函数,用于创建vnode,相当于Vue2的createElement。

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

声明渲染函数

组合式API 无须显式地声明render,在setup()中可以直接把渲染函数返回。

// 无须template部分,就会在页面显示一个div。
import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

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

选项式API使用 render 选项来声明渲染函数:

import { h } from 'vue'

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

setup语法糖中使用渲染函数

官网所给的组合式API使用渲染函数的例子都是在setup()中返回渲染函数。如果想要在setup语法糖中使用应该如何书写呢?试验多次的写法如下:

<template>
  <hd />
</template>
<script lang="tsx" setup>
import { h } from 'vue'

// 返回一个组件hd
const hd = h(
  'div',
  Array.from({ length: 20 }).map(() => {
    return h('p', 'hi')
  })
)
</script>

我没找到怎样写才能够像setup()那样不用写template,还请各位不吝赐教!

如果用render(),可以挂载到一个指定的DOM上,但是这种写法用在页面感觉不太合适。

js文件中使用渲染函数

前几天做了一个自定义指令v-tooltip——在元素上添加Element Plus的tooltip效果

Vue2中,使用element UI的组件来做类似的自定义指令,我们可以使用Vue.extend,但Vue3移除了这个方法,所以最后使用渲染函数实现动态使用Element的tooltip组件。

不管是什么类型的文件,只要从中导入的是有效的 Vue 组件,h 就能正常运作。

首先引入需要的element plus组件

import { ElTooltip, ElTag } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'

然后使用h函数

const vnode = h(
        ElTooltip,
        { content: message, placement, effect },
        h(QuestionFilled, { style: { width: '16px' } })
      )

最后使用render把h函数生成的vnode挂载到对应的dom

render(vnode, dom)

完整指令代码如下:

import { DirectiveBinding, h, render } from 'vue'
import { ElTooltip, ElTag } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'

export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const message = binding.value.message
    const placement = binding.value.placement || 'top'
    const effect = binding.value.effect || 'light'
    const position = binding.value.position || 'left'
    if (binding.value.message) {
      const vnode = h(
        ElTooltip,
        { content: message, placement, effect },
        h(QuestionFilled, { style: { width: '16px' } })
      )

      const dom = document.createElement('span')
      if (position === 'left') el.prepend(dom)
      else el.append(dom)

      render(vnode, dom)
    }
  }
}