前言
Vnode也称虚拟node节点,是对真实元素的抽象。诞生的背景是因为前端在很长一段时间通过直接操作Dom来达到修改视图,随着项目庞大,维护就变成一个问题。那换个角度想如果把真实Dom树抽象成为一棵以JS语法构建的抽象,然后通过修改抽象树的结构来转换成真实的Dom来重新渲染到视图。
所谓虚拟DOM,是一个用于表示真实 DOM 结构和属性的 JavaScript 对象,这个对象用于对比虚拟 DOM 和当前真实 DOM 的差异化,然后进行局部渲染从而实现性能上的优化。在Vue.js 中虚拟 DOM 的 JavaScript 对象就是 VNode。
如何生成虚拟节点
定义
既然是虚拟 DOM 的作用是转为真实的 DOM,那这就是一个渲染的过程。所以我们看看 render 方法。在之前的学习中我们知道了,vue 的渲染函数 _render
方法返回的就是一个 VNode 对象。而在 initRender
初始化渲染的方法中定义的 vm._c
和 vm.$createElement
方法中,createElement
最终也是返回 VNode 对象。所以 VNode 是渲染的关键所在。
var VNode = function VNode (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag; //当前节点的标签名
this.data = data; //当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
this.children = children; //当前节点的子节点,是一个数组
this.text = text; //当前节点的文本
this.elm = elm; //当前虚拟节点对应的真实dom节点
this.ns = undefined; //当前节点的名字空间
this.context = context; //当前节点的编译作用域
this.fnContext = undefined; //函数化组件作用域
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key; //节点的key属性,被当作节点的标志,用以优化
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined; //当前节点的父节点
this.raw = false; //简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false; //是否为注释节点
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory; //异步工厂方法
this.asyncMeta = undefined; //异步Meta
this.isAsyncPlaceholder = false; //是否为异步占位
};
实例
<div class="test">
<span class="demo">hello,VNode</span>
</div>
VNode树
{
tag: 'div'
data: {
class: 'test'
},
children: [
{
tag: 'span',
data: {
class: 'demo'
}
text: 'hello,VNode'
}
]
}
<div id="app">
<span>{{ message }}</span>
<ul>
<li v-for="item of list" class="item-cls">{{ item }}</li>
</ul>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'hello Vue.js',
list: ['jack', 'rose', 'james']
}
})
</script>
VNode树
{
"tag": "div",
"data": {
"attr": { "id": "app" }
},
"children": [
{
"tag": "span",
"children": [
{ "text": "hello Vue.js" }
]
},
{
"tag": "ul",
"children": [
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "jack" }
]
},
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "rose" }
]
},
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "james" }
]
}
]
}
],
"context": "$Vue$3",
"elm": "div#app"
}
在看VNode的时候小结以下几点:
- 所有对象的
context
选项都指向了 Vue 实例。 elm
属性则指向了其相对应的真实 DOM 节点。- DOM 中的文本内容被当做了一个只有
text
没有tag
的节点。 - 像class、id等HTML属性都放在了
data
中
createEmptyVNode
创建一个空VNode节点
var createEmptyVNode = function (text) {
if ( text === void 0 ) text = '';
var node = new VNode();
node.text = text;
node.isComment = true;
return node
};
void的运用 void 运算符 对给定的表达式进行求值,然后返回undefined。void 运算符通常只用于获取 undefined的原始值,一般使用void(0)(等同于void 0)。在上述情况中,也可以使用全局变量undefined来代替(假定其仍是默认值)。 undefined的定义
- 未初始化的变量
- 访问对象不存在的属性
- 数组越界访问
- 缺省的函数参数
- 调用没有任何返回值的函数
- void 操作符
createTextVNode
创建一个文本节点
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
cloneVNode
克隆一个VNode节点
function cloneVNode (vnode) {
var 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
}
createElement
创建元素。
createElement用来创建一个虚拟节点。当data上已经绑定ob的时候,代表该对象已经被Oberver过了,所以创建一个空节点。tag不存在的时候同样创建一个空节点。当tag不是一个String类型的时候代表tag是一个组件的构造类,直接用new VNode创建。当tag是String类型的时候,如果是保留标签,则用new VNode创建一个VNode实例,如果在vm的option的components找得到该tag,代表这是一个组件,否则统一用new VNode创建。
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
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)
}
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
if (isDef(data) && isDef((data).__ob__)) {
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()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
{
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
);
}
}
// support single function children as default scoped slot
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);
}
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
if (isDef(data) && isDef(data.nativeOn)) {
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))) {
// component
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
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()
}
}
function isDef (v) {
return v !== undefined && v !== null
}
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
createComponent
创建组件节点
function createComponent (
Ctor,
data,
context,
children,
tag
) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base;
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
{
warn(("Invalid Component definition: " + (String(Ctor))), context);
}
return
}
// async component
var asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {};
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor);
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}
// extract props
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
var slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data);
// return a placeholder vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
检测变化并更新视图patch
Vue的渲染过程(无论是初始化视图还是更新视图)最终都将走到 _update
方法中,再来看看这个 _update
方法。
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render 初始化渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates 更新渲染
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
不难发现更新试图都是使用了 vm.__patch__
方法,我们继续往下跟。
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
...
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 当前 vnode 未定义、老的 VNode 定义了,调用销毁钩子
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element 老的 VNode 未定义,初始化
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
//当前 VNode 和老 VNode 都定义了,执行更新操作
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node 修改已有根节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// 已有真实 DOM 元素,处理 oldVnode
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
// 挂载一个真实元素,确认是否为服务器渲染环境或者是否可以执行成功的合并到真实 DOM 中
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// 调用 insert 钩子
// inserted:被绑定元素插入父节点时调用
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode
} else {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 不是服务器渲染或者合并到真实 DOM 失败,创建一个空节点替换原有节点
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
// 替换已有元素
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);
// create new node
// 创建新节点
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
// update parent placeholder node element, recursively
// 递归更新父级占位节点元素
if (isDef(vnode.parent)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// destroy old node
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
// 调用 insert 钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
}
createElm
这个方法创建了真实 DOM 元素
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
重点关注代码中的方法执行。代码太多,就不贴出来了,简单说说用途。
cloneVNode
用于克隆当前 vnode 对象。createComponent
用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。在重新激活组件中使用 insert 方法操作 DOM。nodeOps.createElementNS
和nodeOps.createElement
方法,其实是真实 DOM 的方法。setScope
用于为 scoped CSS 设置作用域 ID 属性createChildren
用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法,如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(…) 在真实 DOM 中插入文本内容。insert
用于将元素插入真实 DOM 中。
所以,这里的 nodeOps 指的肯定就是真实的 DOM 节点了。最终,这些所有的方法都调用了 nodeOps 中的方法来操作 DOM 元素。
这里顺便科普下 DOM 的属性和方法。下面把源码中用到的几个方法列出来便于学习:
- appendChild: 向元素添加新的子节点,作为最后一个子节点。
- insertBefore: 在指定的已有的子节点之前插入新节点。
- tagName: 返回元素的标签名。
- removeChild: 从元素中移除子节点。
- createElementNS: 创建带有指定命名空间的元素节点。
- createElement: 创建元素节点。
- createComment: 创建注释节点。
- createTextNode: 创建文本节点。
- setAttribute: 把指定属性设置或更改为指定值。
- nextSibling: 返回位于相同节点树层级的下一个节点。
- parentNode: 返回元素父节点。
- setTextContent: 获取文本内容(这个未在w3school中找到,不过应该就是这个意思了)。
patchVnode
看过了创建真实 DOM 后,我们来学习虚拟 DOM 如何实现 DOM 的更新。这才是虚拟 DOM 的存在意义 —— 比对并局部更新 DOM 以达到性能优化的目的。 看代码~
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 新旧 vnode 相等
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
var elm = vnode.elm = oldVnode.elm;
// 异步占位
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
// 如果新旧 vnode 为静态;新旧 vnode key相同;
// 新 vnode 是克隆所得;新 vnode 有 v-once 的属性
// 则新 vnode 的 componentInstance 用老的 vnode 的。
// 即 vnode 的 componentInstance 保持不变。
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return
}
var i;
var data = vnode.data;
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新旧 vnode 都有 children,且不同,执行 updateChildren 方法。
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
}
}
updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
直接看源码可能比较难以捋清其中的关系,我们通过图来看一下。
首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。
索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode
在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。
当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。
如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。
如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。
当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。
到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。
1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。
2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。