准备
vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
回顾
如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》
__patch__
的定义
pathch定义在platforms/web/runtime/index
中。由于Vue既可以运行在浏览器也可以运行在服务端,所以这里对patch
做了一个兼容性处理:
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
进入./patch
:
patch
这里引入了一些不同平台的dom操作和一些dom属性,并传递给createPatchFuntion
函数,之后将createPatchFuntion
函数的返回值赋值给patch
。
import * as nodeOps from "web/runtime/node-ops"; // 不同平台的一些dom操作
import { createPatchFunction } from "core/vdom/patch";
import baseModules from "core/vdom/modules/index";
import platformModules from "web/runtime/modules/index"; //不同平台的一些dom模块
const modules = platformModules.concat(baseModules);
export const patch: Function = createPatchFunction({ nodeOps, modules });
进入createPatchFuntion
函数:
createPatchFuntion
这个函数的函数体非常长,定义了非常多的辅助函数,这里就不贴了,之后要用到这些辅助函数的时候在进行讲解。
我们直接看createPatchFuntion
函数的返回值吧:
return function patch(oldVnode, vnode, hydrating, removeOnly) {
//somecode...
};
返回值patch()
方法
哎,这个函数的返回值也是一个函数。说明了这里使用了闭包,也就是说,其实__patch__
真正执行的就是这个createPatchFuntion
函数的返回值函数。
那为什么Vue要绕这么一大圈来定义这个patch
呢?为什么不能直接在__patch__
中就定义好这个返回值patch()
中所有的逻辑?
其实这里使用了一种编程思想,函数柯里化。
这样做的好处是,在执行这个返回值patch()
方法时,就不用一直去判断是属于哪个平台的domModules
和DOM操作,因为作用域链的关系,在第一次执行patch
时就已经将对应平台的domModules
和DOM操作全部牵引在自己的作用域链上了。这样就可以做到变量的复用。
我们接下来详细阅读下返回值patch()
的函数体:
/**
* 使用函数柯里化牵引着一些平台化的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 {
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 " +
"<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转换为一个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;
};
代码很多,我们一点一点分析。
先来看函数前两个参数oldVnode
,vnode
,一个是旧的节点,一个是新节点。
我们在core/instance/lifecycle
中可以了解到一个逻辑:
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);
}
- 如果前一次没有渲染过DOM,就说明是第一次渲染,所以
__patch__
第一个参数是$el
,也就是#app
的dom - 如果之前有渲染过DOM,第一个参数就传递上一次渲染的VNode。
也就是说,这个函数中的oldVnode
既有可能是真实的DOM,又有可能是一个VNode类型。知道了这个之后我们继续向下看。
//判断是不是真实的dom
const isRealElement = isDef(oldVnode.nodeType);
if (isRealElement) {
//将真实的dom转换为一个VNode
oldVnode = emptyNodeAt(oldVnode);
}
isDef(oldVnode.nodeType);
判断是不是一个真实的DOM节点,,如果是就执行emptyNodeAt()
,我们来看一下emptyNodeAt()
function emptyNodeAt(elm) {
return new VNode(
nodeOps.tagName(elm).toLowerCase(),
{},
[],
undefined,
elm
);
}
可以看到,patch
把$el
转换成了一个VNode,所以最后oldVnode
都会变成一个VNode。
再往下看:
//真实的dom => #app
const oldElm = oldVnode.elm;
//父节点 => body
const parentElm = nodeOps.parentNode(oldElm);
//深度优先创建真实DOM
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
将之前真实的DOM赋值给oldElm
,父DOM节点赋值给parentElm
,之后调用createElm()
,传入parentElm
和oldElm
的兄弟节点。进入createElm()
:
辅助函数createElm()
/**
* 一个深度优先创建包含子节点的真实dom的方法
* @param {*} vnode
* @param {*} insertedVnodeQueue
* @param {*} parentElm
* @param {*} refElm
* @param {*} nested
* @param {*} ownerArray
* @param {*} index
*/
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;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
//如果tag是个有效值
if (isDef(tag)) {
if (process.env.NODE_ENV !== "production") {
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement(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
);
}
}
//创建一个真实的dom
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
//css scoped相关
setScope(vnode);
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree);
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
createChildren(vnode, children, insertedVnodeQueue);
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
} else {
//如果有子节点,就递归创建子节点的真实dom
//深度优先的插入节点方法
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
//将子节点插入本节点
insert(parentElm, vnode.elm, refElm);
}
if (process.env.NODE_ENV !== "production" && 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);
}
}
代码还是很长,一段一段看:
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
if (isDef(tag)) {
if (process.env.NODE_ENV !== "production") {
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement(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
);
}
}
//创建一个真实的dom
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
//css scoped相关
setScope(vnode);
先尝试着调用createComponent
检查是不是一个组件,如果是组件就执行createComponent
里面的逻辑,如果不是就继续向下。
之后检查tag
是不是一个有效值,如果是有效值再判断是不是一个未知组件,如果是,就抛出警告。如果不是就先给这个VNode创建一个真实的DOM节点。继续:
//如果有子节点,就递归创建子节点的真实dom
//深度优先的插入节点方法
createChildren(vnode, children, insertedVnodeQueue);
//将子节点插入本节点
insert(parentElm, vnode.elm, refElm);
调用createChildren()
看名称像是创建子DOM节点用的方法。然后再调用insert
看名字像是把子节点插入刚刚创建的DOM节点。
不说了,先进入createChildren()
的身体:
辅助函数createChildren()
这个方法的逻辑倒也不复杂,说白了就是判断children
如果是个数组的话就递归调用createElm()
。
function createChildren(vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(children);
}
for (let i = 0; i < children.length; ++i) {
createElm(
children[i],
insertedVnodeQueue,
vnode.elm,
null,
true,
children,
i
);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(
vnode.elm,
nodeOps.createTextNode(String(vnode.text))
);
}
}
辅助函数insert()
这个方法就是调用原生DOM在父节点中插入子节点,没啥好说的。
/**
* 在父节点中插入子节点
* @param {*} parent
* @param {*} elm
* @param {*} ref
*/
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
结合上面两个createChildren()
和insert()
先后调用来看,createElm()
其实是一个深度优先地创建真实DOM节点的方法。
好了,我们回到createElm
继续向下看:
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);
}
- 如果发现
vnode
有isComment
标记,也就是说是个注释节点,就创建真实DOM的注释节点。 - 如果其他条件都不满足,就说明是个文本节点,于是就创建真实DOM的文本节点。
到这里,整一个createEle
就大概分析完毕了,当然,这里面还有一些关于Vue组件的判断我没提到,这部分之后我们专门开一个篇章来阅读Vue组件的源码是怎么写的。
好了,我们在回到返回值patch
函数,接下去看:
//在dom上删除老的节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};
}
这里使用了原生DOM的删除节点的方法,将老的节点删除了,如果你在这之前打一个debugger
的话,你会发现老节点和新节点同时存在于DOM树上,执行这边的方法之后,老节点就被删除了。
最后返回了一个渲染后的真实的DOM元素。
至此,__patch__
阅读完毕