虚拟DOM和diff算法

本文深入解析了虚拟DOM的工作原理,介绍了H函数在生成虚拟节点中的关键作用,以及snabbdom库的简介和在diff算法中的应用。通过手写简单diff算法,探讨了DOM更新的最小化策略,涉及节点比较、层次结构和唯一标识符key。
摘要由CSDN通过智能技术生成


snabbdom简介:

snabbdom是瑞典语单词,单词原意:"速度";
snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;
snabbdom库是DOM库,当然不能在node.js环境运行,所以我们需要搭建webpack和web

虚拟DOM和H函数

虚拟DOM:

用javaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性
真实DOM
<div class="box">
    <h3>我是一个标题</h3>
    <ul>
        <li>牛奶</li>
        <li>咖啡</li>
        <li>可乐</li>
    </ul>
</div>
虚拟DOM
  {
        "sel":"div",                    // 对应真实DOM的div标签
        "data":{                        // 对应真实DOM的div的属性
            "class":{"box":true}
        },
        "children":[                    // 对应真实DOM的div的children子元素
            {
                "sel":"h3",             // 对应真实DOM的h3标签
                "data":{},              // 对应真实DOM的h3的属性
                "text":"我是一个标题"    // 对应真实DOM的h3的内容
            },
            {
                "sel":"ul",             // 对应真实DOM的ul标签
                "data":{},              // 对应真实DOM的ul的属性
                "children":[            // 对应真实DOM的ul的children元素
                    {"sel":"li","data":{},"text":"牛奶"},
                    {"sel":"li","data":{},"text":"咖啡"},
                    {"sel":"li","data":{},"text":"可乐"},
                ]
            }
        ]
    }

diff是发生在虚拟DOM上的

新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上。

旧虚拟DOM

  {
        "sel":"div",                   
        "data":{                       
            "class":{"box":true}
        },
        "children":[                   
            {
                "sel":"h3",            
                          
                "text":"我是一个标题"    
            },
            {
                "sel":"ul",           
                "data":{},             
                "children":[           
                    {"sel":"li","data":{},"text":"牛奶"},
                    {"sel":"li","data":{},"text":"咖啡"},
                    {"sel":"li","data":{},"text":"可乐"},
                ]
            }
        ]
    }
新的虚拟DOM和旧的虚拟DOM进行对比,这就是diff算法,最后反映到真实的DOM上

新虚拟DOM

    {
        "sel":"div",                   
        "data":{                       
            "class":{"box":true}
        },
        "children":[                   
            {
                "sel":"h3",            
                "text":"我是一个标题"    
            },
            {
                "sel":"span",             
                "text":"我是一个新的span"    
            },
            {
                "sel":"ul",           
                "data":{},             
                "children":[           
                    {"sel":"li","data":{},"text":"牛奶"},
                    {"sel":"li","data":{},"text":"咖啡"},
                    {"sel":"li","data":{},"text":"可乐"},
                    {"sel":"li","data":{},"text":"雪碧"},
                ]
            }
        ]
    }

在diff算法的时候,他会发现新的虚拟DOM和旧的虚拟DOM的不同,然后找到不同的地方,发现新的虚拟DOM内的div内新建了个span,他就会命令DOM操作createElement创建节点,然后appendChild添加节点。下面的ul内的li也是一样

DOM如何变为虚拟DOM

    这个属于模板编译原理范畴

虚拟DOM如何被渲染函数(h函数)生产

h函数用来产生虚拟节点(模板编译)

h 函数用来产生虚拟节点(vnode)v:virtual表示虚拟的意思,node:节点
比如这样调用h函数:
    h('a',{props:{href:'https://www.baidu.com'}},'百度')
    a:表示需要创建的节点标签
    props:表示标签的属性
    百度:表示显示的文本
