先贴上官网的介绍:
渲染函数 h()
创建虚拟 DOM 节点 (vnode)。
虚拟DOM(Virtual DOM)
虚拟DOM(Virtual DOM)是一种在前端开发中常用的概念,它并不是浏览器中的实际DOM节点,而是一个JavaScript对象树,用于表示页面中的DOM结构。虚拟DOM节点包含了真实DOM节点的层次结构和属性信息,以及与之相关的数据。
虚拟DOM的工作原理
初始渲染:当应用程序首次加载或数据发生变化时,前端框架会创建一个虚拟DOM树,以描述所需的页面结构和内容。
对比和更新:当数据变化时,前端框架会生成一个新的虚拟DOM树,然后与之前的虚拟DOM树进行比较,找出两者之间的差异。
差异更新:前端框架会计算出需要更新的最小DOM操作,然后将这些操作应用于实际的DOM树。这个过程通常比直接操作实际的DOM树更高效。
类型及详细信息
// 完整参数签名
function h(
type: string | Component,
props?: object | null,
children?: Children | Slot | Slots
): VNode
// 省略 props
function h(type: string | Component, children?: Children | Slot): VNode
type Children = string | number | boolean | VNode | null | Children[]
type Slot = () => Children
type Slots = { [name: string]: Slot }
第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。第二个参数是要传递的 prop,第三个参数是子节点。
当创建一个组件的 vnode 时,子节点必须以插槽函数进行传递。如果组件只有默认槽,可以使用单个插槽函数进行传递。否则,必须以插槽函数的对象形式来传递。
为了方便阅读,当子节点不是插槽对象时,可以省略 prop 参数。
示例:
import { h } from 'vue'
// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })
// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])
创建组件:
import Foo from './Foo.vue'
// 传递 prop
h(Foo, {
// 等价于 some-prop="hello"
someProp: 'hello',
// 等价于 @update="() => {}"
onUpdate: () => {}
})
// 传递单个默认插槽
h(Foo, () => 'default slot')
// 传递具名插槽
// 注意,需要使用 `null` 来避免
// 插槽对象被当作是 prop
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
实现h()渲染函数
我们要实现创建虚拟 DOM 节点 (vnode),得先了解h()的这个函数是直接返回一个vnode类型节点,下面直接通过代码实现:
//h.ts
import { createVNode } from "./vnode";
export const h = (type: any , props: any = null, children: string | Array<any> = []) => {
return createVNode(type, props, children);
};
创建一个vnode需要三个参数
type: any,
props?: any,
children?: string | Array<any>
而一个vnode的属性:
const vnode = {
el: null,
component: null,
key: props?.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
//vnode.ts
import { ShapeFlags } from "@mini-vue/shared";
export { createVNode as createElementVNode }
export const createVNode = function (
type: any,
props?: any,
children?: string | Array<any>
) {
// 注意 type 有可能是 string 也有可能是对象
// 如果是对象的话,那么就是用户设置的 options
// type 为 string 的时候
// createVNode("div")
// type 为组件对象的时候
// createVNode(App)
const vnode = {
el: null,
component: null,
key: props?.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
// 基于 children 再次设置 shapeFlag
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
} else if (typeof children === "string") {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
}
normalizeChildren(vnode, children);
return vnode;
};
export function normalizeChildren(vnode, children) {
if (typeof children === "object") {
// 暂时主要是为了标识出 slots_children 这个类型来
// 暂时我们只有 element 类型和 component 类型的组件
// 所以我们这里除了 element ,那么只要是 component 的话,那么children 肯定就是 slots 了
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// 如果是 element 类型的话,那么 children 肯定不是 slots
} else {
// 这里就必然是 component 了,
vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
}
}
}
// 用 symbol 作为唯一标识
export const Text = Symbol("Text");
export const Fragment = Symbol("Fragment");
/**
* @private
*/
export function createTextVNode(text: string = " ") {
return createVNode(Text, {}, text);
}
// 标准化 vnode 的格式
// 其目的是为了让 child 支持多种格式
export function normalizeVNode(child) {
// 暂时只支持处理 child 为 string 和 number 的情况
if (typeof child === "string" || typeof child === "number") {
return createVNode(Text, null, String(child));
} else {
return child;
}
}
// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
return typeof type === "string"
? ShapeFlags.ELEMENT
: ShapeFlags.STATEFUL_COMPONENT;
}
// ShapeFlags.ts
// 组件的类型
export const enum ShapeFlags {
// 最后要渲染的 element 类型
ELEMENT = 1,
// 组件类型
STATEFUL_COMPONENT = 1 << 2,
// vnode 的 children 为 string 类型
TEXT_CHILDREN = 1 << 3,
// vnode 的 children 为数组类型
ARRAY_CHILDREN = 1 << 4,
// vnode 的 children 为 slots 类型
SLOTS_CHILDREN = 1 << 5
}
一起来看下效果:
import { ref, h } from 'vue'
export default {
props: {
msg:String
},
setup(props) {
const count = ref(1)
// 返回渲染函数
return () => h('div', props.msg + count.value)
}
}