简介
VNode是vue用来描述 DOM 的 JavaScript 对象,它在vue中可以描述不同类型的节点,比如元素节点、组件节点等。
示例
我们在html中的dom节点是这样的
<div class="className">Hello World</div>
而在vue中的vnode是这样的
const vnode = {
type: 'div',
props: {
'class': 'className',
},
children: ['Hello World']
}
如果是一个组件,就是这样的:
<div>
<MyComponent></MyComponent>
</div>
const elementVNode = {
tag: 'div',
props: null,
children: [{
tag: MyComponent,
props: null
}]
}
区分VNode类型
在vue的vnode中,新增了一个字段 shapeFlag,它是一个枚举常量。
export const enum ShapeFlags {
/** HTML 或 SVG 标签 普通 DOM 元素 */
ELEMENT = 1,
/** 函数式组件 */
FUNCTIONAL_COMPONENT = 1 << 1,
/** 普通有状态组件 */
STATEFUL_COMPONENT = 1 << 2,
/** 子节点是纯文本 */
TEXT_CHILDREN = 1 << 3,
/** 子节点是数组 */
ARRAY_CHILDREN = 1 << 4,
/** 子节点是插槽 */
SLOTS_CHILDREN = 1 << 5,
/** Teleport */
TELEPORT = 1 << 6,
/** Suspense */
SUSPENSE = 1 << 7,
/** 需要被 keep-alive 的有状态组件 */
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
/** 已经被 keep-alive 的有状态组件 */
COMPONENT_KEPT_ALIVE = 1 << 9,
/** 有状态组件和函数组件都是组件,用 COMPONENT 表示 */
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
如何创建VNode
在看源码之前,有必要提前讲解一个概念:
在3.0中提出了一个新的概率 Block Tree,顾名思义就是块状区域树。为了解决2.0版本一直被人诟病的diff低效问题;
每个block下,都有一个 dynamicChildren ,在vnode / block创建阶段会将当前block子区域内的动态内容收集并填充到dynamicChildren,那么整个render函数执行完毕时,每个block下的动态子代内容就都被收集到各自的dynamicChildren中,在正式diff时就不再需要重新遍历整棵树,只需要比对同级block下dynamicChildren,无差别的深度遍历升级为靶向的同级比较,这就做到了只关注动态内容。
创建 Block
(openBlock(), createBlock(type, props, children .... ))
源码:
// block 栈
const blockStack = []
// 当前处于收集态的仓库
let currentBlock = null
// 用 openBlock 来存储当前block的动态内容收集栈
function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}
这里的 openBlock 一定要在 createBlock 之前调用,要保证在创建子节点前创建一个block收集仓库。
function createBlock(type, props, children, patchFlag, dynamicProps) {
const vnode = createVNode(
type,
props,
children,
patchFlag,
dynamicProps,
true /* isBlock 声明这里是一个block,下面有解释 */
)
// 此时子节点的动态内容已经收集完毕,将收集的内容挂载到对应的block vnode上
vnode.dynamicChildren = currentBlock || []
// 收集完毕后将当前收集仓库做失效处理
closeBlock()
// block作为一个动态块状子区域需要被作为动态子节点收集到父级block对应的仓库中
if (shouldTrack$1 > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
创建普通VNode
我们在挂载原理的文章中会发现有这样的代码:
mount(rootContainer, isHydrate) {
...
const vnode = createVNode(rootComponent, rootProps)
// rootComponent 就是createApp传入的option
}
下面我们来看createVNode的源码:
function _createVNode(
type,
props = null,
children = null,
patchFlag = 0,
dynamicProps = null,
isBlockNode = false
) {
if (isVNode(type)) {
// 已经是一个vnode了。这种情况发生在
// <component :is="vnode"/>
// 请确保在克隆过程中合并引用,而不是覆盖它
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// 如果是一个class组件
if (isClassComponent(type)) {
type = type.__vccOpts
}
// class 和 style 的标准化,处理在vue中 class=['a'] class={} 等多种情况
if (props) {
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 标记vnode的类型信息
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
if (shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
type = toRaw(type)
}
// 这里就是vnode的结构了
const vnode = {
__v_isVNode: true,
['__v_skip' /* SKIP */]: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
}
// 标准化子节点,其实就是整合子元素
normalizeChildren(vnode, children)
// suspense的话需要单独处理
if (shapeFlag & 128 /* SUSPENSE */) {
const { content, fallback } = normalizeSuspenseChildren(vnode)
vnode.ssContent = content
vnode.ssFallback = fallback
}
if (
shouldTrack$1 > 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.
(patchFlag > 0 || shapeFlag & 6) /* 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.
patchFlag !== 32 /* HYDRATE_EVENTS */
) {
// 动态子代节点或子代block收集到父级block中
currentBlock.push(vnode)
}
return vnode
}
总结:
- 判断如果已经是vnode类型了,返回一个clone的vnode;
- 如果是class组件,赋值type;
- 对props和style的标准化处理,因为在vue中class的写法有很多种,例 :class=”[‘a’]” :class={active: true} ;
- 标记vnode的 shapeFlag;
- 制定vnode的数据结构;
- 判断当前动态子节点block是否收集到父级block中;