虚拟 DOM
虚拟 DOM (Virtual DOM,简称 VDOM),是使用 Javascript 对象来描述 UI 的方式。这个对象会与真实的 DOM 保持同步,下面我会举个例子来说明:
const vnode = {
tag: "div",
props: {
id: "container",
class: "header",
onClick: () => {},
},
children: [
/** 更多 vnode */
],
};
上面的 vnode 就是一个虚拟 DOM,它代表了一个
元素。
createVNode
createVNode方法主要功能就是创造虚拟dom,在介绍之前先看看js中的与、或运算
export const enum ShapeFlags {
ELEMENT = 1, // 元素
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件 向左移1位 00000010
STATEFUL_COMPONENT = 1 << 2, // 普通组件 向左移2位 00000100
TEXT_CHILDREN = 1 << 3, // 孩子是文本 向左移3位 00001000
ARRAY_CHILDREN = 1 << 4, // 孩子是数组 向左移4位 00010000
SLOTS_CHILDREN = 1 << 5, // 组件插槽 向左移5位 00100000
TELEPORT = 1 << 6, // teleport组件 向左移6位 01000000
SUSPENSE = 1 << 7, // suspense组件 向左移7位 10000000
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 组件 00000100 | 00000010 = 00000110
}
xport const enum PatchFlags {
// 动态文本节点
TEXT = 1,
// 2 动态class
CLASS = 1 << 1,
// 4 动态style
STYLE = 1 << 2,
// 8 动态属性,但不好汉class style
PROPS = 1 << 3,
// 16 具有动态key属性,当key改变时,需要进行完整的diff
FULL_PROPS = 1 << 4,
// 32 带有监听事件的节点
HYDRATE_EVENTS = 1 << 5,
// 64 一个不会改变子节点顺序的fragment
STABLE_FRAGMENT = 1 << 6,
// 128 带有key的fragment
KEYED_FRAGMENT = 1 << 7,
// 256 没有key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 512 一个子节点只会进行非props比较
NEED_PATCH = 1 << 9,
// 1024 动态插槽
DYNAMIC_SLOTS = 1 << 10,
// 下面是特殊的,即在diff阶段会被跳过的
// 2048 表示仅因为用户在模板的根级别放置注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态节点,它的内容永远不会改变,不需要进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2
}
我们看到,PatchFlags被定义为十几种的枚举类型,用以更精准的定位diff阶段需要对比节点部分,实现更精准的靶向更新。PatchFlags大致被分为了两类:
- 值大于0,即代表所对应的element在patch阶段,可以进行优化diff
- 值小于0,即代表所对应的element在patch阶段,不需要进行diff
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false,
): VNode {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
type = Comment
}
if (isVNode(type)) {
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag = PatchFlags.BAIL
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 2.x async/functional component compat
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
// class & style normalization.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true,
)
}
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false,
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance,
} as VNode
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) {
currentBlock.push(vnode)
}
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}
h()函数
h() 函数用于辅助创建虚拟 DOM 节点,它是 hypescript 的简称————能生成 HTML (超文本标记语言) 的 JavaScript,它有另外一个名称,叫做 createVnode()。
h()函数接收参数如下:
- type:类型参数,必填。内容为字符串或者 Vue 组件定义。
- propsOrChildren:propsOrChildren参数,非必填。传递内容是一个对象或子节点数组,对象内容包括了即将创建的节点的属性,例如 id、class、style等,节点的事件监听也是通过 props 参数进行传递,并且以 on 开头,以 onXxx 的格式进行书写,如 onInput、onClick 等,节点数组也是个对象数组。
- children:子节点,非必填。内容可以是文本、虚拟 DOM 节点和插槽等等。
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
// 判读参数个数决定后续的创建逻辑
const l = arguments.length
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
// props without children
return createVNode(type, propsOrChildren)
} else {
// omit props
return createVNode(type, null, propsOrChildren)
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
使用方法如下:
import { h } from 'vue'
// 只有 type 参数为必填
h('div')
// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { id: 'vue3' } )
h('div', { class: 'group', innerHTML: 'hello Vue3' })
h('div', { onClick: () =>{} })
// children 为字符串
h('div', { id: 'vue3' }, 'hello Vue3')
// props 参数为空
h('div', 'hello Vue3')
// children 嵌套使用
h('div', ['hello', h('span', 'Vue3')])