diff算法代码详解

105 阅读5分钟

前言

根据前面介绍的图解diff算法,现在对里面的内容进行代码实现

前置

vnode

我对它的见解是:它是一个对象,是对dom元素的一种描述,为了方便对dom的操作所规定的一种数据规范。后续的一切逻辑都是基于这种数据规范进行的

很随意的描述一个dom,不就这样吗

{
    "sel": "li",//标签名称
    "element": {},//对应的真实dom
    "children": [],//子元素集合
    "attr": {//绑定的属性
        "key": "A"
    },
    "text": "A"//标签内的文本
}

将真实dom转成vnode对象

//传递dom上的属性就返回一个对其真实dom的描述对象
const createVnode = (sel, element, children, text, attr) => {
        return {
            sel, element, children, attr, text
        }
    }

有这么简单?加上考虑到子元素的情况

const getElementAttr = (node) => {
        // 获取所有属性
        const attributes = node.attributes
        const res = {}
        // 遍历所有属性
        for (let i = 0; i < attributes.length; i++) {
            let attr = attributes[i]
            res[attr.name] = attr.value
        }
        return res
    }
  const realNodeToVnode = (node) => {
        const tag = node.tagName.toLowerCase()
        const children = node.children
        const txt = node.innerText
        const vnode = createVnode(tag, node, children, txt, getElementAttr(node))
        vnode.children = Array.from(children).map(item => realNodeToVnode(item))
        return vnode
    }

实际操作一下; 页面上渲染的内容如下

<ul id="box">
        <li key="A">A</li>
        <li key="B">B</li>
        <li key="C">C</li>
        <li key="D">
            <span key="D-1">D-1</span>
            <span key="D-2">D-2</span>
            <span key="D-3">D-3</span>
            <span key="D-4">D-4</span>
        </li>
</ul>

将上面的dom转为vnode

const box = document.querySelector("#box")
const res = realNodeToVnode(box)
debugger

结果如下,将真实dom转换成了 简易的一个vnode

image.png diff算法就是对比两个vnode之间的差异,核心就是尽可能的复用旧的 下面是代码实践。可以打开图解diff算法,对照着下面代码看

1.拿到新旧vnode

这里简单写个h函数

    const h = (sel, attr, content) => {
        if (typeof content === "string") return createVnode(sel, undefined, [], content, attr)
        else if (Array.isArray(content)) {
            return createVnode(sel, undefined, content, undefined, attr)
        }
    }

页面已经渲染的dom元素

<ul id="box">
        <li key="A">A</li>
        <li key="B">B</li>
        <li key="C">C</li>
        <li key="D">D</li>
</ul>

生成一个vnode作为将要更新到页面的dom对象

    const vnode4 = h('ul', {}, [
        h('li', { key: "B" }, 'B'),
        h('li', { key: "D" }, 'D'),
        h('li', { key: "C" }, "C"),
        h('li', { key: "A" }, 'A'),
        h('li', { key: "E" }, 'E'),
    ])

这里声明一个patch函数来接收一下这两个vnode

  • 如果两个元素的标签都不一样就完全以新的为准,旧的就不用了
  • patchNode函数来对两个vnode进一步深入的对比
  • 我这里其实写的很简易,细节就不写了,只看核心
    const patch = (oldNode, newNode) => {
        if (!oldNode.sel) {
            // 传进来的非虚拟dom
            oldNode = realNodeToVnode(oldNode)
        }
        if (oldNode.sel !== newNode.sel) {//标签都不一样
            const newRes = createElment(newNode)
            let p = oldNode.element.parentNode
            p.insertBefore(newRes.element, oldNode.element)
            p.removeChild(oldNode.element)
        }
        else {
            patchNode(oldNode, newNode, oldNode.element)
        }
    }
const box = document.querySelector("#box")
patch(box, vnode4)

patchNode分步骤看,操作步骤中我先省去递归调用,理清楚只有一个层级的流程再说

  • 先简单写个函数sameNode
    const sameNode = (node, nodeTar) => {
        let flag = true
        if (node.attr.key && nodeTar.attr.key) {
            flag = node.attr.key === nodeTar.attr.key
        }
        return node.text === nodeTar.text && node.sel === nodeTar.sel && flag
    }
  • 再简单写个createElment函数,这个用于将vnode创建为真实dom,我觉得这里不应该递归创建所有元素
  • 因为diff是同级对比,你把它还没有对比的子元素都创建好了,剩下的怎么玩?
