vue|关于虚拟DOM的那些事🏆🏆

920 阅读5分钟

1、对虚拟DOM的理解

一、什么是虚拟DOM

所谓的虚拟DOM就是用一个js对象,来描述一个DOM节点

    <div class="box">
        <h3>我是一个标题</h3>
        <ul>
            <li>牛奶</li>
            <li>咖啡</li>
            <li>可乐</li>
        </ul>
    </div>
    //虚拟dom
    {
    {
        "sel": "div",
        "data": { "class": { "box": true } }
    "children": [
            {
                "sel": "h3",
                "data": {},
                "text": "我是一个标题"
            },
            {
                "sel": "ul",
                "data": {},
                "children": [
                    { "sel": "li", "data": {}, "text": "牛奶" }
                { "sel": "li", "data": {}, "text": "咖啡" }
                { "sel": "li", "data": {}, "text": "可乐" }
                ]
            }
        ]
    }

操作DOM是非常费时的,所以我们要通过其他的方法来实现,这时候虚拟DOM就出现了

二、为什么要用虚拟DOM

1.用js对象的计算属性换取操作DOM的性能消耗

2.更好的跨平台使用

3.我们对比变化前后的虚拟DOM节点,通过DOM-DIff算法计算出需要更新的地方,然后去更新需要更新的视图,这是虚拟DOM产生的原因和最大的用途

2、diff算法原理

1.涉及对象

【新的虚拟DOM和老的虚拟DOM】

2.步骤

1)对比是否是相同的元素,如果是不同的元素,直接删除,重新创建新的元素

2)如果是相同的元素

  • 对比属性

  • 对比children

    • 新的有儿子,旧的没有儿子

    • 新的没有儿子,旧的有儿子

    • 新旧都是文本节点

    • 新旧都有儿子,采用双指针对比 头头、尾尾、头尾、尾头

3.算法

深度递归+双指针

3、搭建snabbdom

官网

github.com/snabbdom/sn…

安装

npm init // 创建一个身份证
npm i -S snabbdom

思考💡:npm i -D xxx / npm i -S xxx / npm i -g xxx的区别?

安装缩写:

npm i 就是npm install 简写

npm i xxxx -D 就是 npm i xxxx --save-dev,是把依赖写入进devDependencies对象里面

npm i xxxx-S 就是 npm i xxxx--save,是把依赖写入进dependencies对象里面

npm i xxxx-g 就是 全局安装,在命令行的任何地方都可以操作,不会提示“命令不存在等错误”

npm i xxxx 就是本地安装,就是安装到当前命令行下的目录中,但不会记录在package.json中,npm install时不会自动安装此依赖

环境解释:

devDependencies是开发环境,上线后非必需,比如:webpack,gulp等压缩打包工具

dependencies是线上发布环境,上线后必须的,比如:ui库,字体库

4、搭建webpack运行snabbdom的demo

安装webpack的一系列命令

npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

配置webpack.config.js

参考官网提供的demo:webpack.docschina.org/

const path = require('path');

module.exports = {
    // 入口文件
  entry: './src/index.js',
  //出口
  output: {
      //虚拟路径,不会物理生成
    publicPath:'xuni'
    filename: 'bundle.js',
  },
  devServer:{
      port:'8080',
      // 静态资源文件夹
      contentBase:'www'
  }
};

完成8080端口的访问

 // 修改启动命令
  "scripts": {
    "dev": "webpack-dev-server"
  },

界面展示测试数据

image (15).png

访问虚拟出口打包文件

image (16).png

运行demo项目

demo代码粘贴到index.js

demo代码做2处修改

image (17).png

// index.html
<body>
    <div id="container"></div>
<script src="./xuni/bundle.js"></script>
</body>

访问demo数据:

image (18).png

5、h函数的用法

一、作用:

创建虚拟节点

//虚拟节点的全部属性:
children: undefined
data: {props: {}}
elm: undefined
key: undefined
sel: "a"
text: "尚硅谷"

二、3个参数:

//index.js
import {init,classModule,propsModule,styleModule,eventListenersModule,h,} from "snabbdom";
//创建虚拟节点
const myVnode1 = h('a', { props: { href: 'http://www.atguigu.com', target: '_blank' } }, '尚硅谷');
console.log(myVnode1);

image (19).png

三、2个参数:

data没有数据可以省略

const myVnode2 = h('div', '我是一个盒子');

四、虚拟节点上树:

//虚拟节点上树
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const el = document.getElementById("container");
patch(el, myVnode3);

image (20).png

五、h函数嵌套:

const myVnode3 = h('ul', {}, [
    h('li', {}, '牛奶'),
    h('li', '咖啡',[
        h('p','嘻嘻'),
        h('p','哈哈')
    ]),
    h('li', {}, '花生'),
])

