• 【前端面试必知】虚拟DOM与diff算法详解


    前言

    本系列主要整理前端面试中需要掌握的知识点。本节介绍虚拟DOM与diff算法详解。
    知识主要从尚硅谷中学习,并且参考了大神YK菌的博客。


    一、snabbdom介绍与环境搭建

    1、snabbdom

    • snabbdom是虚拟DOM库,提供了一系列操作虚拟DOM需要的方法;
    • 安装snabbdom:
    npm install -S snabbdom
    
    • 1
    • 注意:视频中老师使用snabbdom是2.1.0版本,其中有exports字段,但是截至2022年6月27日,snabbdom的最新版本是3.5.1,直接下载3.5.1就可以,不要下载2.1.0版本,后面和webpack会不兼容报错(亲测!!!血泪!!!)

    2、搭建环境

    • 创建文件夹,并安装环境npm install
    • 安装snabbdom,npm install -S snabbdom
    • 安装webpack5并配置,cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3,(npm不能安装webpack-cli@3);
    • 配置webpack5,创建文件webpack.config.js
    module.exports = {
        // webpack5 不用配置mode
        // 入口
        entry: "./src/index.js",
        // 出口
        output: {
          // 虚拟打包路径,文件夹不会真正生成,而是在8080端口虚拟生成
          publicPath: "xuni",
          // 打包出来的文件名
          filename: "bundle.js",
        },
        // 配置webpack-dev-server
        devServer: {
          // 静态根目录
          contentBase: 'www',
          // 端口号
          port: 8080,
        },
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 复制官方demo,并在www中创建一个index.html,里面放一个id为container的盒子。修改demo中没有定义的函数为空函数占位,确保可以直接跑起来。
    //src.index.js
    import {
      init,
      classModule,
      propsModule,
      styleModule,
      eventListenersModule,
      h,
    } from "snabbdom";
    
    const patch = init([
      // Init patch function with chosen modules
      classModule, // makes it easy to toggle classes
      propsModule, // for setting properties on DOM elements
      styleModule, // handles styling on elements with support for animations
      eventListenersModule, // attaches event listeners
    ]);
    
    const container = document.getElementById("container");
    
    const vnode = h("div#container.two.classes", { on: { click: function () { } } }, [
      h("span", { style: { fontWeight: "bold" } }, "This is bold"),
      " and this is just normal text",
      h("a", { props: { href: "/foo" } }, "I'll take you places!"),
    ]);
    // Patch into empty DOM element – this modifies the DOM as a side effect
    patch(container, vnode);
    
    const newVnode = h(
      "div#container.two.classes",
      { on: { click: function () { } } },
      [
        h(
          "span",
          { style: { fontWeight: "normal", fontStyle: "italic" } },
          "This is now italic type"
        ),
        " and this is still just normal text",
        h("a", { props: { href: "/bar" } }, "I'll take you places!"),
      ]
    );
    // Second `patch` invocation
    patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 能够成功允许即为配置成功

    二、h函数的介绍与手写

    在这里插入图片描述

    1、h函数的使用

    • 基本使用
    const container = document.getElementById("container")
    //创建虚拟节点
    const myVnode1 = h('ul', {}, [
        h('li', {},'A'),
        h('li', {},'B'),
        h('li', {},'C'),
        h('li', {},'D'),
    ])
    //节点上树,更新DOM
    patch(container, myVnode1)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 虚拟节点vnode的属性
      • sel:“” 选择器;
      • data:{} 属性、样式、key;
      • children:undefined 子元素,数组;
      • text:“” 文本内容;
      • elm:undefined 对应真正dom节点,undefined表示还没有上dom树;

    2、手写h函数

    • 手写vnode.js:将传入的参数组合成对象返回
    export default function (sel,data,children,text,elm) {
        return {sel,data,children,text,elm}
    }
    
    • 1
    • 2
    • 3
    • 手写h.js:
    import vnode from './vnode.js'
    
    // 傻瓜h函数,只能接受3个参数,正常中间的{}为空是可以省略的,但我们就直接写死三个参数,方便分析
    export default function (sel, data, c) { 
        if (arguments.length != 3) {
            throw new Error('对不起,h函数必须传入3个参数');
        }
        // 最后一个参数是文本或者数字,就是text内容
        if (typeof c == 'string' || typeof c == 'number') {
            return vnode(sel, data, undefined, c, undefined)
        } else if (Array.isArray(c)) {
            // 数组就是里面有好多h函数,遍历即可
            let children = [];
            for (let i = 0; i < c.length; i++){
                if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) { 
                    throw new Error('传入的数组参数中有项不是h函数')
                }
                children.push(c[i])
            }
            return vnode(sel,data,children,undefined,undefined)
        } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
            // 对象就是只有一个h函数,因为h函数返回的是对象,包裹成数组,再执行即可
            let children = [c];
            return vnode(sel,data,children,undefined,undefined)
    
        } else { 
            throw new Error('第三个参数类型不对')
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • index.js
    import h from './mysnabbdom/h'
    
    var myVnode1 = h('div', {}, h('div', {}, '文字'),)
    console.log(myVnode1);
    
    • 1
    • 2
    • 3
    • 4

    三、手写diff算法

    1、diff算法原理

    • 最小量更新,key很关键。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。
    • 只是同一个虚拟节点,才进行精细化比较(往ul中的 li 添加 li),否则就是暴力删除旧的、插入新的(ul中的li 换到在 ol 中去)
    • 只进行同层比较,不会进行跨层比较。即使是同一片 虚拟节点,但是跨层了,diff就是暴力删除旧的,然后插入新的。
    • 前期流程
      在这里插入图片描述

    2、新旧节点不是同一个节点时:直接暴力删除

    • patch.js
    import vnode from "./vnode";
    import createElement from "./createElement";
    // patch函数的功能是判断新节点和老节点是否是同一个节点,如果不是,分别进行精细比较和暴力插入,至于创建节点的事情就交给了createElement做
    // oldVnode:老的虚拟节点
    // newVnode:新的虚拟节点
    
    export default function (oldVnode, newVnode) {
        // 如果传入的老节点不是虚拟节点,就要转换为虚拟节点
        if (oldVnode.sel == '' || oldVnode.sel == undefined) {
            // oldVnode = h(oldVnode.tagName.toLowerCase(), {},[])
            oldVnode = vnode(oldVnode.tagName.toLowerCase(), {},[],undefined,oldVnode)
        }
    
        if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
            console.log('是同一个节点');
        } else {
            console.log('不是同一个节点,暴力插入新的、删除旧的');
            let newVnodeElm = createElement(newVnode);
            if (oldVnode.elm.parentNode && newVnodeElm) {
                oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm)
            }
            oldVnode.elm.parentNode.removeChild(oldVnode.elm)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • createElement.js
    export default function createElement(vnode) { 
        let domNode = document.createElement(vnode.sel)
        if (vnode.text != "" && (vnode.children == undefined || vnode.length == 0)) {
            // 内部是文字
            domNode.innerText = vnode.text;
            vnode.elm = domNode;
        } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { 
            //内部是子节点,就要递归创建节点
            console.log('内部是子节点,就要递归创建节点');
            for (let i = 0; i < vnode.children.length; i++){
                let ch = vnode.children[i]
                console.log(ch);
                let chDOM = createElement(ch);
                domNode.appendChild(chDOM);
                vnode.elm = domNode;
            }
        }
        return vnode.elm
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3、新旧节点是同一个节点时(不考虑五角星内的内容)

    在这里插入图片描述

    • patch.js
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        patchVnode(oldVnode,newVnode)
    }
    
    • 1
    • 2
    • 3
    • 4
    • patchVnode.js
    import createElement from './createElement'
    
    export default function patchVnode(oldVnode, newVnode) {
        // 新旧vnode完全相同,一样一样的
        if (oldVnode === newVnode) return;
        if (newVnode.text != undefined && (newVnode.children === undefined || newVnode.children.length == 0)) {
            if (newVnode.text !== oldVnode.text) {
                // 直接让新text写入老elm中即可
                oldVnode.elm.innerText = newVnode.text;
            }
        } else { 
            // 判断oldVnode有没有children属性
            if (oldVnode.children != undefined && oldVnode.children.length > 0) {
    
            } else { 
                oldVnode.elm.innerHTML = "";
                for (let ch of newVnode.children) { 
                    let chDOM = createElement(ch);
                    oldVnode.elm.appendChild(chDOM)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4、新旧节点是同一个节点时(五角星内最复杂的)

    四种命中查找:
    ①新前与旧前
    ②新后与旧后
    ③新后与旧前(此种发生后,涉及到移动节点,新后指向的节点,移动到旧后之后)
    ④新前与旧后(此种发生后,涉及到移动节点,新前指向的节点,移动到旧前之前)
    命中一种就不再进行命中判断了,如果都没有命中,就需要用循环来寻找,移动到旧前之前。

    在这里插入图片描述

    这部分可以看大神的整理【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren

  • 相关阅读:
    git之分支管理
    图片清晰度增强易语言代码
    ChatGPT技术原理
    D. A Simple Task
    深度学习框架如何优雅地做算子对齐任务?
    MySQL CREATE TABLE 简单设计模板交流
    Java 基础常见面试题
    a_good_idea
    Python中的元类(metaclass)
    MySQL数据库基本操作
  • 原文地址:https://blog.csdn.net/weixin_44337386/article/details/125479162