手写diff算法

环境搭建

  • 创建一个文件夹,来到文件夹目录下,使用npm init -y 生成package.json文件。
    下载webpack、webpack-cli、webpack-dev-server,这里需要特别注意的是webpack的各个版本。
    下载对应的版本
cnpm i webpack@5 webpack-cli@3 webpack-dev-server@3 -S
  • 创建webpack.config.js文件
module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        path: __dirname + '/public',
        filename: './js/[name].js'
    },
    devServer: {
        contentBase: './public',
        inline: true
    }
}
  • 根据webpack.config.js文件的内容创建相应的文件
    创建完成之后的文件目录
    在这里插入图片描述
  • 在index.html文件中引入js文件
    在这里插入图片描述
  • 运行程序
    需要在package.json文件中加入运行命令
{
  "name": "diff",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^5.52.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}
  • 运行程序
    npm run dev

创建h函数

  • 在src下新建文件夹dom,新建文件h.js
import vnode from './vnode'
export default function(sel, data, params) {
    // console.log(sel, data, params)
    // 没有子元素的情况
    if(typeof params === 'string') {
        return vnode(sel, data, undefined, params, undefined)
    } else if(Array.isArray(params)) { // 有子元素的情况
        let children = []
        for(let item of params) {
            children.push(item)
        }
        return vnode(sel, data, children, undefined, undefined)
    }
}
  • 在dom文件夹下新建vnode.js,该函数的作用是将真实dom转化为虚拟dom
export default function(sel, data, children, text, elm) {
    /**
     * sel: 虚拟dom
     * data: key
     * children: 子元素
     * text: 文本内容
     * elm: 真实dom
     */

    return {
        sel,
       data,
       children,
       text,
       elm
    }
}
  • 在index.js向创建虚拟dom
import h from './dom/h'

let vnode1 = h("div", {}, "你好呀");
console.log(vnode1)

let vnode2 = h("ul", {}, [
    h("li", {}, "a"),
    h("li", {}, "b"),
    h("li", {}, "c"),
    h("li", {}, "d"),
    h("li", {}, "e"),
]);

console.log(vnode2)
  • 运行结果
    在这里插入图片描述

将真实的dom转为虚拟dom,然后进行新旧虚拟节点比较

  • 如果新老节点不是同一个节点名称,那么久暴力删除旧的节点,插入新的节点。
    1、在index.html下新写一个div
 <div id="container">这是container</div>

2、在index.js下获取获取到index.html下的div这个真实dom节点

// 获取真实的dom节点
const container = document.getElementById('container')

3、创建虚拟节点,将虚拟节点patch到真实dom上

// 虚拟节点
let vnode1 = h("h1", {}, "你好呀");
// console.log(vnode1)

let vnode2 = h("ul", {}, [
    h("li", {}, "a"),
    h("li", {}, "b"),
    h("li", {}, "c"),
    h("li", {}, "d"),
    h("li", {}, "e"),
]);
patch(container, vnode1)

4、新建patch.js函数,作用是将真实的dom转化为虚拟dom,然后再进行新旧虚拟dom的比较

import vnode from './vnode'
import createElement from './createElement'
/**
 * @param {*} oldVnode 旧虚拟节点
 * @param {*} newVnode 新虚拟节点
 */
export default function (oldVnode, newVnode) {
    // 如果oldVnode没有sel,则是非虚拟节点
    if (oldVnode.sel === undefined) {
        // console.log(oldVnode.tagName.toLowerCase())
        oldVnode = vnode(
            oldVnode.tagName.toLowerCase(), //虚拟节点名称
            {},
            [],
            undefined,
            oldVnode // 真实的dom元素
        )
    }
    // console.log(oldVnode)
    // 开始判断新旧虚拟节点
    if (oldVnode.sel !== newVnode.sel) { // 如果新老节点不是同一个节点名称,那么久暴力删除旧的节点,插入新的节点。
        // 把新的虚拟节点创建为真实dom节点
        let newVnodeElm = createElement(newVnode) // 在这里最好将创建真实dom写成一个函数,方便递归调用
        // console.log(newVnodeElm)
        let oldVnodeElm = oldVnode.elm
        if(newVnodeElm) {
            oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
        }
        oldVnodeElm.parentNode.removeChild(oldVnodeElm)
    } else { // 是同一个节点,逻辑变复杂

    }
}