const createElment = (vnode) => {
        const parentNode = document.createElement(vnode.sel)
        vnode.text && (parentNode.innerText = vnode.text)
        if (vnode.children && Array.isArray(vnode.children) && vnode.children.length > 0) {
            vnode.children.forEach(item => {
                parentNode.appendChild(createElment(item).element)
            })
        }
        vnode.element = parentNode
        return vnode
}

旧头->新头

if (sameNode(oldNodeLIst[oldStart], newNodeList[newStart])) {
     newNodeList[newStart]['element'] = oldNodeLIst[oldStart]['element']
     oldNodeLIst[oldStart]['element'] = null
     ++newStart
     ++oldStart
}

旧尾->新尾

 else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newEnd])) {
         newNodeList[newEnd]['element'] = oldNodeLIst[oldEnd]['element']
         oldNodeLIst[oldEnd]['element'] = null
         --newEnd
         --oldEnd
}

旧头->新尾

else if (sameNode(oldNodeLIst[oldStart], newNodeList[newEnd])) {
     newNodeList[newEnd]['element'] = oldNodeLIst[oldStart]['element']
     const oldStartEl = oldNodeLIst[oldStart]['element']
     const oldendEl = oldNodeLIst[oldEnd]['element'].nextSibling
     parentNode.insertBefore(oldStartEl, oldStartEl)
    oldNodeLIst[oldStart]['element'] = null
    ++oldStart
    --newEnd
}

旧尾->新头

 else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newStart])) {
      newNodeList[newStart]['element'] = oldNodeLIst[oldEnd]['element']
      parentNode.insertBefore(oldNodeLIst[oldEnd]['element'], oldNodeLIst[oldStart]['element'])
     oldNodeLIst[oldEnd]['element'] = null
     ++newStart
     --oldEnd
}

以上都不满足?

  • 查找是否存在相同节点
const res = oldNodeLIst.findIndex(item => sameNode(newNodeList[newStart], item))
if (res !== -1) {
    newNodeList[newStart]['element'] = oldNodeLIst[res]['element']
    if (res !== 0) {
        parentNode.insertBefore(oldNodeLIst[res]['element'], oldNodeLIst[oldStart]['element'])
    }                  
     oldNodeLIst[res]['element'] = null
    ++newStart
}

还是找不到可以复用的?那就自己创建

const newRes = createElment(newNodeList[newStart])
parentNode.insertBefore(newRes.element, oldNodeLIst[oldStart]['element'])
++newStart

