手写虚拟DOM和diff算法

①:创建虚拟DOM流程
定义vnode函数:

//函数的功能非常简单,就是把传入的字段拼成一个对象返回出去
export default function(sel,data,children,text,elm) {
    return {sel,data,children,text,elm}
}

定义h函数:

import vnode from "./vnode";

//调用的时候一定是下面三种形态之一:
// h('div',{},'文字')  1
// h('div',{},[])  2
// h('div',{},h()) 3
export default function (sel, data, c) {
    //检查参数的个数
    if (arguments.length !== 3) throw new Error("对不起,h函数必须传入三个参数,我们是低配版的h函数");
    //检查C的类型
    if (typeof c == "string" || typeof c == "number") {
        //说明传入的是第一种形态的参数
        return vnode(sel, data, undefined, c, undefined)
    } else if (Array.isArray(c)) {
        //说明传入的是第二种形态的参数
        const children = [];
        //遍历C
        for (let i = 0; i < c.length; i++) {
            const element = c[i];
            if (!(typeof element === "object" && "sel" in element))
                throw new Error("传入的第三个参数,数组中有项不是h函数");
            children.push(element)
        }
        //循环结束说明children收集完毕了,此时就可以返回虚拟节点了,它有children属性的
        return vnode(sel, data, children, undefined, undefined)
    } else if (typeof c === "object" && "sel" in c) {
        //hasOwnproperty方法或者"sel" in c 方式用来判断对象上是否有输入的属性,返回值是布尔值
        //说明现在传入的是第三种形态参数
        //所以传入的C是唯一的children
        const children = [c];
        return vnode(sel, data, children, undefined, undefined)
    } else {
        throw new Error("传入的第三个参数类型不正确");
    }
}

创建虚拟DOM:

import h from "./mySnabbdom/h"

const mySnabbdom = h('div', {}, [
    h('b', {}, '1'),
    h('h4', {}, '2'),
    h('h1', {}, '3'),
    h('br', {}, ''),
    h('p', {}, h('i',{},"qweqwe")),
])
console.log(mySnabbdom);

②:diff算法流程
diff算法的初始流程:
在这里插入图片描述
如何判断旧节点新节点是同一个?

