Vue源码之虚拟DOM和diff算法

Vue源码之虚拟DOM和diff算法

手写一个diff算法和h函数

1. 环境搭建

1.1 为实现模块化开发,我们使用webpack构建工具

"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3"

1.2 webpack.config.js

const path = require("path")

module.exports = {
    mode: "development",
    entry: './src/index.js',
    output: {
        filename: "bundle.js"
    },
    devServer: {
        contentBase: path.join(__dirname, "www"),
        compress: false,
        port: 8080,
        publicPath: "/xuni/"
    }
}

2. diff算法

2.1 什么是diff算法

diff算法就是最小量更新算法,那什么是最小量,在Vue中我们使用v-for指令时,我们要加key属性,如果节点名称和key不变得话,我们就不破坏原来的,哪里变了我更新哪里

2.2 diff算法原理图

在这里插入图片描述
diff算法比较的是虚拟dom,在Vue编译阶段的时候,在形成AST抽象语法树的时候虚拟dom就已经形成了,而我们diff算法比较的是虚拟dom

diff算法的比较策略就是同级比较,不同级不比较,就如上图

diff算法采用4指针,4命中,1循环的方式,具体如何看下面代码,手动实现diff算法

3. 入口文件和html页面

3.1 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>点我切换</button>
<div id="diff"></div>
<script src="/xuni/bundle.js"></script>
</body>
</html>

准备一个按钮,进行diff算法比对

3.2 index.js

import h from './MySnabbdom/h'
import patch from "./MySnabbdom/patch";

let myVNode1 = h('h1', {}, [
    h('h2', {key: "a"}, "a"),
    h('h2', {key: "b"}, "b"),
    h("h2", {key: "c"}, "c"),
    h("h2", {key: "d"}, "d"),
    h("h2", {key: "e"}, "e"),
    h("h2", {key: "f"}, "f"),
])

let myVNode2 = h('h1', {}, [
    h("h2", {key: "f"}, "f"),
    h('h2', {key: "a"}, "a"),
    h("h2", {key: "e"}, "e"),
    h("h2", {key: "d"}, "d"),
    h("h2", {key: "g"}, "g"),
    h("h2", {key: "c"}, "c"),
])
const div = document.querySelector("div")


patch(div, myVNode1)

document.querySelector("button").addEventListener("click", () => {
    patch(myVNode1, myVNode2)
})

准备好两个Node,再给按钮绑定事件监听

4. vNode函数

export default function vNode(sel, data, children, text, elm) {
    //The function passes an argument to an object and returns it
    const key = data.key
    return {
        sel, data, children, text, elm, key
    }
}

vNode函数的功能就是将你传入的模板变成一个对象的形式,方便进行diff算法的比对

5. h函数

import vNode from "./vNode";

export default function h(sel, data, c) {
    if (arguments.length !== 3) throw new Error("The h function must take three arguments")
    if (typeof c === "string" || typeof c === "number") {
        return vNode(sel, data, undefined, c, undefined)
    } else if (Array.isArray(c)) {
        let children = []
        for (let i = 0; i < c.length; i++) {
            if (typeof c[i] !== "object" && !c[i].hasOwnProperty('sel'))
                throw new Error("One or more of the items passed into your array are not H-functions")
            children.push(c[i])
        }
        return vNode(sel, data, children, undefined, undefined)
    } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
        let children = [c]
        return vNode(sel, data, children, undefined, undefined)
    } else {
        throw new Error("The third type passed into the function is incorrect")
    }
}

h函数的主要功能就是判断你传入的模板的合法性,如果不合法,主动抛出异常

6. patch函数

首先我们要搞懂diff算法的更新判定机理
在这里插入图片描述

import vNode from "./vNode";
import createElements from "./createElements";
import patchVNode from "./patchVNode";

export default function patch(oldVNode, newVNode) {
    //Determine whether the old node is a virtual DOM or a DOM
    if (oldVNode.sel === '' || oldVNode.sel === undefined) {
        //Wrapped as a virtual DOM
        oldVNode = vNode(oldVNode.tagName.toLowerCase(), {}, [], undefined, oldVNode)
    }
    //Check whether oldVNode and newVNode are the same node
    if (oldVNode.key === newVNode.key && oldVNode.sel === newVNode.sel) {
        //If so, refine the update
        patchVNode(oldVNode, newVNode)
    } else {
        //If not, take it apart violently and renew it
        let newVNodeDom = createElements(newVNode)
        if (oldVNode.elm && newVNodeDom) {
            oldVNode.elm.parentNode.insertBefore(newVNodeDom, oldVNode.elm)
        }
        //Deleting an Old Node
        oldVNode.elm.parentNode.removeChild(oldVNode.elm)
    }
}

7. createElements函数

export default function createElements(vNode) {
    //This function creates the actual node
    let domNode = document.createElement(vNode.sel)
    //Determine whether there are children or text
    if (vNode.text !== "" && vNode.children === undefined || vNode.children.length === 0) {
        domNode.innerHTML = vNode.text
    } else if (Array.isArray(vNode.children) && vNode.children.length > 0) {
        //Internal child nodes to recurse, create child nodes
        vNode.children.forEach(object => domNode.appendChild(createElements(object)))
    }
    vNode.elm = domNode

    return vNode.elm
}

该函数的主要功能是将传入的虚拟DOM转化为真实DOM

8. patchVNode函数

import createElements from "./createElements";
import updateChildren from "./updateChildren";