image (21).png

6、手写patch【文本节点上树】

一、分析patch的过程

1.jpg

二、手写完成patch【文本节点】

1.新建patch.js文件:

patch 在发现新旧节点不一致的时候,需要新建节点,依赖createElement.js

//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    //  判断oldVnode是不是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        //传入的节点是DOM节点,需要包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    //判断新旧是否是同一个节点
    if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key)    { 
        console.log('是同一个节点');
    } else {
        // 不是同一个节点,暴力删除旧的,添加新的
        createElement(newVnode, oldVnode.elm)
    }
}

2.新建createElement.js:

createElement主要是将虚拟节点创建成DOM。插入到标杆之前,将DOM节点上树

//真正的创建节点,将vnode创建为DOM,插入到pivot之前
export default function (vnode, pivot) {
    console.log('目的是把虚拟节点', vnode, '插入到标杆', pivot, '之前');
    // 创建一个dom节点,domNode是一个孤儿节点
    let domNode = document.createElement(vnode.sel);
    //  判断vode是文本节点还是有子节点
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 文本节点
        domNode.innerText = vnode.text;
        // 孤儿节点上树插入到标杆之前
        pivot.parentNode.insertBefore(domNode, pivot);
    }
}

3.在index.js 调用自己的方法:

import h from "./mysabbdom/h";
import patch from "./mysabbdom/patch";
const vnode = h('h1', {}, '你好');
const container = document.getElementById('container');
patch(container, vnode);

4.界面的展示效果:

image.png

image (1).png

三、改造createElement函数

为了使createElement能处理递归的子节点,将上树操作放到patch.js中

1.createElement.js

//真正的创建节点,将vnode创建为DOM,插入到pivot之前
export default function (vnode) {
    // 创建一个dom节点,domNode是一个孤儿节点
    let domNode = document.createElement(vnode.sel);
    //  判断vode是文本节点还是有子节点
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 文本节点
        domNode.innerText = vnode.text;
        // 补充elm属性
        vnode.elm = domNode;
    }
    return vnode.elm;
}

2.patch.js

//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    //  判断oldVnode是不是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        //传入的节点是DOM节点,需要包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    //判断新旧是否是同一个节点
    if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
        console.log('是同一个节点');
    } else {
        // 不是同一个节点,暴力删除旧的,添加新的
      let newVnodeElm =   createElement(newVnode);
      // 孤儿节点上树
      oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm);
    }
}

四、完成递归子节点

createElement.js:

createElement函数中完成递归子节点,节点是数组,获取到每一项子节点,将子节点转成DOM,追加到父节点中

//真正的创建节点,将vnode创建为DOM,插入到pivot之前
export default function createElement(vnode) {
    // 创建一个dom节点,domNode是一个孤儿节点
    let domNode = document.createElement(vnode.sel);
    //  判断vode是文本节点还是有子节点
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 文本节点
        domNode.innerText = vnode.text;
        // 有子节点
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 递归子节点
        for (let i = 0; i < vnode.children.length; i++) {
            let ch = vnode.children[i];
            //将子节点变成DOM
            let chDOM = createElement(ch);
            // 将chDOM追加到父节点上
            domNode.appendChild(chDOM);
        }
    }
    // 补充elm属性
    vnode.elm = domNode;
    //返回elm,elm属性是一个纯DOM
    return vnode.elm;
}

patch.js:

将返回的DOM节点上树,并且删除旧的节点

//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    //  判断oldVnode是不是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        //传入的节点是DOM节点,需要包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    //判断新旧是否是同一个节点
    if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
        console.log('是同一个节点');
    } else {
        // 不是同一个节点,暴力删除旧的,添加新的
        let newVnodeElm = createElement(newVnode);
        // 孤儿节点上树
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老的节点
    oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
}

界面效果:

image (2).png

按钮点击测试:

新旧节点不是同一节点,会暴力删除并新增

image (3).png

五、精细化比较

111 (1).jpg

六、丰富pach.js

【精细化比较的部分,只完成了图中的绿色部分】