5、新建createElement.js文件,作用是将虚拟节点转化为真实节点,并挂载在真实dom上

export default function createElement (vnode) {
    let domNode = document.createElement(vnode.sel)
    // 判断有没有子节点
    if(vnode.children === undefined) {
        domNode.innerText = vnode.text
    }else if(Array.isArray(vnode.children)) {
        for(let child of vnode.children) {
            let childDom = createElement(child)
            domNode.appendChild(childDom)
        }
    }
    vnode.elm = domNode
    return domNode
}
  • 如果是相同节点,分为以下几种情况
    • (1)新节点没有children,直接替换
    • (2)新节点有children,旧的节点也有children,diff算法核心
    • (3)新节点有children,旧的没有,直接创建元素添加

新建patchVnode.js文件,判断新旧虚拟节点相同的情况

import createElement from './createElement'
export default function(oldVnode, newVnode) {
    // 判断新节点有没有子元素
    if(newVnode.children === undefined) { // 没有子元素
        if(newVnode.text !== oldVnode.text) {
            oldVnode.elm.innerText = newVnode.text
        }
    } else { // 有子元素
        // 判断旧节点有没有子元素
        if(oldVnode.children && oldVnode.children.length > 0) { // 旧节点有子元素,情况较为复杂

        } else { // 旧节点没有子元素
            oldVnode.elm.innerHTML = '' // 删掉旧节点的内容
            for(let child of newVnode.children) {
                let childDom = createElement(child)
                oldVnode.elm.appendChild(childDom)
            }
            console.log(oldVnode.elm)
        }
    }
}

patch.js文件

import vnode from './vnode'
import createElement from './createElement'
import patchVnode from './patchVnode'
/**
 * @param {*} oldVnode 旧虚拟节点
 * @param {*} newVnode 新虚拟节点
 */
export default function (oldVnode, newVnode) {
    // 如果oldVnode没有sel,则是非虚拟节点
    if (oldVnode.sel === undefined) {
        // console.log(oldVnode.tagName.toLowerCase())
        oldVnode = vnode(
            oldVnode.tagName.toLowerCase(), //虚拟节点名称
            {},
            [],
            undefined,
            oldVnode // 真实的dom元素
        )
    }
    // console.log(oldVnode)
    // 开始判断新旧虚拟节点
    if (oldVnode.sel !== newVnode.sel) { // 如果新老节点不是同一个节点名称,那么久暴力删除旧的节点,插入新的节点。
        // 把新的虚拟节点创建为真实dom节点
        let newVnodeElm = createElement(newVnode) // 在这里最好将创建真实dom写成一个函数,方便递归调用
        // console.log(newVnodeElm)
        let oldVnodeElm = oldVnode.elm
        if(newVnodeElm) {
            oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
        }
        oldVnodeElm.parentNode.removeChild(oldVnodeElm)
    } else { // 是同一个节点,逻辑变复杂
        patchVnode(oldVnode, newVnode)
    }
}

index.js文件

import h from './dom/h'
import patch from './dom/patch'

// 虚拟节点
// let vnode1 = h("div", {}, "你好呀");
let vnode1 = h("div", {}, [
    h("span", {}, "a"),
    h("span", {}, "b"),
    h("span", {}, "c"),
    h("span", {}, "d"),
    h("span", {}, "e"),
]);

// 获取真实的dom节点
const container = document.getElementById('container')

patch(container, vnode1)