完整代码

 const createVnode = (sel, element, children, text, attr) => {
        return {
            sel, element, children, attr, text
        }
    }
    const h = (sel, attr, content) => {
        if (typeof content === "string") return createVnode(sel, undefined, [], content, attr)
        else if (Array.isArray(content)) {
            return createVnode(sel, undefined, content, undefined, attr)
        }
    }
    const createElment = (vnode) => {
        const parentNode = document.createElement(vnode.sel)
        vnode.text && (parentNode.innerText = vnode.text)
        if (vnode.children && Array.isArray(vnode.children) && vnode.children.length > 0) {
            vnode.children.forEach(item => {
                parentNode.appendChild(createElment(item).element)
            })
        }
        vnode.element = parentNode
        return vnode
    }
    const sameNode = (node, nodeTar) => {
        let flag = true
        if (node.attr.key && nodeTar.attr.key) {
            flag = node.attr.key === nodeTar.attr.key
        }
        return node.text === nodeTar.text && node.sel === nodeTar.sel && flag
    }
    const patchNode = (oldNode, newNode, parentNode) => {
        if (newNode.children && newNode.children.length > 0 && oldNode.children && oldNode.children.length > 0) {
            let newNodeList = newNode.children
            let oldNodeLIst = oldNode.children
            let oldStart = 0
            let newStart = 0
            let oldEnd = oldNode.children.length - 1
            let newEnd = newNode.children.length - 1
            while (newEnd >= newStart) {
                //   旧头->新头
                if (sameNode(oldNodeLIst[oldStart], newNodeList[newStart])) {
                    newNodeList[newStart]['element'] = oldNodeLIst[oldStart]['element']
                    patchNode(oldNodeLIst[oldStart], newNodeList[newStart], newNodeList[newStart]['element'])
                    oldNodeLIst[oldStart]['element'] = null
                    ++newStart
                    ++oldStart
                }
                // 旧尾->新尾
                else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newEnd])) {
                    newNodeList[newEnd]['element'] = oldNodeLIst[oldEnd]['element']
                    patchNode(oldNodeLIst[oldEnd], newNodeList[newEnd], newNodeList[newEnd]['element'])
                    oldNodeLIst[oldEnd]['element'] = null
                    --newEnd
                    --oldEnd
                }
                // 旧头->新尾
                else if (sameNode(oldNodeLIst[oldStart], newNodeList[newEnd])) {
                    newNodeList[newEnd]['element'] = oldNodeLIst[oldStart]['element']
                    parentNode.insertBefore(oldNodeLIst[oldStart]['element'], oldNodeLIst[oldEnd]['element'].nextSibling)
                    patchNode(oldNodeLIst[oldStart], newNodeList[newEnd], newNodeList[newEnd]['element'])
                    oldNodeLIst[oldStart]['element'] = null
                    ++oldStart
                    --newEnd
                }
                // 旧尾->新头
                else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newStart])) {
                    newNodeList[newStart]['element'] = oldNodeLIst[oldEnd]['element']
                    parentNode.insertBefore(oldNodeLIst[oldEnd]['element'], oldNodeLIst[oldStart]['element'])
                    patchNode(oldNodeLIst[oldEnd], newNodeList[newStart], newNodeList[newStart]['element'])
                    oldNodeLIst[oldEnd]['element'] = null
                    ++newStart
                    --oldEnd
                }
                // 查找是否存在相同节点
                else {
                    const res = oldNodeLIst.findIndex(item => sameNode(newNodeList[newStart], item))
                    if (res !== -1) {
                        newNodeList[newStart]['element'] = oldNodeLIst[res]['element']
                        if (res !== 0) {
                            parentNode.insertBefore(oldNodeLIst[res]['element'], oldNodeLIst[oldStart]['element'])
                        }
                        patchNode(oldNodeLIst[res]['element'], newNodeList[newStart]['element'], newNodeList[newStart]['element'])
                        oldNodeLIst[res]['element'] = null
                        ++newStart
                    } else {
                        const newRes = createElment(newNodeList[newStart])
                        parentNode.insertBefore(newRes.element, oldNodeLIst[oldStart]['element'])
                        ++newStart
                    }
                }
            }
            // 结束之后,清除旧节点中没有复用的节点
            while (oldStart <= oldEnd) {
                oldNodeLIst[oldStart]['element'] && parentNode.removeChild(oldNodeLIst[oldStart]['element'])
                ++oldStart
            }
        } else if (newNode.children && newNode.children.length > 0 && (!oldNode.children || oldNode.children.length === 0)) {
            newNode.children.forEach(item => {
                const newRes = createElment(item)
                parentNode.insertBefore(newRes.element, parentNode.children.length > 0 ? parentNode.children[parentNode.children.length - 1] : null)
            })
        }
        else {
            for (let i = 0; i < parentNode.children.length; i++) {
                parentNode.removeChild(parentNode.children[0])
            }
        }
    }
    const getElementAttr = (node) => {
        // 获取所有属性
        const attributes = node.attributes
        const res = {}
        // 遍历所有属性
        for (let i = 0; i < attributes.length; i++) {
            let attr = attributes[i]
            res[attr.name] = attr.value
        }
        return res
    }
    const realNodeToVnode = (node) => {
        const tag = node.tagName.toLowerCase()
        const children = node.children
        const txt = node.innerText
        const vnode = createVnode(tag, node, children, txt, getElementAttr(node))
        vnode.children = Array.from(children).map(item => realNodeToVnode(item))
        return vnode
    }
    // oldNode一定在页面存在真实节点
    const patch = (oldNode, newNode) => {
        if (!oldNode.sel) {
            // 传进来的非虚拟dom
            oldNode = realNodeToVnode(oldNode)
        }
        if (oldNode.sel !== newNode.sel) {
            const newRes = createElment(newNode)
            let p = oldNode.element.parentNode
            p.insertBefore(newRes.element, oldNode.element)
            p.removeChild(oldNode.element)
        }
        else {
            patchNode(oldNode, newNode, oldNode.element)
        }
    }
     const box = document.querySelector("#box")
const vnode4 = h('ul', {}, [
        h('li', { key: "B" }, 'B'),
        h('li', { key: "D" }, 'D'),
        h('li', { key: "C" }, "C"),
        h('li', { key: "A" }, 'A'),
        h('li', { key: "E" }, 'E'),
    ])
     patch(box, vnode4)

我只是关注核心对比部分,没有过多关注其他细节地方,多多指正!!!