Vue2 diff算法 手写学习

61 阅读5分钟

项目地址:gitee.com/luobf22/vue…

概述

本文旨在学习vue2 Diff算法,可能有的说法没有理解清楚,如果有疑问,可以提出,或者前往官网或者自行查找源码

html页面,引入index.js文件
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    传入index.js
    <script type="module" src="index.js"></script>
</head>

<body>
    <div>vue2 Diff</div>
    <div id="container">
        <h1> 这是container</h1>
    </div>
    <button id="btn">按钮</button>
</body>

</html>
index.js页面,创建新旧虚拟节点

通过h函数创建vnode1、vnode2、vnode3节点 这里先vnode1转为vnode2,通过按钮将vnode2转为vnode3

import h from "./h.js";
import patch from './patch.js'
// 获取到真的dom节点
let container = document.getElementById('container')
// 虚拟dom节点
let vnode1 = h('div', {}, '你好呀')
let vnode2 = h('ul', {}, [
    h('li', {key:'a'}, 'a'),
    h('li', {key:'b'}, 'b'),
    h('li', {key:'c'}, 'c'),
    // h('li', {key:'d'}, 'd'),
    // h('li', {key:'e'}, 'e'),
])
// console.log(vnode2);

patch(container, vnode2)

let vnode3 = h('ul', {}, [
    h('li', {key:'c'}, 'c'),
    h('li', {key:'b'}, 'b'),
    h('li', {key:'e'}, 'e'),
    h('li', {key:'a'}, 'a'),
    // h('li', {key:'d'}, 'd'),
])


let btn = document.getElementById('btn')
btn.onclick = () => {
    console.log('11');
    
    patch(vnode2, vnode3)
}

h.js 根据节点是否有子节点,创建虚拟节点
import vnode from "./vnode.js";
export default function (sel, data, params) {
    // string 没有子元素
    if (typeof params == 'string') {
        return vnode(sel, data, undefined, params, undefined)
    } else if(Array.isArray(params)){
        // 数组则遍历成children传递给第三个参数
        // console.log(params)
        let children = []
        for (const item of params) {
            children.push(item)
        }
        return vnode(sel, data, children, undefined, undefined)
    }
}
vnode.js 虚拟dom函数

返回一个带有key值的虚拟dom节点


export default function (sel, data, children, text, elm) {
    let key = data.key
    return {
        sel, data, children, text, elm, key
    }
}
patch.js
  • 先判断oldVnode是否是真实dom,如果是真实dom(没有sel),转为虚拟dom,diff算法其实就是将两个虚拟dom进行对比,对比有差异,再在真实dom上进行修改
  • 判断两个虚拟dom的元素节点是否一样,如果不一样,则通过createElement根据新虚拟节点内容,创建新的节点,并插入到旧虚拟节点前面,最后移除旧的虚拟节点元素
  • 如果一样,则通过patchVnode方法进行节点内容的对比
import vnode from './vnode.js'
import createElement from './createElement.js'
import patchVnode from './patchVnode.js';
// 旧虚拟节点 oldVnode
// 新虚拟节点 newVnode
export default function (oldVnode, newVnode) {
    // console.log(oldVnode);
    // console.log(oldVnode.sel);
    // console.log(newVnode.sel);

    // 如果oldVnode没有sel 就是非虚拟节点 就让它变成虚拟节点
    if (oldVnode.sel == undefined) {
        oldVnode = vnode(
            oldVnode.tagName.toLowerCase(),
            {}, //data,
            [], //children,
            undefined,//text,
            oldVnode
        )
    }
    // console.log(oldVnode);
    // 元素节点是否相同
    if (oldVnode.sel !== newVnode.sel) {
        // console.log('不同元素');
        // 暴力删除旧节点,插入新节点
        let newVnodeElm = createElement(newVnode)
        // console.log(newVnodeElm);
        // 获取旧的虚拟节点的 .elm就是真的节点
        let oldVnodeElm = oldVnode.elm
        // 创建新的节点
        if (newVnodeElm) {
            oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
        }
        // 删除旧节点
        oldVnodeElm.parentNode.removeChild(oldVnodeElm)
    } else {
        // console.log('相同元素');
        patchVnode(oldVnode, newVnode)
    }
}
creatElement.js 创建新的节点
// vnode为新节点,就是要创建的节点
export default function createElement(vnode) {
    // 创建dom节点
    let domNode = document.createElement(vnode.sel)
    // 判断有没有子节点
    if (vnode.children == undefined) {
        // console.log('// 没有子节点');/
        domNode.innerText = vnode.text
    } else if (Array.isArray(vnode.children)) {
        // console.log('// 有子节点,需要递归创建节点');
        for (const child of vnode.children) {
            let childDom = createElement(child)
            domNode.appendChild(childDom)
        }
    }
    
    // 补充elm属性 elm为真实节点 , 虚拟节点的属性值
    vnode.elm = domNode
    return domNode
}
patchVnode.js 判断子节点内容
  • 新虚拟节点没有子节点,新虚拟节点覆盖旧虚拟节点
  • 新虚拟节点有子节点,旧虚拟节点没有子节点,旧虚拟节点清空,新虚拟节点循环添加到旧虚拟节点下
  • 新虚拟节点和旧虚拟节点都有子节点,通过updateChildren进一步判断子节点内容
