渲染函数也称为**render**
函数,作用就是把虚拟 DOM 转换为真实 DOM 的渲染行为。
编译与渲染过程
那么 Vue 是怎么把组件中的<template>
一步一步转换为真实 DOM 的呢?
例如下面这段代码:
<template>
<div id="box">
<div class="article-box">
<h1 class="title">{{ title }}</h1>
<p>
<span>{{ author }}</span> - <span>{{ dateTime }}</span>
</p>
<p class="content">{{ content }}</p>
</div>
</div>
</template>
1、引入组件
⬇️ ⬇️ ⬇️
2、提取<template>
模版转换为字符串
⬇️ ⬇️ ⬇️
3、编译模版字符串为 AST 树
组件模版中不只有 HTML 能解析的东西也有不认识的东西,例如 v-if、插值表达式等等,浏览器无法解析这些属性,这就需要一种方式把这些浏览器不认识的属性「优化掉」,所以就需要把模版字符串转换为 AST 树。
AST 树是一种自定义的语法树,目的就是把浏览器不认识的东西都解析优化掉,转换为一种对应的 JS 逻辑且从字符串中移除掉,最后剩下一些浏览器能认识的东西。
⬇️ ⬇️ ⬇️
4、把 AST 树转换为虚拟 DOM 树
虚拟 DOM 就是对真实 DOM 的一种描述。
为什么需要虚拟 DOM ?
例如下面这段 DOM 结构:
<span>123</span>
如果没有虚拟 DOM ,我们想要更改<span>
的内容一般都使用:
span.innerText = "123";
从逻辑角度来说,这句代码执行后 DOM 肯定会进行更新,但其实这是没有必要的!所以我们可以进行对比判断是否需要继续更新:
span.innerText !== "123" && (span.innerText = "123");
但是这样需要频繁的去操作获取 DOM。
可以通过虚拟 DOM 的去描述这个 DOM :
const vDOM = {
tag: 'span',
attrs: {},
children: "123"
}
当我们需要更改<span>
的内容时,我们只需要去和之前的<span>
内容进行对比,如果有变化再进行更新。
再比如这段 HTML:
<span>
<span>
123
<span>234</span>
</span>
</span>
当我们去更改的时候:
span.innerText = `
<span>
123
<span>345</span>
</span>`;
但其实内容 123 并没有进行更改,如果不使用虚拟 DOM 就会把整个<span>
都进行更新。而虚拟 DOM 会逐一的进行对比,只会去更新发生了变化的 DOM。
:::info
所以这就是虚拟 DOM 的好处,虚拟 DOM 是为了尽最大的能力去减少对 DOM 的操作。
:::
⬇️ ⬇️ ⬇️
5、虚拟 DOM 树转换为真实的 DOM 树
⬇️ ⬇️ ⬇️
6、当数据发生变化时,虚拟 DOM 树会进行对比,然后打一个「补丁 patch」
DOM 会使用 Diff 算法把新旧 DOM 进行对比,然后形成 patch 对象,该 patch 对象记录了对比后差异,然后根据 patch 对象去更新相应的内容。
7、更新真实的 DOM
以上 7 步就是 Vue 组件的编译、渲染过程!!!
h 函数
在 Vue2 的时候想要创建一个 Vue 的应用实例的时候会传递一个render
函数,render
函数通过 h 函数把根组件进行解析。
new Vue({
render: (h) => h(App)
}).$mount('#app')
文章的开头,我们就说了render
函数就是把虚拟 DOM 渲染为真实的 DOM,那么 h 函数到底是干啥的???**h()**
是 hyperscript 的简称,意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是**createVnode()**
,意思是创建虚拟节点。
但当你需要多次使用渲染函数时,一个简短的名字h()
会更省力。
:::info
所以h()
就是用来创建 vNode (Virtual Node) 虚拟节点的,多个虚拟节点组合就会形成虚拟 DOM 树,虚拟 DOM 就是对真实 DOM 的描述。
:::
所以,当使用h()
创建 DOM 的时候,直接省略了把模版字符串转换为 AST 再转换为虚拟 DOM 的过程,因为h()
会直接返回一个虚拟 DOM!!!
创建 vNode
Vue 提供了一个h()
函数用于创建 vnodes:
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
"This is test content."
)
h()
函数的使用也可以更加灵活:
来源于 Vue 文档:https://cn.vuejs.org/guide/extras/render-function.html#creating-vnodes
// 除了类型必填以外,其他的参数都是可选的
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')])
:::warning
⚠️ 注意
当h()
的子节点是一个元素节点的时候,必须再使用h()
进行包裹!!!
:::
h('div', null, [
h('span', null, 'hello'),
h('span', null, 'world'),
"This is text content."
])
得到的 vnode 为如下形式:
const vnode = h('div', { id: 'foo' }, [])
vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null
例如我想对开头那段 HTML 用h()
进行表达:
<template>
<div id="box">
<div class="article-box">
<h1 class="title">{{ title }}</h1>
<p>
<span>{{ author }}</span> - <span>{{ dateTime }}</span>
</p>
<p class="content">{{ content }}</p>
</div>
</div>
</template>
<script>
import { h } from "vue";
export default{
data() {
return {
title: "This is title.",
author: "Xiechen",
dateTime: "2023/05/23",
content: "This is content."
};
},
// 我们可以使用 render 选项来声明渲染函数
// render() 函数可以访问同一个 this 组件实例
render() {
return h(
"div",
{
id: "box"
},
h(
"div",
{
class: "article-box"
},
[
h(
"h1",
{
class: "title"
},
this.title
),
h("p", null, [
this.author + "-",
h("span", { class: "date-time" }, this.dateTime)
]),
h("p", { class: "content" }, this.content)
]
)
);
}
}
</script>
我们在<template>
中写的任何东西都可以使用h()
来表达,但是h()
不是虚拟节点,而是开发层面对虚拟节点的一种描述,也就是对<template>
的描述,h()
返回的是一个虚拟节点对象,然后组成虚拟节点书,最后组成虚拟 DOM 树。
:::warning
⚠️ 注意
在单文件 SFC 中,如果 render 选项也同时存在于该组件中,template 将被忽略。
如果应用的根组件不含任何 template 或 render 选项,Vue 将会尝试使用所挂载元素的 innerHTML 来作为模板。
:::
h 函数使用的案例
来源于 Vue 文档 https://cn.vuejs.org/guide/extras/render-function.html#render-function-recipes
1、一般写法
import { h } from "vue";
const vNode = h("h1", null, "This is title");
当h()
没有 props/attrs,默认第二个参数为 children。
import { h } from "vue";
const vNode = h("h1", "This is title")
const vNode2 = h("h1", [
'"This is title"',
h('span', null, "author")
])
但是更推荐在没有第二个参数的情况下,使用null
或者{}
进行占位,这样可以避免一定的混乱!
2、vNode 必须是唯一的
多个 children,不要使用同一个虚拟节点,这样会重复使用 vNode,虽然能渲染,但是更新的时候会存在问题。
function render(){
const vNode = h("li", null, "123");
// 错误的行为!!!
return h('div', null, [
vNode,
vNode
])
}
function render(){
// 推荐这样操作
return h('ul', null, Array.from({length:6}).map((el, index)=>{
return h('li',null,index)
}))
}
3、当你用h()
渲染一个全局组件的时候,但是又拿不到该组件的实例,你需要使用resolveComponent()
来把组件名称进行解析。
import { createApp } from "vue";
const app = createApp(App);
// 注册全局组件
app.component("MyTest", {
render: () => h("div", null, "This is MyTest content.")
});
app.mount("#app");
resolveComponent()
接收一个组件名称:
import { h, resolveComponent } from "vue";
function render(){
return h(resolveComponent("MyTest"));
}
如果你能导入了 Vue 的组件,你可以直接把组件的实例传递给h()
,这意味着使用渲染函数时不再需要注册组件了:
import TestComp from "./components/TestComp.vue";
function render(){
return h(TestComp);
}
4、动态组件
动态组件需要使用resolveDynamicComponent()
进行解析。
import { h, defineAsyncComponent, resolveDynamicComponent } from "vue";
const Comp1 = defineAsyncComponent(() => import("./components/Comp1.vue"));
const Comp2 = defineAsyncComponent(() => import("./components/Comp2.vue"));
export default {
data() {
return {
isOpen: true,
currentComponentName: "Comp1"
};
},
computed: {
currentComponent() {
return this.currentComponentName === "Comp1" ? Comp1 : Comp2;
}
},
render(){
const dComponent = resolveDynamicComponent(this.currentComponent);
return h("div", null, [
h(dComponent),
h("button", {
onClick: () => {
this.currentComponentName = this.currentComponentName === "Comp1" ? "Comp2" : "Comp1"
}
}, "Switch Component")
]);
}
}
5、模拟 v-if 的操作h()
无法使用v-if
指令,所以需要使用原生的 JS 去模拟
function render(){
if(true){
return h('h1',null,"This is title.")
}
return h('h2',null,"This is article.")
}
6、模拟 v-show 的操作
function render(){
return h(
"h1",
{
style: {
display: this.isOpen ? "none" : ""
}
},
"This is title."
);
}
7、模拟 v-for 的操作
function render(){
return h(
'ul',
this.items.map(({ id, text }) => {
return h('li', { key: id }, text)
})
);
}
8、模拟 v-on 和 v-model
import { h } from "vue";
import VModel from "./components/VModel.vue";
function render(){
return h(VModel, {
username: "default username",
password: "default password",
"onUpdate:username": (value) => console.log(value),
"onUpdate:password": (value) => console.log(value),
onSubmit: (value) => console.log(value)
});
}
:::warning
⚠️ 注意
必须使用on
开头且小驼峰的形式,来接收子组件emit
出来的事件。
:::
<script>
import { h } from "vue";
export default {
name: "VModel",
props: {
username: String,
password: String
},
emits: ["update:username", "update:password", "submit"],
render() {
return h("div", null, [
h("input", {
type: "text",
value: this.username,
placeholder: "Username",
onInput: ($event) => this.$emit("update:username", $event.target.value)
}),
h("input", {
type: "password",
value: this.password,
placeholder: "Password",
onInput: ($event) => this.$emit("update:password", $event.target.value)
}),
h("button", {
onClick: () => this.$emit("submit")
},'Submit')
]);
}
};
</script>
或者来个简易的版本:
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
对于.passive
、.capture
和.once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
h('input', {
onClickCapture() {
/* 捕捉模式中的监听器 */
},
onKeyupOnce() {
/* 只触发一次 */
},
onMouseoverOnceCapture() {
/* 单次 + 捕捉 */
}
})
9、使用插槽
<script>
import { h } from "vue";
export default {
name: "VSlot",
render() {
return h("div", null, [
// 通过 this.$slot().name 来获取插槽的内容
h("h1", { class: "title" }, this.$slots.default()),
h("p", { class: "author" }, this.$slots.author()),
h(
"p",
{ class: "content" },
// 给插槽传递数据,也就是作用域插槽
this.$slots.content({
content: "This is content"
})
)
]);
}
};
</script>
import { h } from "vue";
import VSlot from "./components/VSlot.vue";
function render(){
return h(VSlot, null, {
default: () => "This is title",
author: () => "Xiechen",
content: (props) => h("p", null, props.content)
});
}
10、使用自定义指令
可以使用withDirectives()
将自定义指令应用于 vnode:
import { h, withDirectives } from 'vue'
// 自定义指令
const pin = {
mounted() { /* ... */ },
updated() { /* ... */ }
}
// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(
h('div'),
[
[pin, 200, 'top', { animate: true }]
]
)
当一个指令是以名称注册并且不能被直接导入时,可以使用resolveDirective()
函数来解决这个问题。
import { h, withDirectives, resolveDirective } from 'vue'
// <div v-pin:top.animate="200"></div>
const pin = resolveDirective('pin');
const vnode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
更多用法请看:
渲染函数 & JSX | Vue.js