//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    //  判断oldVnode是不是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        //传入的节点是DOM节点,需要包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    //判断新旧是否是同一个节点
    if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
        console.log('是同一个节点');
        //【这部分后面会提成patchNode.js 方便递归调用】
        // 判断oldVode和newVnode是不是同一个对象
        if (newVnode === oldVnode) {
            return;
        };
        //判断newVode有没有text
        if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
            console.log('newVnode有text属性');
            // 判断newVnode和oldVnode的text是否相同
            if (newVnode.text != oldVnode.text) {
                // 将newVnode的text给到oldNode.elm
                oldVnode.elm.innerText = newVnode.text;
            }
        } else {
            console.log('newVnode没有text属性');
            // 判断oldVode有没有children
            if (oldVnode.children != undefined && oldVnode.children.length > 0) {
                // 最复杂的情况
                //
                //
                //
            } else {
                // newVnode有children,oldVode没有
                // ① 删除老节点
                oldVnode.elm.innerHTML = ''
                // ② 新节点的children添加到DOM
                for (let i = 0; i < newVnode.children.length; i++) {
                    let dom = createElement(newVnode.children[i]);
                    oldVnode.elm.appendChild(dom);
                }
            }
        }
    } else {
        // 不是同一个节点,暴力删除旧的,添加新的
        let newVnodeElm = createElement(newVnode);
        // 孤儿节点上树
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老的节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
}

七、对代码和图示做说明:

newVnode有text,oldVnode可能是text也可能是children,做patch:

image (4).png

newVnode有children,oldVnode有text,做patch:

image (5).png

八、处理最复杂的情况

newVnode和oldVnode都有children:

出现一个四判定的算法 ① 新前与旧前 ② 新后与旧后 ③ 新后与旧前 ④ 新前与旧后

① 新前与旧前:

新前与旧前对比,如果命中,新前和旧前的指针会下移,如果没有命中,会走下一个判定,如果旧节点先循坏完毕,说明新节点中有要插入的节点,如果新节点先循坏完毕,说明旧节点中有删除的节点。

image (6).png

image (7).png

④ 新前与旧后:

当新前与旧后命中的时候,此时要移动节点,将新前节点移动到旧前节点的前面

image (8).png

③ 新后与旧前:

当新后与旧前命中的时候,此时需要移动节点,将新前节点移动到旧后的后面

image (9).png

总结:

image (10).png

九、diff更新子节点【四判定】

updateChildren.js

import patchVnode from "./patchVnode";
import createElement from './createElement'
function checkSameVnode(oldVnode, newVnode) {
    return oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key;
}
export default function updateChildren(parentElm, oldCh, newCh) {
    // 旧前
    let oldStartIdx = 0;
    // 新前
    let newStartIdx = 0;
    // 旧后
    let oldEndIdx = oldCh.length - 1;
    // 新后
    let newEndIdx = newCh.length - 1;
    // 旧前节点
    let oldStartVnode = oldCh[0];
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx];
    // 新前节点
    let newStartVnode = newCh[0];
    // 新后节点
    let newEndVnode = newCh[newEndIdx];

    let keyMap = null;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 略过已经加undefined标记的内容
        if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {
            oldStartVnode = oldCh[++oldStartIdx];
        }
        else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        }
        else if (newStartVnode == null || newCh[newStartIdx] === undefined) {
            newStartVnode = newCh[++newStartIdx];
        }
        else if (newEndVnode == null || newCh[newEndIdx] === undefined) {
            newEndVnode = newCh[--newEndIdx];
        }
        else if (checkSameVnode(oldStartVnode, newStartVnode)) {
            // 新前与旧前
            console.log('新前与旧前命中');
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            // 新后和旧后
            console.log('新后和旧后命中');
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndVnode];
        }
        else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            console.log('新后和旧前命中');
            patchVnode(oldStartVnode, newEndVnode);
            // 当新后与旧前命中的时候,此时要移动节点,移动新后指向的这个节点到老节点旧后的后面
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            // 新前和旧后
            console.log('新前和旧后命中');
            patchVnode(oldEndVnode, newStartVnode);
            // 当新前和旧后命中的时候,此时要移动节点,移动新前指向的这个节点到老节点旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        else {
            // 四种都没有命中
            // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了
            if (!keyMap) {
                keyMap = {};
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key !== undefined) {
                        keyMap[key] = i;
                    }
                }
            }
            // 寻找当前这项(newStartIdx)在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key];
            if (idxInOld === undefined) {
                // 如果idxInOld是undefined表示踏实全新的项,此时会将该项创建为DOM节点并插入到旧前之前
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
            }
            else {
                // 如果不是undefined,则不是全新的项,则需要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStartVnode);
                // 把这项设置为undefined,表示已经处理完这项了
                oldCh[idxInOld] = undefined;
                // 移动
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
            }
            // 指针下移,只移动新的头
            newStartVnode = newCh[++newStartIdx];
        }
    }

    // 循环结束后,处理未处理的项
    if (newStartIdx <= newEndIdx) {
        console.log('new还有剩余节点没有处理,要加项,把所有剩余的节点插入到oldStartIdx之前');
        // 遍历新的newCh,添加到老的没有处理的之前
        const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去
            // newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]),before);
        }
    }
    else if (oldStartIdx <= oldEndIdx) {
        console.log('old还有剩余节点没有处理,要删除项');
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
}

patchVnode.js