import createElement from "./createElement.js";
import updatChildren from './updatChildren.js'
export default function (oldVnode, newVnode) {
    // console.log(oldVnode.children);
    // console.log(newVnode.children);
    if (newVnode.children == undefined) {
        // console.log('新节点没有children,直接覆盖旧的');
        if (newVnode.text !== oldVnode.text) {
            // console.log('文本不同');
            oldVnode.elm.innerText = newVnode.text
        }
    } else {
        if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
            // console.log('新有,旧有',oldVnode.elm, oldVnode.children, newVnode.children);
            updatChildren(oldVnode.elm, oldVnode.children, newVnode.children)
        } else {
            // console.log('新有,旧无');
            // 将旧节点清空 可能有文本节点
            oldVnode.elm.innerHTML = ''
            // 遍历新的子节点添加到旧节点
            for (const child of newVnode.children) {
                let childDom = createElement(child)
                oldVnode.elm.appendChild(childDom)
            }
        }
    }
}
updateChildren.js, 对比新旧虚拟节点的子节点更新虚拟dom
  • 根据双端对比法
    • 通过sameVnode判断两旧前和新前节点是否一致,如果一致,patchVnode更新旧虚拟节点内容,
    • 通过sameVnode判断两旧后和新后节点是否一致,如果一致,然后patchVnode更新旧虚拟节点内容,
    • 通过sameVnode判断两旧前和新后节点是否一致,如果一致,然后patchVnode更新旧虚拟节点内容,将旧前节点插入到旧后节点后面
    • 通过sameVnode判断两旧后和新前节点是否一致,如果一致,然后patchVnode更新旧虚拟节点内容,将旧后节点插入到旧前节点前面
  • 双端对比法查询之后,没有循环结束则通过哈希表存储旧节点的值,然后在哈希表里找新节点内容,如果一致,则将旧节点插入到旧节点的顶部,并设置为undefined,如果没找到,旧创建新的节点放在旧节点底部
  • 循环结束之后,如果oldStartIdx > oldEndIdx,则表示新的虚拟节点子节点比旧的虚拟节点的子节点多,则旧的虚拟节点需要新增相应的节点到尾部,反之,表示新的虚拟节点比旧的虚拟节点少,则旧的虚拟节点删除相应的节点
import patchVnode from "./patchVnode.js"
import createElement from "./createElement.js"

// 判断两个虚拟节点是否为同一个节点
function sameVnode(vNode1, vNode2) {
    return vNode1.key == vNode2.key
}

// 真实dom节点 , 旧的虚拟节点 新的虚拟节点
export default function name(parentElm, oldCh, newCh) {
    // console.log(parentElm,oldCh,newCh);
    let oldStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let newStartIdx = 0
    let newEndIdx = newCh.length - 1

    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == undefined) {

            oldStartVnode = oldCh[++oldStartIdx]

        } else if (oldEndVnode == undefined) {

            oldEndVnode = oldCh[--oldStartIdx]

        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            console.log(1);
            // 旧前和新前
            patchVnode(oldStartVnode, newStartVnode)
            // 看看有什么问题
            if (newStartVnode) newStartVnode.elm = oldStartVnode?.elm
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]

        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            console.log(2);

            // 旧后和新后
            patchVnode(oldEndVnode, newEndVnode)
            if (newEndVnode) newEndVnode.elm = oldEndVnode?.elm
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]

        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            console.log(3);
            // 旧前和新后
            patchVnode(oldStartVnode, newEndVnode)
            if (newEndVnode) newEndVnode.elm = oldStartVnode?.elm
            // 把旧前指定的节点移动到旧后指向的节点后面
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)

            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]

        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            console.log(4);

            // 旧后和新前
            patchVnode(oldEndVnode, newStartVnode)
            if (newStartVnode) newStartVnode.elm = oldEndVnode?.elm
            // 把旧后指定的节点移动到旧前指向的节点后面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)

            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            // 其他情况 查找
            console.log(5);

            // 创建对象 存放虚拟节点 判断新旧有没有相同节点
            const keyMap = {}
            for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                const key = oldCh[i]?.key
                if (key) keyMap[key] = i
            }
            // 在旧节点中查找匹配新前节点
            let idxInOld = keyMap[newStartVnode.key]
            // 如果匹配到节点 该节点在新旧虚拟节点中都存在
            if (idxInOld) {
                const elmMove = oldCh[idxInOld]
                patchVnode(elmMove, newStartVnode)
                // 处理过的节点 在旧虚拟节点的数组中,设置为undefined
                oldCh[idxInOld] = undefined
                parentElm.insertBefore(elmMove.elm, oldStartVnode.elm)
            } else {
                // 创建节点
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
            }
            newStartVnode = newCh[++newStartIdx]
        }
    }

    // 结束循环 新增和删除
    //oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
    if (oldStartIdx > oldEndIdx) {
        // 新增
        const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null

        for (let i = newStartIdx; i <= newEndIdx; i++) {
            parentElm.insertBefore(createElement(newCh[i]), before)

        }
    } else {
        // 删除
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            parentElm.removeChild(oldCh[i].elm)

        }
    }
}