文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Vue中的虚拟DOM和Diff算法实例分析

2023-06-29 10:47

关注

这篇文章主要介绍了Vue中的虚拟DOM和Diff算法实例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue中的虚拟DOM和Diff算法实例分析文章都会有所收获,下面我们一起来看看吧。

Vue中的虚拟DOM和Diff算法实例分析

简单介绍一下 虚拟DOM 和 diff算法

先用一个简单的例子来说一下 虚拟DOMdiff算法:比如有一个户型图,现在我们需要对这个户型图进行下面的改造,

Vue中的虚拟DOM和Diff算法实例分析

其实,这个就是相当于一个进行找茬的游戏,让我们找出与原来的不同之处。下面,我已经将不同之处圈了出来,

Vue中的虚拟DOM和Diff算法实例分析

现在,我们已经知道了要进行哪些改造了,但是,我们该如何进行改造呢?最笨的方法就是全部拆了再重新建一次,但是,在我们实际中肯定不会进行拆除再新建,这样效率太低了,而且代价太昂贵。确实是完成了改造,但是,这不是一个最小量的更新,所以我们想要的是 diff,

Vue中的虚拟DOM和Diff算法实例分析

那么diff是什么呢?其实,diff 在我们计算机中就是代表着最小量更新的一个算法,会进行精细化对比,以最小量去更新。这样你就会发现,它的代价比较小,也不会昂贵,也会比较优化,所以对应在我们 Vue底层中是非常关键的。
好了,现在回归到我们的Vue中,上面的户型图中就相当于vue中的 DOM节点,我们需要对这些节点进行改造(增删调),然后以最小量去更新DOM,这样就会避免我们性能上面的开销。

// 原先DOM<div class="box">        <h3></h3>        <ul>            <li>1</li>            <li>2</li>            <li>3</li>        </ul>    </div>
// 修改后的DOM<div class="box">        <h3></h3>        <span>青峰</span>        <ul>            <li>1</li>            <li>2</li>            <li>3</li>            <li>4</li>        </ul>    </div>

在这里,我们就可以利用 diff算法进行精细化对比,实现最小量更新。上面我们了解了什么是diff,下面再来简单了解一下什么是虚拟DOM,

