# VDOM
虚拟DOM(VDOM),从 Vue2 开始使用,借鉴了snabbdom。
VDOM通过一个js对象去描述了DOM的节点,所以创建的成本比较小。
# VNode
在VDOM中通过VNode来代表一个DOM节点
src/core/vdom/vnode.js
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor(
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
this.fnContext = undefined;
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false;
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false;
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
}
get child(): Component | void {
return this.componentInstance;
}
}
VNode就是一个类,定义了很多属性,其中关键的就是tag,data,children,key。
- tag:节点类型
div等 - data: 节点数据
attr,style,class等 - children: 子节点
# 创建空节点或者注释节点: createEmptyVNode('')
export const createEmptyVNode = (text: string = "") => {
const node = new VNode();
node.text = text;
node.isComment = true;
return node;
};
# 创建文本节点:createTextVNode('hello')
export function createTextVNode(val: string | number) {
return new VNode(undefined, undefined, undefined, String(val));
}
# 克隆一个节点: cloneVNode(vnode)
export function cloneVNode(vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned;
}
# 创建 VNode: createElement
src/core/vdom/create-element
Vue主要通过createElement来创建一个VNode
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType);
}
封装了一层使传入的参数更加的灵活,最终调用的_createElement
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== "production" &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + "Always create fresh vnode data objects in each render!",
context
);
return createEmptyVNode();
}
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
if (!tag) {
return createEmptyVNode();
}
if (
process.env.NODE_ENV !== "production" &&
isDef(data) &&
isDef(data.key) &&
!isPrimitive(data.key)
) {
if (!__WEEX__ || !("@binding" in data.key)) {
warn(
"Avoid using non-primitive value as key, " +
"use string/number value instead.",
context
);
}
}
if (Array.isArray(children) && typeof children[0] === "function") {
data = data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
let vnode, ns;
if (typeof tag === "string") {
let Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
if (
process.env.NODE_ENV !== "production" &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== "component"
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
);
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
);
} else if (
(!data || !data.pre) &&
isDef((Ctor = resolveAsset(context.$options, "components", tag)))
) {
vnode = createComponent(Ctor, data, context, children, tag);
} else {
vnode = new VNode(tag, data, children, undefined, undefined, context);
}
} else {
vnode = createComponent(tag, data, context, children);
}
if (Array.isArray(vnode)) {
return vnode;
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns);
if (isDef(data)) registerDeepBindings(data);
return vnode;
} else {
return createEmptyVNode();
}
}
_createElement一共有 5 个参数:
- context: 上下文
- tag: 标签名或者组件名
- data: vnode 数据
- children: 子节点
- normalizationType: 子节点规范的类型
_createElement流程比较复杂:
- 首先判断了
data不能是响应式数据,否则返回空的VNode - 接着处理动态组件的
is,将is的值赋给tag,如果tag为空,直接返回空的VNode - 保证
data.key是一个原始值 - 处理作用域插槽
- 规范子节点
- 创建 VNode,会区分
string和component,返回VNode
# 规范子节点
- 主要通过
normalizationType来判断需要怎么规范子节点。
# SIMPLE_NORMALIZE
export function simpleNormalizeChildren(children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children);
}
}
return children;
}
simpleNormalizeChildren主要是把多维数组转成低一个维度的数组,通过apply方法,将参数通过数组的形式传递。
[VNode,[VNode]] => [VNode,VNode]
# ALWAYS_NORMALIZE
export function normalizeChildren(children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined;
}
normalizeChildren会做一些判断:
- 如果是基础类型,返回一个文本节点的
VNode数组 - 如果是数组,调用
normalizeArrayChildren函数处理 - 其他的情况直接返回
undefined
处理normalizeArrayChildren
function normalizeArrayChildren(
children: any,
nestedIndex?: string
): Array<VNode> {
const res = [];
let i, c, lastIndex, last;
for (i = 0; i < children.length; i++) {
c = children[i];
if (isUndef(c) || typeof c === "boolean") continue;
lastIndex = res.length - 1;
last = res[lastIndex];
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ""}_${i}`);
// merge adjacent text nodes
// 如果前一个数组的元素是文本节点,当前last也是文本节点,会合并。last为已有数组的最后一项,c[0]会push,也就是会放到last的后面,最后去除c[0]
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
c.shift();
}
res.push.apply(res, c);
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + c);
} else if (c !== "") {
res.push(createTextVNode(c));
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text);
} else {
// default key for nested array children (likely generated by v-for)
if (
isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)
) {
c.key = `__vlist${nestedIndex}_${i}__`;
}
res.push(c);
}
}
}
return res;
}
主要的过程就是遍历children,根据不同的数据类型做处理:
- 如果是数组,会将二维数组转化成一维数组,一般处理
v-for。
const children = [
[{ tag: "div" }],
[[{ tag: "p" }, { tag: "p" }, { tag: "p" }]],
];
// 转化为:
const children = [
[{ tag: "div" }],
[{ tag: "p" }],
[{ tag: "p" }],
[{ tag: "p" }],
];
2.如果是基础类型,就是调用createTextVNode将结果push到到结果数组中。
- 其他的情况,如果都是文本节点,则合并,如果
children是一个列表并且列表还存在嵌套的情况,则根据nestedIndex去更新它的key。