Vue3 的 render 函数与 JSX 写法
1. 概述
-
template 写法可以满足 90% 的需求, template 模板经过 vue 的模板编译后会生成渲染函数
-
render 函数 + TSX 可以补充 Vue 模板的灵活性
-
通过使用 JSX/TSX 可以更加容易的写渲染函数
-
但同时会导致 vue 的模板编译阶段中静态提升等其他运行时优化手手段失效, 从而产生性能损失, 不过一般情况下无须担心
-
可以使用 h() / createVNode() 手写渲染函数, 可以理解 h() 是对用户友好版本的 createVNode()
渲染管线
-
编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
-
挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
-
更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
关于JSX/TSX 造成的性能损失
参见 template 编译为渲染函数: template-explorer.vuejs.org
模板写法
<div>
<div>foo</div>
<div>bar</div>
<div>{{ dynamic }}</div>
</div>
模板写法编译后
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "foo", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "bar", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]))
}
JSX写法
function myRenderer() {
return (
<div>
<div>foo</div>
<div>bar</div>
<div>{dynamic.value}</div>
</div>
)
}
JSX写法编译后
function myRenderer() {
return _createVNode(
"div",
null,
[
_createVNode("div", null, [_createTextVNode("foo")]),
_createVNode("div", null, [_createTextVNode("bar")]),
_createVNode("div", null, [dynamic.value])
]);
}
2. render 函数的基本用法
当组合式 API 与模板一起使用时,setup() 钩子的返回值是用于暴露数据给模板
当我们使用渲染函数时,可以直接把渲染函数返回,也可以将渲染函数写在 render 选项中, 如下面代码所示
<script lang="tsx">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'TestRenderView',
setup() {
const text = ref('vue in render')
return {
text
}
},
render() {
// 在 render 中使用 setup 中声明的变量需要使用 this, 此处写法和 vue2 类似
// 同时 setup 中需要将对应的变量暴露出去
// 此处可用的所有属性参考: https://cn.vuejs.org/api/component-instance.html
return <div>{this.text}</div>
}
})
</script>
3. TSX 的基本应用
TestSetupView.vue
<script lang="tsx">
import MyChildComp from '@/components/MyChildComp.vue';
import { defineComponent, ref } from 'vue';
import { Input as AInput } from 'ant-design-vue'
export default defineComponent({
name: 'TestSetupView',
setup() {
const comp = ref()
const text = ref('vue in render')
const inputVal = ref()
const options = ref([
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
])
const divRefs = ref<any[]>([])
function handleMyEvent(data: string) {
alert('received data: '+ data)
}
return () => (
// <> </> 是空标签写法
<>
{/* 此处需要使用 .value 获取 text 的值 */}
<div>{text.value}</div>
<div>使用v-model: { inputVal.value }</div>
<div><AInput v-model:value={inputVal.value} style={{width: '300px'}}></AInput></div>
<hr></hr>
{/* v-for, 行内样式, 循环中的 ref 设置 */}
{options.value.map((item: any) => (
<div
key={item.id}
style={{ fontSize: '20px', color: 'red' }}
ref={(ref: any) => { divRefs.value.push(ref) }}
>
{item.value}
</div>
))}
<button onClick={() => console.log(divRefs.value)}>show divRefs</button>
<hr></hr>
{/* 绑定 ref 不需要写 .value */}
<MyChildComp class="myComp" ref={comp} text="abc" number={123} onMyEvent={handleMyEvent}>
{/* 插槽写法 */}
{{
default: () => (<div>我是默认插槽</div>),
someName: () => (<div>我是具名插槽</div>),
scopeSlot: (data: string) => (<div>作用域插槽: {data}</div>)
}}
</MyChildComp>
<button onClick={() => comp.value.someFn('some data')}>do something</button>
</>
)
},
})
</script>
MyChildComp.vue
<script lang="tsx">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'MyChildComp',
// props 添加复杂类型参考: https://cn.vuejs.org/guide/typescript/options-api.html#typing-component-props
props: {
text: {
type: String,
require: true,
},
number: {
type: Number,
}
},
// emits 添加 Ts 标注类型参考: https://cn.vuejs.org/guide/typescript/options-api.html#typing-component-emits
emits: ['myEvent'],
setup(props, { slots, expose, attrs, emit }) {
const { text, number } = { ...props }
function someFn(data: string) {
alert('我是传进来的数据: ' + data)
}
// 用于暴露方法/属性
expose({ someFn })
return () => (
// 属性穿透写法
<div {...attrs}>
<div>组件传参</div>
<div>text: {text}</div>
<div>number: {number}</div>
<hr></hr>
{/* 默认插槽 */}
{slots.default && <div>{slots.default()}</div>}
{/* 具名插槽 */}
{slots.someName && <div>{slots.someName()}</div>}
{/* 作用域插槽 */}
{slots.scopeSlot && <div>{slots.scopeSlot(text)}</div>}
<button onClick={() => emit('myEvent', 'Hello')}>emit data</button>
</div>
)
},
})
</script>
4. 一些拓展
工厂组件函数写法: 一般常见在组件库源码中
MyTsxComp.tsx
import { defineComponent } from 'vue';
// 解锁更加灵活的组件传参方式
export const buildMyTsxComp = (str: string, configObj: any) => defineComponent({
name: 'MyTsxComp',
setup() {
return () => (
<>
<div>{str}</div>
{/* 条件渲染 v-if, 两种写法均可 */}
{ configObj ? <div>configObj.id: {configObj.id}</div> : null }
{ configObj && <div>configObj.num: {configObj.num}</div> }
</>
)
}
})
TestSetupView2.vue
<script setup lang="tsx">
import { buildMyTsxComp } from '@/components/MyTsxComp';
import { createVNode, h, ref, createApp } from 'vue';
const MyComp = buildMyTsxComp('abc', { id: 'def', num: 123 })
const myString = ref('function comp')
// 此处函数签名与 setup 函数签名相同
// 注意函数式组件不会保存任何状态!!!
// 此外最好是个纯函数, 下面这个就不是个纯函数, 因为依赖了外部的 myString
// 详见: https://cn.vuejs.org/guide/extras/render-function.html#functional-components
function FunctionComp(props: any) {
return (
<>
<div>我是函数式组件</div>
<div>闭包传入的变量: {myString.value}</div>
{props.num && <div>{props.num}</div>}
</>
)
}
function FunctionComp2() {
// h 是 createVNode 的简化版, 支持函数重载, 使用起来比 createVNode 更方便
// 但不能设置相关优化属性, 比如 patchFlag, dynamicProps
// 详见: https://cn.vuejs.org/guide/extras/rendering-mechanism.html#static-hoisting
return h(
'div', // 可写 html 或组件, 详见 vue-core, packages\runtime-core\src\h.ts
{ class: 'colorRed' },
[
'使用了 h',
createVNode( // 详见 vue-core, packages\compiler-core\src\ast.ts 其中的 createVNodeCall
'div',
{ style: 'font-size: 26px' },
['使用了 createVNode'],
1, // 设置 patchFlag 用于标记节点类型, 详见 vue-core, packages\shared\src\patchFlags.ts
)
]
)
}
// 此处是抛开实际用处不谈的一些花活...
const compInstance = ref<any[]>([])
compInstance.value.push({
id: 1,
comp: FunctionComp
})
compInstance.value.push({
id: 2,
comp: FunctionComp2
})
function manualMountComp() {
const compVNodes = createApp(FunctionComp, { props: 123 })
// 此处已拿到组件 vnode
const container = document.createElement('div')
compVNodes.mount(container)
// 此处可以已经拿到组件 dom 从而修改已经渲染好的组件
console.log(container)
// 将修改后的组件挂载到 自定义挂载点上
document.querySelector('.my-app')!.appendChild(container)
}
setTimeout(() => {
manualMountComp()
});
</script>
<template>
<h2>使用了组件工厂拿的组件</h2>
<MyComp></MyComp>
<hr>
<!-- 调用函数式组件 -->
<h2>函数式组件</h2>
<FunctionComp :num="789"></FunctionComp>
<hr>
<!-- 使用 h / createVNode -->
<h2>使用 h / createVNode</h2>
<FunctionComp2></FunctionComp2>
<hr>
<!-- 一些花活 -->
<h2>以下是一些花活....</h2>
<template v-for="item of compInstance" :key="item.id">
<component :is="item.comp" :num="91011"></component>
</template>
<hr>
<h2>手动挂载组件</h2>
<div class="my-app"></div>
</template>
<style scoped>
.colorRed {
color: red
}
</style>
如果文章有什么错误欢迎大家指教~
如果文章对你有帮助除了收藏外不妨帮我点个赞~