export default function patchVNode(oldVNode, newVNode) {
    if (oldVNode === newVNode) return
    if (newVNode.hasOwnProperty("text") && newVNode.children === undefined || newVNode.children.length === 0) {
        //The new node has no text property
        if (newVNode.text !== oldVNode.text) oldVNode.elm.innerText = newVNode.text
    } else {
        if (oldVNode.children !== undefined && oldVNode.children.length > 0) {
            updateChildren(oldVNode.elm, oldVNode.children, newVNode.children)
        } else {
            oldVNode.elm.innerHTML = ""
            newVNode.children.forEach(object => oldVNode.elm.appendChild(createElements(object)))
        }
    }
}

该函数的作用是除最后一个diff细致化的更新,在diff更新机理上的所有内容完成

9. updateChildren函数

该函数是diff算法最强和最核心的功能实现,是在children属性上的变化的侦测

在次Vue的设计是设置4个指针,分别指向着旧DOM的头,旧DOM的尾,新DOM的头和新DOM的尾,来进行比较

那么这4个指针设计到4个命中,还有设计这4个命中的顺序

  1. 新前与旧前 ---------- 如果新前与旧前相同,也就是命中,则新前和旧前指针下移
  2. 新后与旧后 ---------- 如果新前与旧前没有比对成功,就进行新后与旧后的比较,如果命中成功,新后与旧后指针上移
  3. 新后与旧前 ---------- 如果前两种没有命中成功,就比较新后与旧前,如果命中,则新前指向的节点,移动到旧后之后,并且,新后指针上移,旧前指针下移
  4. 新前与旧后 ---------- 如果前三种都没命中,就匹配这种,如果命中,则新前指向的节点,移动到旧前之前
  5. 如果前4种都没命中,就循环,映射来查找,最后找到映射到的结果移动到oldStartLdx之前
import patchVNode from "./patchVNode";
import createElements from "./createElements";

//Determine whether it is a node
function checkSameVNode(a, b) {
    return a.sel === b.sel && a.key === b.key
}

export default function updateChildren(parentElm, oldCh, newCh) {
    //Old before
    let oldStartIdx = 0
    //Before the new
    let newStartIdx = 0
    //After the old
    let oldEndIdx = oldCh.length - 1
    //After the new
    let newEndIdx = newCh.length - 1
    //Before the old node
    let oldStartVNode = oldCh[0]
    //After the old node
    let oldEndVNode = oldCh[oldEndIdx]
    //Before the new node
    let newStartVNode = newCh[0]
    //After the new node
    let newEndVNode = newCh[newEndIdx]
    let keyMap = null
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        switch (undefined) {
            case oldStartVNode || oldCh[oldStartIdx]:
                oldStartVNode = oldCh[++oldStartIdx]
                break
            case oldEndVNode || oldCh[oldEndIdx]:
                oldEndVNode = oldCh[--oldEndIdx]
                break
            case newStartVNode || newCh[newStartIdx]:
                newStartVNode = newCh[++newStartIdx]
                break
            case newEndVNode || newCh[newEndIdx]:
                newEndVNode = newCh[--newEndIdx]
                break
            default:
                //Before the new and before the old hit
                if (checkSameVNode(oldStartVNode, newStartVNode)) {
                    patchVNode(oldStartVNode, newStartVNode)
                    oldStartVNode = oldCh[++oldStartIdx]
                    newStartVNode = newCh[++newStartIdx]
                }
                //The new back hits the old back
                else if (checkSameVNode(oldEndVNode, newEndVNode)) {
                    patchVNode(oldEndVNode, newEndVNode)
                    oldEndVNode = oldCh[--oldEndIdx]
                    newEndVNode = newCh[--newEndIdx]
                }
                //The new post hits the old front
                else if (checkSameVNode(oldStartVNode, newEndVNode)) {
                    patchVNode(oldStartVNode, newEndVNode)
                    parentElm.insertBefore(oldStartVNode.elm, oldEndVNode.elm.nextSibling)
                    oldStartVNode = oldCh[++oldStartIdx]
                    newEndVNode = newCh[--newEndIdx]
                }
                //New before and old after hit
                else if (checkSameVNode(oldEndVNode, newStartIdx)) {
                    patchVNode(oldEndVNode, newStartVNode)
                    parentElm.insertBefore(oldEndVNode.elm, oldStartVNode.elm)
                    oldEndVNode = oldCh[--oldEndIdx]
                    newStartIdx = newCh[++newStartIdx]
                }
                //None of them hit
                else {
                    if (!keyMap) {
                        keyMap = {}
                        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                            const key = oldCh[i].key
                            if (key !== undefined) {
                                keyMap[key] = i
                            }
                        }
                    }
                    const idxInOld = keyMap[newStartVNode.key]
                    if (idxInOld === undefined) {
                        parentElm.insertBefore(createElements(newStartVNode), oldStartVNode.elm)
                    } else {
                        const elmToMove = oldCh[idxInOld]
                        patchVNode(elmToMove, newStartVNode)
                        oldCh[idxInOld] = undefined
                        parentElm.insertBefore(elmToMove.elm, oldStartVNode.elm)
                    }
                    newStartVNode = newCh[++newStartIdx]
                }
        }
    }
    //After the loop ends, start is still smaller than old
    if (newStartIdx <= newEndIdx) {
        const before = newCh[newEndIdx + 1] === undefined ? null : newCh[newEndIdx + 1].elm
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            parentElm.insertBefore(createElements(newCh[i]), before)
        }
    } else if (oldStartIdx <= oldEndIdx) {
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm)
            }
        }
    }
}

同时如果命中成功了,也要进行递归调用patchVNode函数,这样就能成功的解决children的嵌套

10. 最后

代码已上传至github

https://github.com/Bald-heads/DomAndDiff.git
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值