为什么要学习渲染函数和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 组件挂载时会发生如下几件事:
- 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
- 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
- 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
理解了上图,就可以开始看渲染函数了。
手写渲染函数相当于上图黄色框框,因为可以完全利用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)
}
}
}