尝试梳理分析,如有错误,请多多指正,谢谢~
来到最后一章的梳理了,接下来就是分析patch的操作。patch操作就是根据patchObj将当前挂载的DOM真实节点进行操作(增删改),也就是经常说的,将很多次操作合并为一次对DOM的操作。上一节已经通过diff拿到了patchObj,来看看patch是怎么使用这个东西的吧。
patch 函数
先从入口看,通过patch.js看到,调用的是/vdom/patch.js,patch函数实现如下:
function patch(rootNode, patches, renderOptions) {
renderOptions = renderOptions || {}
renderOptions.patch = renderOptions.patch && renderOptions.patch !== patch
? renderOptions.patch
: patchRecursive
renderOptions.render = renderOptions.render || render
return renderOptions.patch(rootNode, patches, renderOptions)
}
可以看到接收三个参数,第一个是真实的DOM,第二个是diff得到的patchObj对象,第三个就是附加的参数。代码里面对参数进行了判断,也就是说patch函数可以是自己传入的patch,但是没传默认使用patchRecursive函数,所以紧跟着我们来看一下这个函数。
patchRecursive 函数
function patchRecursive(rootNode, patches, renderOptions) {
var indices = patchIndices(patches)
if (indices.length === 0) {
return rootNode
}
var index = domIndex(rootNode, patches.a, indices)
var ownerDocument = rootNode.ownerDocument
if (!renderOptions.document && ownerDocument !== document) {
renderOptions.document = ownerDocument
}
for (var i = 0; i < indices.length; i++) {
var nodeIndex = indices[i]
rootNode = applyPatch(rootNode,
index[nodeIndex],
patches[nodeIndex],
renderOptions)
}
return rootNode
}
- 拿出
patchObj里面的下标。上一节说到patchObj的形成过程,最终张这个样子{ a: {...}, 0: {...}, 1: {...}}。a里面对应的是上一个VD Tree,其他数值下标就是当前root的每一个子元素的patchObj。如图。所以indices的值应该类似于这样[0, 1...]。如果这个数组为空,则说明root`下面没有子元素就直接返回了,不需要再做挂载。
function patchIndices(patches) {
var indices = []
for (var key in patches) {
if (key !== "a") {
indices.push(Number(key))
}
}
return indices
}
- 根据之前的
VD Tree和DOM获取到对于的DOM对象的index数组。domIndex函数大致的操作就是将DOM的顺序根据下标升序排序,然后通过DOM和VD Tree进行对比,递归去整理每个元素的子元素等操作。官方的注释如下:
Maps a virtual DOM tree onto a real DOM tree in an efficient manner. We don't want to read all of the DOM nodes in the tree so we use the in-order tree indexing to eliminate recursion down certain branches. We only recurse into a DOM node if we know that it contains a child of interest. 大致意思如下:eyes:: 以一种有效的方式将虚拟DOM树映射到真实的DOM树。 我们不想读取树中的所有DOM节点,所以使用 按顺序树索引,以消除某些分支上的递归。 我们只有在知道一个DOM节点包含一个子节点时才递归到它
var index = domIndex(rootNode, patches.a, indices)
- 根据传入如的
options和默认的判断,拿到顶级元素实例,这里一般是document对象。
var ownerDocument = rootNode.ownerDocument
if (!renderOptions.document && ownerDocument !== document) {
renderOptions.document = ownerDocument
}
- 最后循环
indices数组来根据patchObj来挂载,目光再次转移到applyPatch函数,每次循环都是去执行的这个函数。取出每个子节点的DOM实例,patchObj实例和整个DOM树传入到applyPatch去执行。
for (var i = 0; i < indices.length; i++) {
var nodeIndex = indices[i]
rootNode = applyPatch(rootNode,
index[nodeIndex],
patches[nodeIndex],
renderOptions)
}
根据以上四个步骤,可以看到当前这个函数是将子元素的相关信息拿出来进行了规整,然后在继续往下走。
applyPatch 函数
function applyPatch(rootNode, domNode, patchList, renderOptions) {
if (!domNode) {
return rootNode
}
var newNode
if (isArray(patchList)) {
for (var i = 0; i < patchList.length; i++) {
newNode = patchOp(patchList[i], domNode, renderOptions)
if (domNode === rootNode) {
rootNode = newNode
}
}
} else {
newNode = patchOp(patchList, domNode, renderOptions)
if (domNode === rootNode) {
rootNode = newNode
}
}
return rootNode
}
- 如果传入的
DOM实例为空(也就是一个一个子元素)则直接返回。
if (!domNode) {
return rootNode
}
- 如果
patchObj是一个数组,则循环调用patchOp函数,如果不是就直接调用,这里也就是对应的是子元素是多个和单个的时候。
newNode = patchOp(patchList[i], domNode, renderOptions)
最终调用的是patchOp函数,这个函数就是去真是的操作DOM了。所以最简单也是最核心的东西就里面。
这里的applyPatch的函数通过patchOp操作完DOM后,就可以直接返回得到的rootNode了。
patchOp 函数
/vdom/patch-op.js 文件有 152 行,以后可能会改动吧,只是想表达,这实际操作DOM的函数其实并不复杂,也不能复杂,因为就是去修改和删除DOM等,也就是浏览器提供的操作函数而已。
function applyPatch(vpatch, domNode, renderOptions) {
var type = vpatch.type
var vNode = vpatch.vNode
var patch = vpatch.patch
switch (type) {
case VPatch.REMOVE:
return removeNode(domNode, vNode)
case VPatch.INSERT:
return insertNode(domNode, patch, renderOptions)
case VPatch.VTEXT:
return stringPatch(domNode, vNode, patch, renderOptions)
case VPatch.WIDGET:
return widgetPatch(domNode, vNode, patch, renderOptions)
case VPatch.VNODE:
return vNodePatch(domNode, vNode, patch, renderOptions)
case VPatch.ORDER:
reorderChildren(domNode, patch)
return domNode
case VPatch.PROPS:
applyProperties(domNode, patch, vNode.properties)
return domNode
case VPatch.THUNK:
return replaceRoot(domNode,
renderOptions.patch(domNode, patch, renderOptions))
default:
return domNode
}
}
可以很明确的看到,这里通过switch判断patchObj里面的type值,来完成相应的变更。
这里看几个常用的操作remove、insert和replace操作。
remove
function removeNode(domNode, vNode) {
var parentNode = domNode.parentNode
if (parentNode) {
parentNode.removeChild(domNode)
}
destroyWidget(domNode, vNode);
return null
}
首先拿到该元素的parentNode,然后调用DOM的removeChild方法,将元素删除,很直接。
insert
function insertNode(parentNode, vNode, renderOptions) {
var newNode = renderOptions.render(vNode, renderOptions)
if (parentNode) {
parentNode.appendChild(newNode)
}
return parentNode
}
首先通过renderOptions.render函数创建DOM,render之前赋值过,默认是调用createElement方法,具体是怎么创建的,请看第一节。创建了newNode后,将元素插入到当前等元素位置是上。
replace
function vNodePatch(domNode, leftVNode, vNode, renderOptions) {
var parentNode = domNode.parentNode
var newNode = renderOptions.render(vNode, renderOptions)
if (parentNode && newNode !== domNode) {
parentNode.replaceChild(newNode, domNode)
}
return newNode
}
同样的,先拿到parentNode元素,然后通过creatElement生成先的DOM,再判断是否与之前的 相同,如果不同的话,就直接调用DOM的replaceChild函数替换子元素。
小结
通过例举了三个常用的操作,了解到其实最终都是直接调用DOM提供的一些元素操作方法来完成,也是理所当然的,一定会调用这些,现在可以明白了是怎么调用。到这里 virtual-dom 的流程算是梳理完了,知道了从创建 VD 到最后渲染和更新的具体操作,想自己动手实现一个 virtual-dom 推荐看看下面这文章:segmentfault.com/a/119000001…