快完事咯!
简单看了下patch函数,虽然不长,但是实际上很长很长,慢慢来吧,
首先来个总览:
// line-5250
// oldVnode => 原生DOM节点
// vnode => 虚拟DOM
// hydrating => undefined
// removeOnly => false
// 后面两个undefined
function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 判断是否存在虚拟DOM
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
// 空挂载或作为组件
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue, parentElm, refElm);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
} else {
if (isRealElement) {
// 判断是否SSR
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
// hydrating这个到底是啥????
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
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.'
);
}
}
// 两个都不是 新建空虚拟DOM
oldVnode = emptyNodeAt(oldVnode);
}
// 此处oldElm => div | parentElm$1 => body
var oldElm = oldVnode.elm;
var parentElm$1 = nodeOps.parentNode(oldElm);
createElm(
vnode,
insertedVnodeQueue,
// BUG:当节点处于leaving transition状态中不插入
oldElm._leaveCb ? null : parentElm$1,
// 获取相邻节点
nodeOps.nextSibling(oldElm)
); if (isDef(vnode.parent)) {
// component root element replaced.
// update parent placeholder node element, recursively
var ancestor = vnode.parent;
while (ancestor) {
ancestor.elm = vnode.elm;
ancestor = ancestor.parent;
}
if (isPatchable(vnode)) {
for (var i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent);
}
}
} if (isDef(parentElm$1)) {
removeVnodes(parentElm$1, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
} invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
函数总共有6个形参,其中有3个为undefined,内部总共主要是条件判断,按照顺序跑下来。
一、
由于不是SSR,也没有hydrating这个参数,所以会直接创建一个空的虚拟DOM作为oldVnode,如下:
// line-4778
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
二、
接下来到了createElm方法,代码如下:
// line-4802
// vnode => 之前的虚拟DOM
// insertedVnodeQueue => [] => 空数组
// parentElm => body => 父元素
// refElm => 空白文本 => 节点的相邻元素
// nested => undefined
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
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)) {
// error检测
{
if (data && data.pre) {
inPre++;
}
// 这个错误经常见!
if (!inPre &&
!vnode.ns &&
!(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
// ns???
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 ("development" !== 'production' && data && data.pre) {
inPre--;
}
}
// 注释
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);
}
}
函数进入就会调用createComponent进行判断,这个函数暂时看不太懂,一个分支都没有进,直接返回了undefined:
// line-4855
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
// 获取属性
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */ , parentElm, refElm);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
下一步是跳到createElement函数,nodeOps中的方法都是自封装的DOM操作方法,超级简单。
// line-4599
function createElement$1(tagName, vnode) {
var elm = document.createElement(tagName);
if (tagName !== 'select') {
return elm
}
// 对select做特殊处理
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple');
}
return elm
}
可以看,这个方法也就是创建一个DOM节点并返回,对select会做特殊处理,于是vnode.elm也就是个普通的DOM节点。
接下来是setScope函数(),该函数目的是给根节点添加一个特殊的ID,避免css样式影响其他模块。
这个东西我就熟悉啦,触发形式是这样:
渲染结果是这样:
这样可以保证所有的样式都是独立的,就算名字一样,后面添加的随机字符串可以保证唯一。
// line-4958
function setScope(vnode) {
var i;
var ancestor = vnode;
while (ancestor) {
if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
nodeOps.setAttribute(vnode.elm, i, '');
}
ancestor = ancestor.parent;
}
// for slot content they should also get the scopeId from the host instance.
if (isDef(i = activeInstance) &&
i !== vnode.context &&
isDef(i = i.$options._scopeId)) {
nodeOps.setAttribute(vnode.elm, i, '');
}
}
函数判断虚拟DOM是否存在_scopeId,然后对每一个节点添加对应的Id,这里没有,所以就会跳过去。
接下来调用createChildren方法,从名字也能看出来是对子节点的处理。实际上,也是对子节点或者纯文本进行递归处理:
// line-4927
function createChildren(vnode, children, insertedVnodeQueue) {
// 存在复杂子节点
if (Array.isArray(children)) {
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true);
}
}
// 子节点为纯文本
else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
}
}
第一条分支处理DOM子节点,直接递归。本例子节点只是一个纯文本:
因此,重新跳入createElm方法后,会直接进入第三个分支,即:
// line-4802
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
// 属性获取
// ...
if (isDef(tag)) {
// 处理节点类型
} else if (isTrue(vnode.isComment)) {
// 处理注释
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
这个分支只有两条语句,第一条会创建一个文本节点给elm属性,第二条负责插入。
第一个DOM操作只是简单调用document.createTextNode方法,直接看第二个函数吧:
// line-4915
// parent => div => 父节点
// elm => #text => 本节点
// ref => null => 相邻节点
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (ref.parentNode === parent) {
//
nodeOps.insertBefore(parent, elm, ref);
}
}
// parent.appendChild(elm)
else {
nodeOps.appendChild(parent, elm);
}
}
}
第二个也是DOM操作!好吧,比较简单,看看就懂了。
子节点处理完后,接下来处理本节点的属性,相关函数为invokeCreateHooks:
// line-4944
// vnode => 虚拟DOM
// insertedVnodeQueue => []
function invokeCreateHooks(vnode, insertedVnodeQueue) {
// cbs为工具对象
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) {
i.create(emptyNode, vnode);
}
if (isDef(i.insert)) {
insertedVnodeQueue.push(vnode);
}
}
}
比较重要的是这里的cbs对象,在外部函数运行最开始有进行初始化,如下:
// line-4762
function createPatchFunction(backend) {
var i, j;
var cbs = {};
// line-6968
// var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
var modules = backend.modules;
var nodeOps = backend.nodeOps; for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
} // tools fn // return fn
}
这个对象来源于多个对象的拼接,包含大量虚拟DOM的操作方法,内容如图:
从名字可见,该对象整合了创建、摧毁、移除、更新等功能库,此处仅用create来创建,内部函数如图:
invokeCreateHooks函数一开始遍会遍历此数组,依次传参执行,参数为空vnode与当前的vnode。
这部分比较长,下次再讲。