将得到这样的虚拟节点:
{“sel":"a","data":{"props":{href:'https://www.baidu.com'}},"text":"百度"}
真实的DOM节点
<a href="https://www.baidu.com">百度</a>
一个虚拟节点的属性
children:表示子元素,如果是undfined就是表示他没有子元素
data:表示他的属性  ,calss  style...
elm:表示这个元素对应的真实DOM节点,如果是undfined就说明虚拟DOM还没上树
key:表示这个节点的唯一标识
sel:表示选择器 select
text:表示这个虚拟节点的文本内容

h函数可以嵌套使用,从而得到虚拟DOM数(重要)

 h函数是可以嵌套的比如
    h('ul',{},[
        h('li',{},'牛奶'),
        h('li',{},'咖啡'),
        h('li',{},'可乐')
    ])
    虚拟DOM{
        "sel":"ul",
        "data":{},
        "children":[
            {
                "sel":"li",
                "text":"牛奶"
            },
            {
                "sel":"li",
                "text":"咖啡"
            },
            {
                "sel":"li",
                "text":"可乐"
            },
        ]
    }

使用h函数简单实现真实DOM树

  第一步:先引入snabbdom里面的方法
    import {
            init,
            classModule,
            propsModule,
            styleModule,
            eventListenersModule,
            h,
    } from "snabbdom";
    // 创建patch函数
            var patch = init([classModule, propsModule, styleModule, eventListenersModule])
    //创建虚拟节点
    const myVnode3 = h('ul', [
        h('li', '牛奶'),
        h('li', [
            h('div', [
                h('p', '咖啡'),
                h('p', '雪碧')
            ])
        ]),
        h('li', [h('p', '可乐')])
    ])
    // 让虚拟节点上树
    const container = document.getElementById('container')
    patch(container, myVnode3)

diff算法原理

手写一个简单的diff算法

1、先创建一个h.js

    import vnode from "./vnode"
    // 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可,
    // 相当于它的重载功能弱
    // 也就是说,调用的时候形态必须是下面的三种之一
    // 形态一:h('div',{},'文字')
    // 形态二:h('div',{},[])
    // 形态三:h('div',{},h())
    /**
    *这里只写了三种情况,因为我们只是简单的使用一个h函数
    *
    */
    export default function (sel, data, c) {
        //  这个函数用到了递归,自己调用自己
        /**
        *sel必须是选择器
        *data必须是一个对象
        *c可以是string,array,object
        */
        // console.log(Array.isArray(c));
        // 检查参数的个数
        if (arguments.length != 3)
        /**
        *先判断传过来的数据长度是否等于3位  因为我们只判断3位
        */
            throw new Error('对不起,h函数必须传入3个参数,我们是低配版 的h 函数');
        // 检查c的类型
        if (typeof c === 'string' || typeof c === 'number') {
            // 说明现在调用h函数是形态一
            // 如果c的类型是string 或者number 就说明传过来的是文本
            return vnode(sel, data, undefined, c, undefined);
        } else if (Array.isArray(c)) {
            // 说明现在调用的是h函数形态2
            // 如果这里为true  说明c是一个数组,是sel的子元素
            // 是Array我们就需要循环遍历,判断子元素内的情况。
            let children = []
            // 遍历数组C
            for (let i = 0; i < c.length; i++) {
                // 检查c[i]必须是一个对象
                // 如果c[i]的每一项有一项不是对象,并且不是一个h函数,
                // 不拥有选择器sel属性,这里要报错,[13131]如果你这样写  他虽然是个数组不错
                // ,但是他不是一个对象 并且不拥有sel选择器所以需要报错!!!!
                if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))
                    throw new Error('传入的数组有项不是h函数');
                // 这里不是c[i]执行
                // 这里如果他是一个h函数,就把每一项push进对应的children内
                children.push(c[i])
            }
            return vnode(sel, data, children, undefined, undefined)
        } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
            // 说明现在调用的是h函数形态3
              // 如果c[i]的每一项有一项是对象,并且是一个h函数,拥有选择器sel属性,这里要报错,[h('sel',{},'文本')]或者h('sel',{},'text')如果你这样写  他是个数组, 并且拥有sel选择器
            let children = [c]
            return vnode(sel, data, children, undefined, undefined)
        } else {
            throw new Error('传入的第三个参数不对')
        }

    }
2、创建一个vnode.js
   export default function (sel, data, children, text, elm) {
       return {
           sel, data, children, text, elm
       }
    }
    sel:选择器
    data:属性
    children:子元素
    text:文本
    elm:表示这个元素对应的真实DOM节点,如果是undfined就说明虚拟DOM还没上树
创建一个index.js
 import h from "./mysnabbdom/h"
    var myVnode1 = h('div', {}, [
        h('p', {}, '哈哈'),
        h('p', {}, '哈哈'),
        h('p', {}, h('span', {}, 'A'))
    ])
    var myVnode2 = h('ul', {}, [
        h('li', {}, '西瓜'),
        h('li', {}, '苹果'),
        h('li', {}, [
            h('div', {}, [
                h('span', {}, '香蕉')
            ])
        ]),
        h('li',{},h('span',{},'文字'))
    ])
    console.log(myVnode2);

diff算法心得

1、只有同一层时才会最小量更新,当然key是必须得,key是这个节点的唯一标识,告诉diff算法,在更改前后
它们是同一个DOM节点
2、只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的节点。如何定是同一个虚拟节点?
选择器相同且key相同。
    import {
            init,
            classModule,
            propsModule,
            styleModule,
            eventListenersModule,
            h,
        } from "snabbdom";
        // 创建patch函数
        const patch = init([classModule, propsModule, styleModule, eventListenersModule])
        var btn = document.getElementById('btn')
        const vnode1 = h('ul', {}, [
            h('li', {
                key: "A"
            }, 'A'),
            h('li', {
                key: "B"
            }, 'B'),
            h('li', {
                key: "C"
            }, 'C'),
            h('li', {
                key: "D"
            }, 'D')
        ])
        patch(container, vnode1)
        const vnode2 = h('ol', {}, [
            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'),
        ])
        btn.onclick = function () {
            patch(vnode1, vnode2)
        }
3、只进行同层比较,不会进行跨层比较。即使是同一个虚拟节点,但是跨层了,对不起,精细化比较不会diff你。
而是暴力删除旧的,然后插入新的。
 const vnode1 = h('div', {}, [
           h('p', {
               key: "A"
           }, 'A'),
           h('p', {
               key: "B"
           }, 'B'),
           h('p', {
               key: "C"
           }, 'C'),
           h('p', {
               key: "D"
           }, 'D')
        )
        DOM<div>
                <p>A</p>
                <p>B</p>
                <p>C</p>
                <p>D</p>
            </div>
        const vnode2 = h('div', {}, h('section', {}, [
            h('p', {
                key: 'A'
            }, 'A'),
            h('p', {
                key: 'B'
            }, 'B'),
            h('p', {
                key: 'C'
            }, 'C'),
            h('p', {
                key: 'D'
            }, 'D'),
        ]))
        DOM<div>
                <section>
                    <p>A</p>
                    <p>B</p>
                    <p>C</p>
                    <p>D</p>
                </section>
            </div>
只能比同层级,如果不是就是把旧的删除,创建新的节点,不是同层级,只要父级更新,子级也会更新。把上面的四
个拆除,创建一个新的祖级是div,父级是section,子级是p
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值