vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》
有了前面数据驱动、组件化、响应式原理篇的知识储备,(没看过的同学可以戳这:《Vue源码分析系列》)这时我们就有足够的的理论去支持我们研究Vue里面大名鼎鼎的diff
算法了。
_update
diff算法体现在页面的更新过程中,所以我们直接进入_update
查看:
const prevVnode = vm._vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
由于是页面更新过程,所以vm._vnode
是有值的,所以最后调用vm.__patch__(prevVnode, vnode)
。
patch
之前我们在patch的执行过程有提到过最后__patch__
调用的是createPatchFunction
的返回值函数:
/**
* 使用函数柯里化牵引着一些平台化的dom操作
*/
return function patch(oldVnode, vnode, hydrating, removeOnly) {
//没有找到新节点,但是有旧节点,就删除该节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
const insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
//没有找到就节点(也有可能是组件),就代表是个新增节点
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
//判断是不是真实的dom
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
//如果节点相同(节点内部更改)
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
//如果节点不同,为新vnode创建新dom节点,删除旧的dom节点
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode;
} else if (process.env.NODE_ENV !== "production") {
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 " +
", or missing
. Bailing hydration and performing " +
"full client-side render."
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
//将真实的dom转换为一个VNode
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
//真实的dom => #app
const oldElm = oldVnode.elm;
//父节点 => body
const parentElm = nodeOps.parentNode(oldElm);
// create new node
//深度优先创建真实DOM
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)) {
let ancestor = vnode.parent;
//是否可挂载
const patchable = isPatchable(vnode);
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// destroy old node
//在dom上删除老的节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};
前面的我不多赘述了,想知道的可以直接去看我之前的:patch的执行过程。
我们这边直接进入这个判断if (!isRealElement && sameVnode(oldVnode, vnode))
,由于是页面更新过程,所以isRealElement
为false
,之后按照一个算法比较新旧vnode
是否相同,如果相同,说明是vnode内部可能有变化,就调用patchVnode
,进入patchVnode
。
patchVnode
patchVnode
这个方法有点长,我一段一段拆开来分析。
前面一些无关紧要的逻辑我直接就略过了,一直到这里:
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
//组件vnode,执行prepatch钩子
i(oldVnode, vnode);
}
判断这个vnode是不是有data.hook
,有的话说明是一个组件vnode(占位符vnode),调用prepatch
hook。这个prepatch
hook等我们把patchVnode
分析完毕后再回来分析。(因为在prepatch
中会递归调用patchVnode
)
继续向下阅读patchVnode
:
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
//如果有data,执行update钩子
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);
}
判断有没有vnode.data
且这个vnode
是一个可挂载的vnode
,(isPatchable
方法感兴趣的童鞋可以去关注下,我觉得挺有意思的),如果是的话就执行update
hook。
继续:
if (isUndef(vnode.text)) {
//新节点不是文本节点
if (isDef(oldCh) && isDef(ch)) {
//新旧节点都存在children就执行updateChildren
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
} else if (isDef(ch)) {
//新节点增加了children
if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(ch);
}
//如果老节点是文本节点,清除DOM上的老节点的文本gggggggggf
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
//新节点做DOM插入
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
//新节点删除了children,删除老的DOM元素
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
//新节点和老节点都没有children,且老节点有文本节点
//删除DOM上的老文本节点
nodeOps.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
//文本节点更新
nodeOps.setTextContent(elm, vnode.text);
}
这部分有很多的判断,这些判断在这里不好描述,我全部写在上面的注释里面了。
需要特别关注的是:
//新旧节点都存在children就执行updateChildren
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
如果新旧节点都存在children
就执行updateChildren
,这个updateChildren
的实现是diff算法里非常复杂的部分,我们后面专开一章节来研究他。
继续向下阅读patchVnode
:
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch)))
i(oldVnode, vnode);
}
最后执行了一个postpatch
hook。
prepatch
hook
回到之前提到过的prepatch
hook,定义在create-component.js
中:
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions;
const child = (vnode.componentInstance = oldVnode.componentInstance);
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
},
调用了updateChildComponent
,进入updateChildComponent
。
updateChildComponent
export function updateChildComponent(
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== "production") {
isUpdatingChildComponent = true;
}
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren.
// check if there are dynamic scopedSlots (hand-written or compiled but with
// dynamic slot names). Static scoped slots compiled from template has the
// "$stable" marker.
const newScopedSlots = parentVnode.data.scopedSlots;
const oldScopedSlots = vm.$scopedSlots;
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
);
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
);
vm.$options._parentVnode = parentVnode;
vm.$vnode = parentVnode; // update vm's placeholder node without re-render
if (vm._vnode) {
// update child tree's parent
vm._vnode.parent = parentVnode;
}
vm.$options._renderChildren = renderChildren;
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject;
vm.$listeners = listeners || emptyObject;
// update props
if (propsData && vm.$options.props) {
toggleObserving(false);
const props = vm._props;
const propKeys = vm.$options._propKeys || [];
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i];
const propOptions: any = vm.$options.props; // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm);
}
toggleObserving(true);
// keep a copy of raw propsData
vm.$options.propsData = propsData;
}
// update listeners
listeners = listeners || emptyObject;
const oldListeners = vm.$options._parentListeners;
vm.$options._parentListeners = listeners;
updateComponentListeners(vm, listeners, oldListeners);
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context);
vm.$forceUpdate();
}
if (process.env.NODE_ENV !== "production") {
isUpdatingChildComponent = false;
}
}
平时我们更新组件主要是因为组件中props
的变化,所以我们这边主要关注props
的逻辑:
const props = vm._props;
const propKeys = vm.$options._propKeys || [];
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i];
const propOptions: any = vm.$options.props; // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm);
}
这里对props[key]
进行了赋值,同时会触发props
的setter
,会通知props
中的依赖(组件的render watcher
),进行更新页面,同时又会触发组件的_update
,重复触发了以上所有逻辑。
总结
这篇文章主要是分析了diff算法的浅层,浅层次的diff算法逻辑还是比较简单的,更加深层次的updateChildren
我们放在下一篇文章中去解析。
你可能感兴趣的:(vue,源码)