快完事咯!
简单看了下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 ' + ', or missing . 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。
这部分比较长,下次再讲。