virtual-dom 梳理分析【patch函数】

296 阅读5分钟

尝试梳理分析,如有错误,请多多指正,谢谢~

来到最后一章的梳理了,接下来就是分析patch的操作。patch操作就是根据patchObj将当前挂载的DOM真实节点进行操作(增删改),也就是经常说的,将很多次操作合并为一次对DOM的操作。上一节已经通过diff拿到了patchObj,来看看patch是怎么使用这个东西的吧。

patch 函数

先从入口看,通过patch.js看到,调用的是/vdom/patch.jspatch函数实现如下:

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
}
  1. 拿出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
}

patchObj

  1. 根据之前的VD TreeDOM获取到对于的DOM对象的index数组。domIndex函数大致的操作就是将DOM的顺序根据下标升序排序,然后通过DOMVD 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)
  1. 根据传入如的options和默认的判断,拿到顶级元素实例,这里一般是document对象。
var ownerDocument = rootNode.ownerDocument

if (!renderOptions.document && ownerDocument !== document) {
    renderOptions.document = ownerDocument
}
  1. 最后循环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
}
  1. 如果传入的DOM实例为空(也就是一个一个子元素)则直接返回。
if (!domNode) {
    return rootNode
}
  1. 如果 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值,来完成相应的变更。

这里看几个常用的操作removeinsertreplace操作。

remove

function removeNode(domNode, vNode) {
    var parentNode = domNode.parentNode

    if (parentNode) {
        parentNode.removeChild(domNode)
    }

    destroyWidget(domNode, vNode);

    return null
}

首先拿到该元素的parentNode,然后调用DOMremoveChild方法,将元素删除,很直接。

insert

function insertNode(parentNode, vNode, renderOptions) {
    var newNode = renderOptions.render(vNode, renderOptions)

    if (parentNode) {
        parentNode.appendChild(newNode)
    }

    return parentNode
}

首先通过renderOptions.render函数创建DOMrender之前赋值过,默认是调用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,再判断是否与之前的 相同,如果不同的话,就直接调用DOMreplaceChild函数替换子元素。

小结

通过例举了三个常用的操作,了解到其实最终都是直接调用DOM提供的一些元素操作方法来完成,也是理所当然的,一定会调用这些,现在可以明白了是怎么调用。到这里 virtual-dom 的流程算是梳理完了,知道了从创建 VD 到最后渲染和更新的具体操作,想自己动手实现一个 virtual-dom 推荐看看下面这文章:segmentfault.com/a/119000001…

原文地址