Vue渲染—深入VNode(h函数、JSX、render函数)
最近要面试,来复习总结一下vue渲染相关的知识。
h 函数
h 函数会返回一个 vnode(虚拟节点)
h 函数底层是基于 createVnode
创建虚拟节点的
- 优势:跳过了模版的编译
- 缺点:写起来不方便(可以用 jsx 去代替)
不过对于写一些弹窗之类的,需要挂载到 #app
根节点外面的,用 h 函数会比较方便。
template 编译过程:parser -> ast -> transform -> render
const vnode = h('div', '111')
console.log(vnode)
// {
// type: "div",
// props: null,
// children: "111",
// }
渲染:
- 普通组件
<template><vnode />
</template><script setup>import { h } from 'vue'const vnode = h('div', '111')
</script>
- 动态组件
<component>
可以接收一个 vnode,渲染到页面
<template><component :is="vnode" />
</template><script setup lang="ts">
import { h, createVNode } from 'vue'
const vnode = h('div', '111')
h 函数的写法(参数):
// 写法1
const vnode = h('div', '333')
// 写法2 第一个参数可以传递一个组件
import A from './A.vue'
const vnode = h(A)
// 写法3 第二个参数可以传递多个子节点
const vnode = h('div', [h('span', 'hello'), h('span', 'world')])
// 写法4 第二个参数可以传递props
const vnode = h('div', { style: { color: 'red' } }, '333')
// 写法5 插槽
import { h, createVNode, useSlots } from 'vue'
const slots = useSlots()
const vnode = h('div', slots.default()) // slots.default() 返回一个vnode数组
补充:
import A from './A.vue'
console.log(A)
当我们去打印组件的时候,会有一个 render
属性,是一个函数,这个函数会返回一个 vnode,也就是 A.render()
调用后返回的就是 vnode。
关于渲染响应式数据:
vnode 会正常渲染,但是响应式数据变了,不会触发 rerender,但是 watch
可以监听到数据变化
因为当前的环境是 setup 的环境,没有被 effect 包裹,所以响应式数据不会响应式
// ⚠️注意:响应式数据不会响应式,因为响应式数据需要effect包裹
let num = ref(0)
const vnode = h('div', num.value)
setTimeout(() => {num.value++
}, 1000)
watchEffect(() => {console.log(num.value)
})
而下面的这种写法,就会触发 rerender(采用函数 () => h('div', num.value)
)
因为当组件在渲染的时候,会帮我们调用 vnode
函数,调用函数的环境就是一个effect
变成函数可以理解为是一个组件(函数式组件),就不是一个 vnode 了
let num = ref(0)
const vnode = () => h('div', num.value)setTimeout(() => {num.value++
}, 1000)
函数式组件:
上面的例子就是一个函数式组件,可以接收props
,类型为 FunctionalComponent
,返回一个 vnode
<template><Vnode :count="1"><div>hello</div></Vnode>
</template><script setup lang="ts">
import { h, type FunctionalComponent } from 'vue'
const Vnode: FunctionalComponent<{ count: number }> = (props, ctx) => {// ctx.attrs => props => { count: 1 }// ctx.slots => { default: () => [h('div', 'hello')] } // vctx.slots.default():node数组// ctx.emitreturn h('div', [props.count, ctx.slots.default()])
}
</script>
JSX
写过 react 的对 JSX 应该不陌生,这里也不过多赘述
在.vue 文件或.jsx/.tsx 文件中的基础 JSX 用法
import { defineComponent } from 'vue'export default defineComponent({setup() {const greeting = 'Hello, Vue with JSX!'return () => (<div><h1>{greeting}</h1><p>This is rendered using JSX.</p></div>)}
})
defineComponent
是 vue 提供的一个辅助函数,为 ts 提供更好的类型支持。对于
<script setup>
语法,默认已经是类型推导的,因此通常不需要显式使用defineComponent
。
函数式组件写法也可以:
// A.tsx
import type { FunctionalComponent } from "vue"export const Vnode: FunctionalComponent<{ count: number }> = (props, ctx) => {return <div><div>{props.count}</div></div>
}
与 h
函数的关系:
JSX
是一种创建 VNode 的语法糖,最终转化为 h()
调用。
这意味着你可以选择是直接使用 h()
还是使用更直观的 JSX
语法。两者本质上是一样的——都是为了创建 VNode。
render 函数
模板编译后的 render 函数
在 vue 中我们平时使用最多的写法就是template
模版,模版的本质就是 render
函数的语法糖。
render
函数作为组件的渲染入口,返回的就是虚拟节点(vnode)
- vue3:
在 vue3 中可以用
setup
函数返回一个render
函数
export default {setup() {return () => h('h1', 'hello world')// return () => <h1>hello world</h1>}
}
- vue2:
// 示例:render函数返回vnode
export default {render(h) {return h('div', 'hello world')}
}
Vue 包里的 render 函数
import { render } from 'vue'
这个 render
和 模板编译后的 render
函数作用是不一样的。
- 作用:将虚拟节点渲染到真实 DOM 容器中
- 参数:
(vnode, container)
- 虚拟节点和容器元素 - 调用时机:手动控制何时渲染/更新/卸载
- 用途:手动挂载组件、动态渲染等
import { h, render } from 'vue'const vnode = h('div', { class: 'container' }, 'Hello World')
const container = document.getElementById('app')
// 渲染到页面
render(vnode, container)// 更新渲染(传入新的 vnode)
const newVnode = h('div', { class: 'container updated' }, 'Updated Content')
render(newVnode, container)// 卸载组件(传入 null)
render(null, container)
实际应用场景:
- 手动挂载应用(类似 createApp)
import { h, render } from 'vue'
import App from './App.vue'const appVNode = h(App)
const container = document.getElementById('app')
render(appVNode, container)
使用
createApp
(推荐)createApp(App).mount('#app')
- 动态渲染组件
import { h, render } from 'vue'function showMessage(message: string) {const vnode = h('div',{class: 'message',onClick: () => {render(null, container) // 点击关闭}},message)const container = document.createElement('div')document.body.appendChild(container)render(vnode, container)
}
通常开发中使用 createApp().mount()
更方便,底层就是基于 render
实现的。