//查看新旧节点,key唯一值相同并且是同一个选择器
function sameVnode (vnode1, vnode2){
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

创建patch.js文件(方法)

import vnode from "./vnode";
import createElement from "./createElement"
import patchVnode from "./patchVnode"
/**
 *
 *
 * @export
 * @param {老DOM} oldVnode
 * @param {新DOM} newVnode
 */
export default function (oldVnode, newVnode) {
    //判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == "" || oldVnode.sel == undefined) {
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    //判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        patchVnode(oldVnode, newVnode)
    } else {
        console.log('不是同一个节点,暴力删除,插入新的节点');
        let newVnodeElm = createElement(newVnode)
        //插入到老节点之前
        if (oldVnode.elm && newVnodeElm) {
            //insertBefore() 方法可在已有的子节点前插入一个新的子节点
            //第一个参数:新节点  第二个参数:已有的节点
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        //删除老的节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

创建createElement.js(方法)

//真正创建节点,将vnode创建为DOM,是孤儿节点不进行插入
/**
 *
 *
 * @export
 * @param {新DOM} vnode
 * @returns
 */
export default function createElement(vnode) {
    //创建一个DOM节点,这个节点现在还是孤儿节点
    const domNode = document.createElement(vnode.sel);
    //有子节点还是有文本
    if (vnode.text !== "" && (vnode.children == undefined || vnode.children.length == 0)) {
        //它内部是文字
        domNode.innerText = vnode.text;
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        //内部是子节点,就要递归创建节点
        for (let i = 0; i < vnode.children.length; i++) {
            //得到当前这个children
            const ch = vnode.children[i];
            //console.log(ch);
            //创建出它的DOM,一旦调用createrElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点
            let chDom = createElement(ch);
            //上树
            domNode.appendChild(chDom)
        }
    }
    //补充domNode
    vnode.elm = domNode;
    //返回elm,elm是一个纯DOM属性
    return vnode.elm
}

精细化比较:
在这里插入图片描述
经典的diff算法优化策略:
在这里插入图片描述
什么是新前和旧前:
在这里插入图片描述
创建patchVnode.js(方法)

import createElement from "./createElement"
import updataChildren from "./updataChildren"
/**
 *
 *
 * @export
 * @param {真实DOM} oldVnode
 * @param {虚拟DOM} newVnode
 */
export default function patchVnode(oldVnode, newVnode) {
    //判断老节点和新节点是否是同一片内存空间的
    if (oldVnode !== newVnode) {
        //判断新节点是否有文本
        if (newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
            //console.log("新节点有text属性");
            //判断新节点和老节点的文本是否相同,如果不相同那么直接替换(老节点的内容无论是什么,使用innerText方法都会全部替换掉)
            if (newVnode.text !== oldVnode.text) {
                oldVnode.elm.innerText = newVnode.text;
            }
        } else {
            //console.log("新节点没有text属性");
            //如果新节点没有文本那么肯定有子节点(children),那么判断老节点是否有子节点(children)这部分是最复杂的
            if (oldVnode.children && oldVnode.children.length) {
                updataChildren(oldVnode.elm,oldVnode.children,newVnode.children)
            } else {
                //老节点没有子节点(children),那么清空老节点中的文本,并把新节点中的子节点(children)插入到DOM中
                //为什么要先清空在插入,是因为appendChild不会替换之前已经存在的文本,而是追加元素上树
                oldVnode.elm.innerHTML = "";
                for (let i = 0; i < newVnode.children.length; i++) {
                    const ch = newVnode.children[i];
                    let dom = createElement(ch)
                    oldVnode.elm.appendChild(dom)
                }
            }
        }
    }
}

创建updataChildren.js(方法)

import patchVnode from "./patchVnode"
import createElement from "./createElement"
/**
 *
 *
 * @export
 * @param {父节点} parentElm
 * @param {老的子元素} oldCh
 * @param {新的子元素} newCh
 */
export default function updataChildren(parentElm, oldCh, newCh) {
    //四种命中指针
    let newStarIdx = 0; //新前
    let newEndIdx = newCh.length - 1; //新后
    let oldStarIdx = 0; //旧前
    let oldEndIdx = oldCh.length - 1; //旧后

    let newStarVnode = newCh[0]; //新前节点
    let newEndVnode = newCh[newEndIdx]; //新后节点
    let oldStarVnode = oldCh[0]; //旧前节点
    let oldEndVnode = oldCh[oldEndIdx]; //旧后节点

    let keyMap = null;

    //开始大while了
    while (newStarIdx <= newEndIdx && oldStarIdx <= oldEndIdx) {
        console.log('★');
        //首先不是判断四种指针是否命中,而是要略过已经家undefined标记的东西
        if (oldStarVnode == null || oldCh[oldStarIdx] == undefined) {
            oldStarVnode = oldCh[++oldStarIdx];
        } else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newStarVnode == null || newCh[newStarIdx] == undefined) {
            newStarVnode = newCh[++newStarIdx];
        } else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
            newEndVnode = newCh[++newEndIdx];
        } else if (checkSameVnode(newStarVnode, oldStarVnode)) {
            //新前和旧前
            console.log('新前和旧前');
            patchVnode(oldStarVnode, newStarVnode)
            //前指针向后走
            newStarVnode = newCh[++newStarIdx]
            oldStarVnode = oldCh[++oldStarIdx]

        } else if (checkSameVnode(newEndVnode, oldEndVnode)) {
            //新后和旧后
            console.log('新后和旧后');
            patchVnode(oldEndVnode, newEndVnode)
            //后指针向前走
            newEndVnode = newCh[--newEndIdx]
            oldEndVnode = oldCh[--oldEndIdx]

        } else if (checkSameVnode(newEndVnode, oldStarVnode)) {
            //新后和旧前
            console.log('新后和旧前');
            patchVnode(oldStarVnode, newEndVnode)
            //当新后和旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            //如何移动节点? 只要你插入一个已经在DOM树上的节点,它就会被移动
            parentElm.insertBefore(oldStarVnode.elm, oldEndVnode.elm.nextSibling) //insertBefore方法参数是把第一个参数DOM插入到第二个参数DOM的前面
            newEndVnode = newCh[--newEndIdx] //新的后指针向前
            oldStarVnode = oldCh[++oldStarIdx] //旧的前指针向后
        } else if (checkSameVnode(newStarVnode, oldEndVnode)) {
            //新前和旧后
            console.log('新前和旧后');
            patchVnode(oldEndVnode, newStarVnode)
            //当新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
            //如何移动节点? 只要你插入一个已经在DOM树上的节点,它就会被移动
            parentElm.insertBefore(oldEndVnode.elm, oldStarVnode.elm) //insertBefore方法参数是把第一个参数DOM插入到第二个参数DOM的前面
            newStarVnode = newCh[++newStarIdx] //新的前指针向后
            oldEndVnode = oldCh[--oldEndIdx] //旧的后指针向前
        } else {
            //四种命中都没有命中,都没有找到的情况
            
            //寻找key的map
            if (!keyMap) {
                keyMap = {}
                //创建keyMap映射对象,这样就不用每次都遍历老对象了
                for (let i = oldStarIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key !== undefined) {
                        keyMap[key] = i;
                    }
                }
            }

            //寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStarVnode.key];
            
            if (idxInOld == undefined) {
                //判断,如果idxInOld是undefined表示他是全新的项
                //现在被加入的项(就是被newStartVnode这项)现在还不是真正的DOM节点
                parentElm.insertBefore(createElement(newStarVnode), oldStarVnode.elm)

            } else {
                //如果不是undefined,就表示不是全新的项,而是需要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStarVnode);
                //把这项设置为undefined,表示我已经处理完了
                oldCh[idxInOld] = undefined;
                //移动,调用insertBefore也可以实现移动
                parentElm.insertBefore(elmToMove.elm, oldStarVnode.elm)
            }
            //指针下移,之移动新的头
            newStarVnode = newCh[++newStarIdx]
        }
    }
    //继续看看有没有剩余的,循环结束了start还是比End小,老节点走完了新节点还有没走完的,那么剩下start和end中间的节点就是需要增加的节点
    if (newStarIdx <= newEndIdx) {
        console.log(newCh, newStarIdx, newEndIdx, oldCh[oldEndIdx + 1], parentElm);
        const before = oldCh[oldEndIdx + 1] == null ? null : oldCh[oldEndIdx + 1].elm;
        console.log(before);
        for (let i = newStarIdx; i <= newEndIdx; i++) {
            const element = newCh[i];
            parentElm.insertBefore(createElement(element), before)
        }
    } else if (oldStarIdx <= oldEndIdx) {
        //批量删除oldStart和oldEnd指针之前的项,新节点走完了老节点还有剩余,start和end之间是老节点需要删除的
        for (let i = oldStarIdx; i <= oldEndIdx; i++) {
            const element = oldCh[i];
            if (element) {
                console.log(element);
                parentElm.removeChild(element.elm)
            }
        }
    }
}

//判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
    return a.key === b.key && a.sel === b.sel
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值