import updateChildren from "./updateChildren";
export default function patchVnode(oldVnode, newVnode) {
    // 判断oldVode和newVnode是不是同一个对象
    if (newVnode === oldVnode) {
        return;
    };
    //判断newVode有没有text
    if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
        console.log('newVnode有text属性');
        // 判断newVnode和oldVnode的text是否相同
        if (newVnode.text != oldVnode.text) {
            // 将newVnode的text给到oldNode.elm
            oldVnode.elm.innerText = newVnode.text;
        }
    } else {
        console.log('newVnode没有text属性');
        // 判断oldVode有没有children
        if (oldVnode.children != undefined && oldVnode.children.length > 0) {
            // 最复杂的情况
            // 老的有children,新的也有children
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
        } else {
            // newVnode有children,oldVode没有
            // ① 删除老节点
            oldVnode.elm.innerHTML = ''
            // ② 新节点的children添加到DOM
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

vnode.js

vnode.js 增加了key

export default function vnode(sel, data, children, text, elm) {
    //key值是标签的唯一标识,在data.key中,这里获取一下key
    const key = data === undefined ? undefined : data.key
    return { sel, data, children, text, elm, key };
}

index.js

import h from "./mysabbdom/h";
import patch from "./mysabbdom/patch";
// 命中新前与旧前 新增
const myVnode22 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B')

])
const myVnode33 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'B' }, 'C'),
    h('li', { key: 'B' }, 'D'),
    h('li', { key: 'B' }, 'E')
])
// 命中新前与旧前 删除
const myVnode222 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'E' }, 'E'),
])
const myVnode333 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
])

// 命中新前和旧后
const myVnode2222 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'E' }, 'E')
])
const myVnode3333 = h('ul', {}, [
    h('li', { key: 'E' }, 'E'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'M' }, 'M'),
])
// 命中新后与旧前
const myVnode22222 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'E' }, 'E')
])
const myVnode33333 = h('ul', {}, [
    h('li', { key: 'E' }, 'E'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'A' }, 'A'),
])

const container = document.getElementById('container');
const btn = document.getElementById('btn');
patch(container, myVnode22222);
btn.onclick = function () {
    patch(myVnode22222, myVnode33333)
}

image (11).png

十、diff算法的核心流程图

未命名文件.png

7、 手写h函数

一、vnode.js

h函数依赖vnode.js ,vnode主要是将虚拟dom的参数,转成对象返回

export default function (sel, data, children, text, elm) {
    return {
        sel, data, children, text, elm
    }
}

二、h函数

h函数只能处理3个参数,其他情况暂时不考虑,判断第三个参数c的类型

形式1: h('sel', {}, '文字'); 直接将c作为text返回

形式2:h('sel', {}, []);对c进行遍历,将收集的children作为children返回

形式3:h('sel', {}, h()); 将c作为children直接返回

import vnode from './vnode'
// 编写一个低配版的h函数 只处理3个参数的情况
// 形式1: h('sel', {}, '文字');
// 形式2:h('sel', {}, []);
// 形式3:h('sel', {}, h());
export default function (sel, data, c) {
    //判断参数的个数是否是3个
    if (arguments.length != 3) {
        throw new Error('对不起,参数个数只支持3个!');
    }
    //处理c的类型 形式1
    if (typeof c === 'string' || typeof c === 'number') {
        return vnode(sel, data, undefined, c, undefined)
        //处理 形式2 数组
    } else if (Array.isArray(c)) {
        let children = [];
        // 遍历数组的每一项,收集到children中
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个对象
            if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
                throw new Error('数组的一项不是h函数');
            }
            // 在调用c[i]的时候就执行了h函数,也就进行了c的类型判断
            children.push(c[i]);
        }
        // 收集完,返回
        return vnode(sel, data, children, undefined, undefined);
        //处理形式3
    } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
        return vnode(sel, data, c, undefined, undefined)

    } else {
        throw new Error('有一项参数不规范!')
    }
}

三、虚拟dom挂树

利用自己写的h函数,完成页面展示

import h from "./mysabbdom/h";
import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
} from "snabbdom";
const myVnode3 = h('ul', {}, [
    h('li', {}, '牛奶'),
    h('li', '咖啡', [
        h('p', {}, '嘻嘻'),
        h('p', {}, '哈哈')
    ]),
    h('li', {}, '花生'),
])
//虚拟节点上树
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const el = document.getElementById("container");
patch(el, myVnode3);

image (12).png

8、key值问题

vue 中 key 值的作用可以分为两种情况来考虑:

第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。

第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速

更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。

更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快更快速

为什么不用index做key值?

使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。

参考👀

blog.csdn.net/qq_35603476…

www.bilibili.com/video/BV1v5…

blog.csdn.net/weixin_4497…