<div class="box">        <h3></h3>        <ul>            <li>1</li>            <li>2</li>            <li>3</li>        </ul>    </div>
{    sel: "div",    elm: undefined, // 表示虚拟节点还没有上树    key: undefined, // 唯一标识    data: {        class: { "box" : true}    },    children: [        {            sel: "h3",            data: {},            text: ""        },        {            sel: "ul",            data: {},            children: [                { sel: li, data: {}, text: "1"},                { sel: li, data: {}, text: "2"},                { sel: li, data: {}, text: "3"}            ]        }    ]}

通过观察可以发现,虚拟DOM 是一个 JavsScript对象,里面包含 sel选择器data数据text文本内容children子标签等等,一层嵌套一层。这样就表达了一个 虚拟DOM结构,处理 虚拟DOM 的方式总比处理 真实的DOM 要简单并且高效,所以 diff算法 是发生在 虚拟DOM 上的。
注意:diff算法 是发生在 虚拟DOM 上的。

为什么需要 Virtual DOM(虚拟DOM)

h函数(创建虚拟DOM)

作用:h函数 主要用来产生 虚拟节点(vnode)
第一个参数:标签名字、组件的选项对象、函数
第二个参数:标签对应的属性 (可选)
第三个参数:子级虚拟节点,字符串或者是数组形式

 h('a',{ props: {href: 'http://www.baidu.com'}, '百度'})

上面的h函数对应的虚拟节点为:

{ sel: 'a', data: { props: {href: 'http://www.baidu.com'}}, text: "百度"}

真正的DOM节点为:

<a href = "http://www.baidu.com">百度</a>

我们还可以嵌套的使用h函数,比如:

h('ul', {}, [    h('li', {}, '1'),    h('li', {}, '2'),    h('li', {}, '3'),])

嵌套使用h函数,就会生成一个虚拟DOM树。

{            sel: "ul",            elm: undefined,            key: undefined,            data: {},            children: [                { sel: li, elm: undefined, key: undefined, data: {}, text: "1"},                { sel: li, elm: undefined, key: undefined, data: {}, text: "2"},                { sel: li, elm: undefined, key: undefined, data: {}, text: "3"}            ]        }

好了,上面我们已经知道了h函数是怎么使用的了,下面我们手写一个阉割版的h函数。

手写 h函数

我们手写的这个函数只考虑三种情况(带三个参数),分别如下:

情况①:h('div', {}, '文字')情况②:h('div', {}, [])情况③:h('div', {}, h())

在手写h函数之前,我们需要声明一个函数,用来创建虚拟节点

// vnode.js 返回虚拟节点export default function(sel, data, children, text, elm) {    // sel 选择器 、data 属性、children 子节点、text 文本内容、elm 虚拟节点绑定的真实 DOM 节点    const key = data.key    return {        sel,        data,        children,        text,        elm,        key    }}

声明好vnode函数之后,我们正式来手写h函数,思路如下:

// h.js h函数import vnode from "./vnode";// 情况①:h('div', {}, '文字')// 情况②:h('div', {}, [])// 情况③:h('div', {}, h())export default function (sel, data, c) {    // 判断是否传入三个参数    if (arguments.length !== 3)         throw Error('传入的参数必须是三个参数')     // 判断c的类型     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++) {            // 子元素必须是h函数            if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))                throw Error('数组中有一项不是h函数')            // 收集子节点 不需要执行 因为数组里面已经执行h函数来            children.push(c[i])        }        return vnode(sel, data, children, undefined, undefined)    } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {        // 直接将子节点放到children中        let children = [c]        return vnode(sel, data, children, undefined, undefined)    } else {        throw Error('传入的参数格式不对')    }}

通过上面的代码,我们已经实现了一个简单 h函数 的基本功能。

感受 diff 算法

在讲解 diff算法 之前,我们先来感受一下 diff算法 的强大之处。先利用 snabbdom 简单来举一个例子。

import {    init,    classModule,    propsModule,    styleModule,    eventListenersModule,    h,} from "snabbdom";//创建出patch函数const patch = init([    classModule,     propsModule,     styleModule,     eventListenersModule, ]);//让虚拟节点上树const container = document.getElementById("container");const btn = document.getElementById("btn");//创建虚拟节点const myVnode1 = h('ul', {}, [    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D')])patch(container, myVnode1)const myVnode2 = h('ul', {}, [    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D'),    h('li', {}, 'E'),])btn.addEventListener('click', () => {    // 上树    patch(myVnode1,myVnode2)})

Vue中的虚拟DOM和Diff算法实例分析

当我们点接改变DOM的时候,发现会新增一个 li标签 内容为 E,单单的点击事件,我们很难看出,是将 旧的虚拟DOM 全部替换掉 新的虚拟DOM,然后再渲染成 真实DOM,还是直接在 旧的虚拟DOM 上直接在后面添加一个节点,所以,在这里我们可以巧妙的打开测试工具,直接将标签内容进行修改,如果点击之后是全部拆除,那么标签的内容就会发生改变,若内容没有发生改变,则是将最后添加的。

Vue中的虚拟DOM和Diff算法实例分析

点击改变 DOM 结构:

Vue中的虚拟DOM和Diff算法实例分析

果然,之前修改的内容没有发生变化,这一点,就可以验证了是进行了 diff算法精细化的比较,以最小量进行更新。那么问题就来了,如果我在前面添加一个节点呢?是不是也是像在最后添加一样,直接在前面添加一个节点。我们不妨也来试一试看看效果:

...const container = document.getElementById("container");const btn = document.getElementById("btn");//创建虚拟节点const myVnode1 = h('ul', {}, [    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D')])patch(container, myVnode1)const myVnode2 = h('ul', {}, [    h('li', {}, 'E'),  // 将E移至前面    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D'),])btn.addEventListener('click', () => {    // 上树    patch(myVnode1,myVnode2)})

Vue中的虚拟DOM和Diff算法实例分析

点击改变 DOM 结构

Vue中的虚拟DOM和Diff算法实例分析

哦豁!!跟我们想的不一样,你会发现,里面的文本内容全部发生了变化,也就是说将之前的 DOM 全部拆除,然后将新的重新上树。这时候,你是不是在怀疑其实 diff算法 没有那么强大,但是你这样想就大错特错了,回想一下在学习 Vue 的过程中,在遍历DOM节点 的时候,是不是特别的强调要写上key唯一标识符,此时,key在这里就发挥了它的作用。 我们带上key再来看一下效果:

...const myVnode1 = h('ul', {}, [    h('li', { key: "A" }, 'A'),    h('li', { key: "B" }, 'B'),    h('li', { key: "C" }, 'C'),    h('li', { key: "D" }, 'D')])patch(container, myVnode1)const myVnode2 = h('ul', {}, [    h('li', { key: "E" }, 'E'),    h('li', { key: "A" }, 'A'),    h('li', { key: "B" }, 'B'),    h('li', { key: "C" }, 'C'),    h('li', { key: "D" }, 'D'),])...

Vue中的虚拟DOM和Diff算法实例分析

点击改变 DOM 结构

Vue中的虚拟DOM和Diff算法实例分析

看到上面的结果,此时此刻,你是不是恍然大悟了,顿时知道了key在循环当中有什么作用了吧。我们可以推出的结论一就是:key是当前节点的唯一标识,告诉 diff算法,在更改前后它们是同一个 DOM节点

当我们修改父节点,此时新旧虚拟DOM的父节点不是同一个节点,继续来观察一下 diff算法是如何分析的

const myVnode1 = h('ul', {}, [    h('li', { key: "A" }, 'A'),    h('li', { key: "B" }, 'B'),    h('li', { key: "C" }, 'C'),    h('li', { key: "D" }, 'D')])patch(container, myVnode1)const myVnode2 = h('ol', {}, [    h('li', { key: "A" }, 'A'),    h('li', { key: "B" }, 'B'),    h('li', { key: "C" }, 'C'),    h('li', { key: "D" }, 'D'),])

Vue中的虚拟DOM和Diff算法实例分析

点接改变 DOM结构

Vue中的虚拟DOM和Diff算法实例分析

你会发现,这里将旧节点进行了全部的拆除,然后重新将新节点上树。我们可以推出的结论二就是:
只有是同一个虚拟节点diff算法 才进行精细化比较,否则就是暴力删除旧的、插入新的。判断同一个虚拟节点的依据:选择器(sel)相同且key相同。

那么如果是同一个虚拟节点,但是子节点里面不是同一层在比较的呢?

const myVnode1 = h('div', {}, [    h('li', { key: "A" }, 'A'),    h('li', { key: "B" }, 'B'),    h('li', { key: "C" }, 'C'),    h('li', { key: "D" }, 'D')])patch(container, myVnode1)const myVnode2 = h('div', {}, h('section', {},    [        h('li', { key: "A" }, 'A'),        h('li', { key: "B" }, 'B'),        h('li', { key: "C" }, 'C'),        h('li', { key: "D" }, 'D'),    ]))

Vue中的虚拟DOM和Diff算法实例分析

点击改变DOM结构

Vue中的虚拟DOM和Diff算法实例分析

你会发现,此时DOM结构同多了一层 section标签 包裹着,然后,文本的内容也发生了变化,所以我们可以推出结论三
diff算法 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,不进行精细化比较,而是暴力删除旧的、然后插入新的。

综上,我们得出diff算法的三个结论:

看到这里,相信你已经对 diff算法 已经有了很大的收获了。

patch 函数

patch函数 的主要作用就是:判断是否是同一个节点类型,是就在进行精细化对比,不是就进行暴力删除,插入新的
我们在可以简单的画出patch函数现在的主要流程图如下:

Vue中的虚拟DOM和Diff算法实例分析

// patch.js    patch函数import vnode from "./vnode";import sameVnode from "./sameVnode";import createElement from "./createElement";export default function (oldVnode, newVnode) {    // 判断oldVnode是否是虚拟节点    if (oldVnode.sel == '' || oldVnode.sel == undefined) {        // console.log('不是虚拟节点');        // 创建虚拟DOM        oldVnode = emptyNodeAt(oldVnode)    }    // 判断是否是同一个节点    if (sameNode(oldVnode, newVnode)) {        console.log('是同一个节点');    } else {        // 暴力删除旧节点,插入新的节点        // 传入两个参数,创建的节点 插入到指定标杆的位置        createElement(newVnode, oldVnode.elm)    }}// 创建虚拟DOMfunction emptyNodeAt (elm) {    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)}

在进行上DOM上树之前,我们需要了解一下DOM中的insertBefore()方法、appendChild()方法,因为,只有你真正的知道它们两者的用法,才会让你在下面手写上树的时候更加的清晰。

appendChild()方法

appendChild() 方法:可以向节点的子节点列表的末尾添加新的子节点。比如:appendChild(newchild)。
注意: appendChild()方法是在父节点中的子节点的末尾添加新的节点。(相对于父节点来说)。

<body>    <div class="box">        <span>青峰</span>        <ul>            <li>1</li>            <li>2</li>        </ul>    </div>    <script>        const box = document.querySelector('.box')        const appendDom = document.createElement('div')        appendDom.style.backgroundColor = 'blue'        appendDom.style.height = 100 + 'px'        appendDom.style.width = 100 + 'px'        // 在box里面的末尾追加一个div        box.appendChild(appendDom)    </script></body>

Vue中的虚拟DOM和Diff算法实例分析

你会发现,创建的div是嵌套在box里面的,div 属于 box 的子节点,box 是 div 的子节点。

insertBefore()方法

insertBefore() 方法:可在已有的字节点前中插入一个新的子节点。比如:insertBefore(newchild,rechild)。
注意: insertBefore()方法是在已有的节点前添加新的节点。(相对于子节点来说的)。

<body>    <div class="box">        <span>青峰</span>        <ul>            <li>1</li>            <li>2</li>        </ul>    </div>    <script>        const box = document.querySelector('.box')        const insertDom = document.createElement('p')        insertDom.innerText = '我是insertDOM'        // 在body中 box前面添加新的节点        box.parentNode.insertBefore(insertDom, box)    </script></body>

Vue中的虚拟DOM和Diff算法实例分析

我们发现,box 和 div 是同一层的,属于兄弟节点。

处理不同节点

sameVnode 函数

作用:比较两个节点是否是同一个节点

// sameVnode.jsexport default function sameVnode(oldVnode, newVnode) {    return (oldVnode.data ? oldVnode.data.key : undefined) === (newVnode.data ? newVnode.data.key : undefined) && oldVnode.sel == newVnode.sel}

手写第一次上树

理解了上面的 appendChild()方法insertBefore()方法之后,我们正式开始让 真实DOM 上树,渲染页面。

// patch.js    patch函数import vnode from "./vnode";import sameVnode from "./sameVnode";import createElement from "./createElement";export default function (oldVnode, newVnode) {    // 判断oldVnode是否是虚拟节点    if (oldVnode.sel == '' || oldVnode.sel == undefined) {        // console.log('不是虚拟节点');        // 创建虚拟DOM        oldVnode = emptyNodeAt(oldVnode)    }    // 判断是否是同一个节点    if (sameNode(oldVnode, newVnode)) {        console.log('是同一个节点');    } else {        // 暴力删除旧节点,插入新的节点        // 传入两个参数,创建的节点 插入到指定标杆的位置        createElement(newVnode, oldVnode.elm)    }}// 创建虚拟DOMfunction emptyNodeAt (elm) {    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)}

上面我们已经明确的知道,patch的作用就是判断是否是同一个节点,所以,我们需要声明一个createElement函数,用来创建真实DOM。

createElement 函数

createElement主要用来 创建子节点的真实DOM。

// createElement.jsexport default function createElement(vnode,pivot) {    // 创建上树的节点    let domNode = document.createElement(vnode.sel)    // 判断有文本内容还是子节点    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {        // 文本内容 直接赋值        domNode.innerText = vnode.text        // 上树 往body上添加节点        // insertBefore() 方法:可在已有的字节点前中插入一个新的子节点。相对于子节点来说的        pivot.parentNode.insertBefore(domNode, pivot)    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {        // 有子节点    }}
// index.jsimport patch from "./mysnabbdom/patch";import h from './mysnabbdom/h'const container = document.getElementById("container");//创建虚拟节点const myVnode1 = h('h2', {}, '文字')patch(container, myVnode1)

Vue中的虚拟DOM和Diff算法实例分析

我们已经将创建的真实DOM成功的渲染到页面上去了,但是这只是实现了最简单的一种情况,那就是 h函数 第三个参数是字符串的情况,所以,当第三个参数是一个数组的时候,是无法进行上树的,下面我们需要将 createElement函数 再进一步的优化,实现递归上树。

递归创建子节点

我们发现,在第一次上树的时候,createElement函数 有两个参数,分别是:newVnode (新的虚拟DOM),标杆(用来上树插入到某个节点的位置),在createElement内部 我们是使用 insertBefore()方法 进行上树的,使用这个方法我们需要知道已有的节点是哪一个,当然,当有 text(第三个参数是字符串或数字)的时候,我们是可以找到要插入的位置的,但是当有 children(子节点)的时候,我们是无法确定标杆的位置的,所以,我们要将上树的工作放到 patch函数 中,即 createElement函数只负责创建节点

// index.jsimport patch from "./mysnabbdom/patch";import h from './mysnabbdom/h'const container = document.getElementById("container");//创建虚拟节点const myVnode1 = h('ul', {}, [    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D')])patch(container, myVnode1)
// patch.jsimport vnode from "./vnode";import sameVnode from "./sameVnode";import createElement from "./createElement";export default function (oldVnode, newVnode) {    // 判断oldVnode是否是虚拟节点    if (oldVnode.sel == '' || oldVnode.sel == undefined) {        // console.log('不是虚拟节点');        // 创建虚拟DOM        oldVnode = emptyNodeAt(oldVnode)    }    // 判断是否是同一个节点    if (sameNode(oldVnode, newVnode)) {        console.log('是同一个节点');    } else {        // 暴力删除旧节点,插入新的节点        // 传入参数为创建的虚拟DOM节点  返回以一个真实DOM        let newVnodeElm = createElement(newVnode)        console.log(newVnodeElm);        // oldVnode.elm.parentNode 为body 在body中 在旧节点的前面添加新的节点        if (oldVnode.elm.parentNode && oldVnode.elm) {            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)        }        // 删除老节点        oldVnode.elm.parentNode.removeChild(oldVnode.elm)    }}// 创建虚拟DOMfunction emptyNodeAt (elm) {    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)}

完善 createElement 函数

//  createElement.js只负责创建真正节点 export default function createElement(vnode) {    // 创建上树的节点    let domNode = document.createElement(vnode.sel)    // 判断有文本内容还是子节点    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {        // 文本内容 直接赋值        domNode.innerText = vnode.text        // 上树 往body上添加节点        // insertBefore() 方法:可在已有的字节点前中插入一个新的子节点。相对于子节点来说的    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {        // 有子节点        for(let i = 0; i < vnode.children.length; i++) {            // console.log(vnode.children[i]);            let ch = vnode.children[i]            // 进行递归 一旦调用createElement意味着 创建了DOM 并且elm属性指向了创建好的DOM            let chDom = createElement(ch)            // 添加节点 使用appendChild 因为遍历下一个之前 上一个真实DOM(这里的domVnode)已经生成了 所以可以使用appendChild            domNode.appendChild(chDom)        }    }    vnode.elm = domNode    return vnode.elm}

经过上面的分析,我们已经完成了对createElem函数的完善,可能你对这个递归有点不了解,那么大概捋一下进行的过程:

执行上面的代码,测试结果如下:

Vue中的虚拟DOM和Diff算法实例分析

完美!我们成功的将递归子节点完成了,无论嵌套多少层,我们都可以通过递归将子节点渲染到页面上。

前面,我们实现了不是同一个节点的时候,进行删除旧节点和插入新节点的操作,下面,我们来实现是相同节点时的相关操作,这也是文章中最重要的部分,diff算法 就包含在其中!!!

处理相同节点

上面的 patch函数 流程图中,我们已经处理了不同节点的时候,进行暴力删除旧的节点,然后插入新的节点,现在我们进行处理相同节点的时候,进行精细化的比较,继续完善 patch函数 的主流程图:

Vue中的虚拟DOM和Diff算法实例分析

看到上面的流程图,你可能会有点疑惑,为什么不在 newVnode 是否有 Text属性 中继续判断 oldVnode 是否有 children 属性而是直接判断两者之间的 Text 是否相同,这里需要提及一个知识点,当我们进行 DOM操作的时候,文本内容替换DOM的时候,会自动将DOM结构全部销毁掉,innerText改变了,DOM结构也会随之被销毁,所以这里可以不用判断 oldVnode 是否存在 children 属性,如果插入DOM节点,此时的Text内容并不会被销毁掉,所以我们需要手动的删除。这也是为什么在流程图后面,我们添加 newVnode 的children 的时候需要将 oldVnode 的 Text 手动删除,而将 newVnode 的 Text 直接赋值oldVnode.elm.innerText 的原因。
知道上面流程图是如何工作了,我们继续来书写patch函数中是同一个节点的代码。

// patch.jsimport vnode from "./vnode";import sameVnode from "./sameVnode";import createElement from "./createElement";export default function (oldVnode, newVnode) {    // 判断oldVnode是否是虚拟节点    if (oldVnode.sel == '' || oldVnode.sel == undefined) {        // console.log('不是虚拟节点');        // 创建虚拟DOM        oldVnode = emptyNodeAt(oldVnode)    }    // 判断是否是同一个节点    if (sameNode(oldVnode, newVnode)) {        console.log('是同一个节点');        // 是否是同一个对象        if (oldVnode === newVnode) return        // newVnode是否有text        if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) {            // 判断newVnode和oldVnode的text是否相同            if (!(newVnode.text === oldVnode.text)) {                // 直接将text赋值给oldVnode.elm.innerText 这里会自动销毁oldVnode的cjildred的DOM结构                oldVnode.elm.innerText = newVnode.text            }            // 意味着newVnode有children        } else {            // oldVnode是否有children属性            if (oldVnode.children != undefined && oldVnode.children.length > 0) {                // oldVnode有children属性            } else {                // oldVnode没有children属性                // 手动删除 oldVnode的text                oldVnode.elm.innerText = ''                // 遍历                for (let i = 0; i < newVnode.children.length; i++) {                    let dom = createElement(newVnode.children[i])                    // 追加到oldvnode.elm中                    oldVnode.elm.appendChild(dom)                }            }        }    } else {        // 暴力删除旧节点,插入新的节点        // 传入参数为创建的虚拟DOM节点  返回以一个真实DOM        let newVnodeElm = createElement(newVnode)        console.log(newVnodeElm);        // oldVnode.elm.parentNode 为body 在body中 在旧节点的前面添加新的节点        if (oldVnode.elm.parentNode && oldVnode.elm) {            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)        }        // 删除老节点        oldVnode.elm.parentNode.removeChild(oldVnode.elm)    }}// 创建虚拟DOMfunction emptyNodeAt(elm) {    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)}
....//创建虚拟节点const myVnode1 = h('ul', {}, 'oldVnode有text')patch(container, myVnode1)const myVnode2 = h('ul', {}, [    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D')])btn.addEventListener('click', () => {    patch(myVnode1, myVnode2)})

oldVnode 有 tex属性 和 newVnode 有 children属性 的效果如下:

Vue中的虚拟DOM和Diff算法实例分析

Vue中的虚拟DOM和Diff算法实例分析

...//创建虚拟节点const myVnode1 = h('ul', {}, [    h('li', {}, 'A'),    h('li', {}, 'B'),    h('li', {}, 'C'),    h('li', {}, 'D')])patch(container, myVnode1)const myVnode2 = h('ul', {}, 'newVnode 有text')btn.addEventListener('click', () => {    patch(myVnode1, myVnode2)})

oldVode 有children属性 和 newVnode 有 text属性 的效果如下:

Vue中的虚拟DOM和Diff算法实例分析

Vue中的虚拟DOM和Diff算法实例分析

完美!现在我们就只差最后diff算了。

patchVnode 函数

在patch函数中,我们需要将同同一节点的比较分成一个单独的模块patchVnode函数,方便我们在diff算法中进行递归运算。

patchVnode函数的主要作用就是:

// patchVnode.jsexport default function patchVnode(oldVnode, newVnode) {    // 是否是同一个对象    if (oldVnode === newVnode) return    // newVnode是否有text    if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) {        // 判断newVnode和oldVnode的text是否相同        if (!(newVnode.text === oldVnode.text)) {            // 直接将text赋值给oldVnode.elm.innerText 这里会自动销毁oldVnode的cjildred的DOM结构            oldVnode.elm.innerText = newVnode.text        }        //说明 newVnode有 children    } else {        // oldVnode是否有children属性        if (oldVnode.children != undefined && oldVnode.children.length > 0) {            // oldVnode有children属性        } else {            // oldVnode没有children属性            // 手动删除 oldVnode的text            oldVnode.elm.innerText = ''            // 遍历            for (let i = 0; i < newVnode.children.length; i++) {                let dom = createElement(newVnode.children[i])                // 追加到oldvnode.elm中                oldVnode.elm.appendChild(dom)            }        }    }}

diff算法

精细化比较:diff算法 四种优化策略这里使用双指针的形式进行diff算法的比较,分别是旧前、旧后、新前、新后指针,(前指针往下移动,后指针往上移动

四种优化策略:(命中:key 和 sel 都要相同)

注意: 当只有第一种不命中的时候才会采取第二种,依次类推,如果四种都不命中,则需要通过循环来查找。命中指针才会移动,否则不移动。

①、新前与旧前

Vue中的虚拟DOM和Diff算法实例分析 如果就旧节点先循环完毕,说明需要新节点中有需要插入的节点

②、新后与旧后

Vue中的虚拟DOM和Diff算法实例分析

如果新节点先循环完毕,旧节点还有剩余节点,说明旧节点中有需要删除的节点。

多删除情况:Vue中的虚拟DOM和Diff算法实例分析当只有情况①命中,剩下三种都没有命中,则需要进行循环遍历,找到旧节点中对应的节点,然后在旧的虚拟节点中将这个节点设置为undefined。删除的节点为旧前与旧后之间(包含旧前、旧后)。

③、新后与旧前

Vue中的虚拟DOM和Diff算法实例分析③新后与旧前命中的时候,此时要移动节点,移动 新后指向的节点到旧节点的 旧后的后面,并且找到旧节点中对应的节点,然后在旧的虚拟节点中将这个节点设置为undefined

④、新前与旧后

Vue中的虚拟DOM和Diff算法实例分析

④新前与旧后命中的时候,此时要移动节点,移动新前指向的这个节点到旧节点的 旧前的前面,并且找到旧节点中对应的节点,然后在旧的虚拟节点中将这个节点设置为undefined

好了,上面通过动态讲解的四种命中方式之后,动态gif图片有水印,看着可能不是很舒服,但当然能够理解是最重要的,那么我们开始手写 diff算法 的代码。

updateChildren 函数

updateChildren()方法 主要作用就是进行精细化比较,然后更新子节点。这里代码比较多,需要耐心的阅读。

import createElement from "./createElement";import patchVnode from "./patchVnode";import sameVnode from "./sameVnode";export default function updateChildren(parentElm, oldCh, newCh) {    //parentElm 父节点位置 用来移动节点 oldCh旧节点children newCh新节点children    // console.log(parentElm, oldCh, newCh);    // 旧前    let oldStartIndex = 0    // 旧后    let oldEndIndex = oldCh.length - 1    // 新前    let newStartIndex = 0    // 旧后    let newEndIndex = newCh.length - 1    // 旧前节点    let oldStartVnode = oldCh[0]    // 旧后节点    let oldEndVnode = oldCh[oldEndIndex]    // 新前节点    let newStartVnode = newCh[0]    // 新后节点    let newEndVnode = newCh[newEndIndex]    // 存储mapkey    let keyMap    // 循环 条件 旧前 <= 旧后 && 新前 <= 新后    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {        // 首先需要判断是否已经处理过了         if (oldCh[oldStartIndex] == undefined) {            oldStartVnode = oldCh[++oldStartIndex]        } else if (oldCh[oldStartIndex] == undefined) {            oldEndVnode = oldCh[--oldEndIndex]        } else if (newCh[newStartIndex] == undefined) {            newStartVnode = newCh[++newStartIndex]        } else if (newCh[newEndIndex] == undefined) {            newEndVnode = newCh[--newEndIndex]        } else if (sameVnode(oldStartVnode, newStartVnode)) {            // ①、新前与旧前命中            console.log('①、新前与旧前命中');            //调用 patchVnode 对比两个节点的 对象 文本 children            patchVnode(oldStartVnode, newStartVnode)            // 指针下移改变节点            oldStartVnode = oldCh[++oldStartIndex]            newStartVnode = newCh[++newStartIndex]        } else if (sameVnode(oldEndVnode, newEndVnode)) {            // ②、新后与旧后命中            console.log('②、新后与旧后命中');            //调用 patchVnode 对比两个节点的 对象 文本 children            patchVnode(oldStartVnode, newStartVnode)            // 指针下移并改变节点            oldEndVnode = oldCh[--oldEndIndex]            newEndVnode = newCh[--newEndIndex]        } else if (sameVnode(oldStartVnode, newEndVnode)) {            // ③、新后与旧前命中            patchVnode(oldStartVnode, newEndVnode)            console.log(newEndVnode);            // 移动节点 当③新后与旧前命中的时候,此时要移动节点,            // 移动 新后(旧前两者指向的是同一节点) 指向的节点到旧节点的 旧后的后面,并且找到旧节点中对应的节点,然后在旧的虚拟节点中将这个节点设置为undefined。            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)            // 在上面动画中 命中③是在旧节点的后面插入的 所以使用nextSibling            // 指针下移并改变节点            oldStartVnode = oldCh[++oldStartIndex]            newEndVnode = newCh[--newEndIndex]        } else if (sameVnode(oldEndVnode, newStartVnode)) {            // ④、新前与旧后命中            patchVnode(oldEndVnode, newStartVnode)            // 移动节点            // 当`④新前与旧后`命中的时候,此时要移动节点,移动`新前(旧后指向同一个节点)`指向的这个节点到旧节点的 `旧前的前面`,            //并且找到`旧节点中对应的节点`,然后在`旧的虚拟节点中将这个节点设置为undefined`            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)            //指针下移并改变节点            oldEndVnode = oldCh[--oldEndIndex]            newStartVnode = newCh[++newStartIndex]        } else {            // 四种都没有命中            console.log(11);            //kepMap作为缓存不用每次遍历对象            if (!keyMap) {                keyMap = {}                // 遍历旧的节点                for (let i = oldStartIndex; i <= oldEndIndex; i++) {                    // 获取旧节点的key                    const key = oldCh[i].data.key                    if (key != undefined) {                        //key不为空 并且将key存放到keyMap对象中                        keyMap[key] = i                    }                }            }            // 取出newCh中的的key 并找出在keyMap中的位置 并映射到oldCh中            const oldIndex = keyMap[newStartVnode.key]            if (oldIndex == undefined) {                // 新增                console.log(111);                parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)            } else {                // 移动位置                // 取出需要移动的项                const elmToMove = oldCh[oldIndex]                // 判断是选择器是否一样                patchVnode(elmToMove, newStartVnode)                // 标记已经处理过了                oldCh[oldIndex] = undefined                // 移动节点 移动到旧前前面 因为旧前与旧后之间要被删除                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)            }            // 只移动新的节点            newStartVnode = newCh[++newStartIndex]        }    }    //循环结束 还有剩余节点没处理    if (newStartIndex <= newEndIndex) {        //说明 新节点还有未处理的节点,意味着需要添加节点        console.log('新增节点');        // 创建标杆         console.log(newCh[newEndIndex + 1]);        // 节点插入的标杆位置 官方源码的写法 但是我们写代码新的虚拟节点中,elm设置了undefined 所以这里永远都是会在后面插入 小bug        // let before = newCh[newEndIndex + 1] == null ? null : newCh[newEndIndex + 1].elm        // 若不想出现bug 可以在插入节点中直接oldCh[oldStartIndex].elm 但是也会出现不一样的bug 所以重在学习思路        for (let i = newStartIndex; i <= newEndIndex; i++) {            // 插入节点 因为旧节点遍历完之后 新节点还有剩余节点 这里需要使用crateElement函数新建一个真实DOM节点            // insertBefore会自动识别null            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIndex].elm)        }    } else if (oldStartIndex <= oldEndIndex) {        //说明旧节点还有剩余节点还没有处理 意味着需要删除节点        console.log('删除节点');        for (let i = oldStartIndex; i <= oldEndIndex; i++) {            if(oldCh[i]) parentElm.removeChild(oldCh[i].elm)        }    }}

关于“Vue中的虚拟DOM和Diff算法实例分析”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Vue中的虚拟DOM和Diff算法实例分析”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