diff算法核心

  • 1、旧前和新前
    匹配:旧前指针++、新前指针++
  • 2、旧后和新后
    匹配:旧后指针–、新后指针–
  • 3、旧前和新后
    匹配:旧前指针++、新后指针–
  • 4、旧后和新前
    匹配:旧后指针–、新前指针++
  • 5、以上都不满足条件 ===》查找
    查找到:新的指针++,将新的指针指向的元素渲染到页面,并且将在旧的节点中找到相应的元素将其赋值为undefined
  • 6、创建或者删除
    新建updateChildren.js,作用是处理diff算法中的5种情况
import patchVnode from './patchVnode'
import createElement from './createElement'
// 判断两个虚拟节点是否为同一个节点
function sameVnode(vnode1, vnode2) {
    return vnode1.key === vnode2.key
}

/**
 * 
 * @param {*} parentElm 旧的父节点
 * @param {*} oldCh 旧的子节点
 * @param {*} newCh 新的子节点
 */
export default function(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]
        }
        if(oldEndVnode === undefined) {
            oldEndVnode = oldCh[--oldEndIdx]
        }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(oldEndVnode) {
                newEndVnode.elm = oldEndVnode?.elm
            }
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if(sameVnode(oldStartVnode, newEndVnode)) {
            // 旧前和新后
            console.log(3)
            patchVnode(oldStartVnode, newEndVnode)
            if(oldStartVnode) {
                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(oldEndVnode) {
                newStartVnode.elm = oldEndVnode?.elm
            }
            // 把旧后指定的节点移动到旧前指向的节点的前面
            parentElm.insertBefore( oldEndVnode.elm, oldStartVnode.elm )
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            // 以上都不满足条件 ===》查找
            console.log(5)
            // 创建一个对象存虚拟节点
            let keyMap = {}
            for(let i = oldStartIdx;i <= oldEndIdx;i++) {
                const key = oldCh[i]?.key
                if(key) {
                    keyMap[key] = i
                }
            }
            // console.log(keyMap)
            // 在旧节点中寻找前节点指向的节点
            let indxInOld = keyMap[newStartVnode.key]
            if(indxInOld) { // 能找到
                const elmMove = oldCh[indxInOld]
                patchVnode(elmMove, newStartVnode)
                // 处理过的节点,在旧虚拟节点的数组中,设置为undefined
                oldCh[indxInOld] = undefined
                parentElm.insertBefore(elmMove.elm,  oldStartVnode.elm)
            }else { // 不能找到
                // 说明是新节点,需要重新创建
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
            }
            // 新节点指针+1
            newStartVnode = newCh[++newStartIdx]
        }
    }
}

在patchVnode.js中引用updateChildren方法

import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVnode (oldVnode, newVnode) {
    // console.log(oldVnode) 
    // 判断新节点有没有子元素
    if (newVnode.children === undefined) { // 没有子元素
        if (newVnode.text !== oldVnode.text) {
            oldVnode.elm.innerText = newVnode.text
        }
    } else { // 有子元素
        // 判断旧节点有没有子元素
        if (oldVnode.children && oldVnode.children.length > 0) { // 旧节点有子元素,情况较为复杂
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
            // console.log(oldVnode.elm, oldVnode.children, newVnode.children)
        } else { // 旧节点没有子元素
            oldVnode.elm.innerHTML = '' // 删掉旧节点的内容
            for (let child of newVnode.children) {
                let childDom = createElement(child)
                oldVnode.elm.appendChild(childDom)
            }
            // console.log(oldVnode.elm)
        }
    }
}
  • 创建或者删除节点
    在updateChild.js中添加以下代码
// 结束while 只有两种情况 (新增和删除)
    // 1、oldStartIdx > oldEndIdx
    // 2、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++) {
            // console.log(oldCh[i])
            parentElm.removeChild(oldCh[i].elm)
        }
    }

参考文献

https://www.bilibili.com/video/BV1K64y1s7ot?p=9&spm_id_from=pageDriver

全部代码

gitee地址
https://gitee.com/jjm1/diff
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值