vue的底层虚拟DOM库——snabbdom

文章内容输出来源:拉勾教育前端高薪训练营

什么是Vitual DOM

Vitual DOM(虚拟DOM),是由普通的JS对象来描述的DOM对象

为什么使用Vitual DOM

  • 简化DOM复杂操作
  • MVVM框架解决视图和状态同步问题
  • 模版引擎可以简化视图操作,但没办法跟踪状态
  • 虚拟DOM可以维护程序的状态,跟踪上一次的状态,通过比较前后两次状态差异更新真实DOM

Vitual DOM的作用

  • 维护视图和状态的关系,保存视图的状态
  • 复杂视图情况下提升渲染性能
  • 跨平台
    • 浏览器平台渲染DOM
    • 服务端渲染SSR(Nuxt.js/Next.js)
    • 原生应用(Weex/React Native)
    • 小程序(mpvue/uni-app)

Snabbdom

vue使用的底层虚拟DOM库

Snabbdom的核心

  • init()设置模块,创建patch()函数
  • 使用h()函数创建JavaScript对象(VNode)来描述真实DOM
  • patch()比较新旧两个VNode
  • 把变化的内容更新到真实DOM树

Snabbdom的基本使用

import { init, h } from 'snabbdom'

// 参数数组中传递Snabbdom的模块
const patch = init([])

// h函数第一个参数:标签+选择器,第二个参数:如果是字符串就是标签中的文本内容
const vnode = h('div#container.test', 'Hello World')
const app = document.querySelector('#app')

// patch函数对比两个VNode更新差异
// 第一个参数:旧的VNode或者DOM元素,第二个参数:新的VNode
const oldVNode = patch(app, vnode)

setTimeout(() => {
  const newVNode = h('div#container.vnode', [
    h('h1', 'Hello Snabbdom'),
    h('p', 'xxxxxxxx')
  ])
  patch(oldVNode, newVNode)

  // 清空节点内容
  // patch(oldVNode, h('!'))
}, 5000)

Snabbdom模块的作用

  • Snabbdom的核心库并不能处理DOM元素的属性/样式/事件等,可以通过注册Snabbdom默认提供的模块来实现
  • Snabbdom中的模块可以用来扩展Snabbdom的功能
  • Snabbdom中的模块的实现是通过注册全局的钩子函数来实现的

官方提供的模块

  • attributes:设置vnode对应的dom元素的属性
  • props:通过对象.属性的方式设置dom元素的属性,不会处理bool值
  • dataset:处理data-属性
  • class:切换类样式
  • style:设置行内样式
  • eventlisteners:注册和移除事件
import {
  init,
  styleModule,
  eventListenersModule,
  h,
} from 'snabbdom'

let oldVNode

const handleClick = vnode => {
  const newVNode = h('div#container.vnode2', [
    h('h1', {
      style: {
        color: 'blue',
      },
    }, 'Hello Snabbdom'),
    h('p', {
      on: {
        click: handleClick,
      },
    }, '请点击我')
  ])
  oldVNode = patch(oldVNode, newVNode)
}

const patch = init([
  styleModule,
  eventListenersModule,
])

// h函数第一个参数:标签+选择器,第二个参数:如果是字符串就是标签中的文本内容
const vnode = h('div#container.test', 'Hello World')
const app = document.querySelector('#app')

// patch函数对比两个VNode更新差异
// 第一个参数:旧的VNode或者DOM元素,第二个参数:新的VNode
oldVNode = patch(app, vnode)

setTimeout(() => {
  const newVNode = h('div#container.vnode2', [
    h('h1', {
      style: {
        color: 'red',
      },
    }, 'Hello Snabbdom'),
    h('p', {
      on: {
        click: handleClick,
      },
    }, '请点击我')
  ])
  oldVNode = patch(oldVNode, newVNode)
}, 5000)

Snabbdom中的h函数和vnode

h函数的作用是创建VNode对象

函数重载

  • 参数个数或参数类型不同的函数
  • JavaScript中没有重载的概念
  • TypeScript中有重载,不过重载的实现还是通过代码调整参数
function add (a: number, b: number) {
  console.log(a + b)
}
function add (a: number, b: number, c: number) {
  console.log(a + b + c)
}
function add (a: number, b: string) {
  console.log(a + b)
}
add(1, 2) // 执行第一个add函数
add(1, 2, 3) // 执行第二个add函数
add(1, '2') // 执行第三个add函数
// h函数的实现
export function h(sel, b, c) {
    let data = {};
    let children;
    let text;
    let i;
    if (c !== undefined) { // 判读第三个参数是否传递
        if (b !== null) { // 判断第二个参数属性对象是否不为null
            data = b;
        }
        if (is.array(c)) { // 判读第三个参数是否为数组,为数组就存在子节点
            children = c;
        }
        else if (is.primitive(c)) { // 判读第三个参数是否string或number文本类型
            text = c;
        }
        else if (c && c.sel) { // 判读第三个参数是为单个节点,封装成数组形式
            children = [c];
        }
    }
    else if (b !== undefined && b !== null) { // 当第三个参数未传递,判断第二个参数是否传递,判断方法同上

        if (is.array(b)) {
            children = b;
        }
        else if (is.primitive(b)) {
            text = b;
        }
        else if (b && b.sel) {
            children = [b];
        }
        else {
            data = b;
        }
    }
    if (children !== undefined) {
        for (i = 0; i < children.length; ++i) {
            if (is.primitive(children[i])) // 遍历children,判断children[i]是否为文本类型
                children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
        }
    }
    if (sel[0] === "s" && // 判断是否为svg
        sel[1] === "v" &&
        sel[2] === "g" &&
        (sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
        addNS(data, children, sel);
    }
    return vnode(sel, data, children, text, undefined);
}

// vnode实现

// children和text互斥
export interface VNode {
  sel: string | undefined;
  data: VNodeData | undefined;
  children: Array<VNode | string> | undefined;
  elm: Node | undefined;
  text: string | undefined;
  key: Key | undefined;
}

export function vnode(sel, data, children, text, elm) {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

Snabbdom中的patch

  • 用法:patch(oldVNode, newVNode)
  • 利用diff算法查找两个节点的差异,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧VNode是否是相同节点(通过节点的key和sel判断)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVNode的text不同,直接更新文本内容
    • 如果新的VNode有children,依次对比新旧节点的子节点,判断子节点是否哟变化

patchVnode工作流程

patchVnode
触发prepatch钩子函数
触发update钩子函数
新节点有text属性且不等于旧节点的text属性
如果老节点有children 移除老节点children对应的DOM元素
设置新节点对应DOM元素的textContent
新老节点都有children属性且不相等
调用updateChildren方法
对比子节点并且更新子节点差异
只有新节点有children属性
如果老节点有text属性 清空对应DOM元素的textContent
添加所有子节点
只有老节点有children属性
移除所有老节点
只有老节点有text属性
清空对应DOM元素的textContent
触发postpatch钩子函数

Diff算法

  • 渲染DOM操作的开销很大,操作DOM会引起浏览器的重排和重绘
  • 虚拟DOM中的Diff算法就是查找两棵树每一个节点的差异,会比较n平方次,找到差异后再循环一次更新差异部分,核心是当数据变化后不直接操作DOM,而是用js对象描述真实DOM,当数据变化后先比较js对象是否发生变化,找到所有变化的位置,最小化的更新所有变化的位置,从而提高性能
  • Snabbdom根据DOM特点对传统的Diff算法做了优化,DOM操作时很少会跨级别操作节点,所以只比较同级别的节点,只需要循环n次,并在比较过程中同时更新差异

执行过程:对开始和结束节点比较,总共四种情况

  • oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)

  • oldEndVnode / newEndVnode(旧结束节点 / 新结束节点)

  • oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)

  • oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)

  • oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)比较

    • 如果新旧开始节点是sameVnode(key和sel相同),会重用之前旧节点对应的DOM元素
      • 调用patchVnode()对比更新节点到重用的DOM元素上
      • 把旧开始和新开始索引往后移动 oldStartIdx++ / newStartIdx++
    • 如果新旧开始节点不是sameVnode,执行下面的比较
  • oldEndVnode / newEndVnode(旧结束节点 / 新结束节点)比较

    • 如果新旧结束节点是sameVnode(key和sel相同),会重用之前旧节点对应的DOM元素
      • 调用patchVnode()对比更新节点到重用的DOM元素上
      • 把旧开始和新开始索引往前移动 oldStartIdx-- / newStartIdx–
  • oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)比较

    • 如果两个是sameVnode(key和sel相同),会重用之前旧节点对应的DOM元素
      • 调用patchVnode()对比更新节点到重用的DOM元素上
      • 把oldStartVnode对应的DOM元素移动到最后,并更新索引 oldStartIdx++ / newEndIdx–
  • oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)比较

    • 如果两个是sameVnode(key和sel相同),会重用之前旧节点对应的DOM元素
      • 调用patchVnode()对比更新节点到重用的DOM元素上
        -2 把oldEndVnode对应的DOM元素移动到最前面,并更新索引 oldEndIdx-- / newStartIdx++
  • 上述情况都不满足的情况下,在旧节点数组中依次查找是否有相同的新节点

    • 遍历新的开始节点
      • 如果旧节点中没有,则创建新的DOM元素,并插入到最前面
      • 如果旧节点中有key值相同,sel不相同的,则创建新的DOM元素,并插入到最前面
      • 如果新旧开始节点是sameVnode(key和sel相同),此时旧节点会被赋值给elmToMove变量,调用patchVnode()对比更新新旧节点,然后把elmToMove对应的DOM元素移动到最前面
  • 循环结束收尾

    • 当老节点的所有子节点先遍历完(oldStartIdx > oldEndIdx),此时新节点有剩余,把剩余节点批量插入到右边
    • 新节点的所有子节点先遍历完(newStartIdx > newEndIdx),此时老节点有剩余,把剩余节点批量删除
// 判断是否为相同节点
function sameVnode(vnode1, vnode2) {
    var _a, _b;
    const isSameKey = vnode1.key === vnode2.key;
    const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is);
    const isSameSel = vnode1.sel === vnode2.sel;
    return isSameSel && isSameKey && isSameIs;
}
// 判断是否是VNode对象
function isVnode(vnode) {
    return vnode.sel !== undefined;
}
function createKeyToOldIdx(children, beginIdx, endIdx) {
    var _a;
    const map = {};
    for (let i = beginIdx; i <= endIdx; ++i) {
        const key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key;
        if (key !== undefined) {
            map[key] = i;
        }
    }
    return map;
}

// 定义钩子
const hooks = [
    "create",
    "update",
    "remove",
    "destroy",
    "pre",
    "post",
];

// modules:依赖模块,domApi:执行环境
export function init(modules, domApi) {
    let i;
    let j;
    const cbs = {
        create: [],
        update: [],
        remove: [],
        destroy: [],
        pre: [],
        post: [],
    };
    const api = domApi !== undefined ? domApi : htmlDomApi; // 设置默认执行环境为浏览器环境
    for (i = 0; i < hooks.length; ++i) { // 遍历设置钩子函数 cbs => { create: [fn1, fn2...], update: [fn1, fn2...] }
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
            const hook = modules[j][hooks[i]];
            if (hook !== undefined) {
                cbs[hooks[i]].push(hook);
            }
        }
    }
    function emptyNodeAt(elm) { // 转换成VNode对象
        const id = elm.id ? "#" + elm.id : "";
        // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot.
        // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring
        const classes = elm.getAttribute("class");
        const c = classes ? "." + classes.split(" ").join(".") : "";
        return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
    }
    function createRmCb(childElm, listeners) {
        return function rmCb() {
            if (--listeners === 0) { // 执行完所有的钩子函数才移除节点
                const parent = api.parentNode(childElm);
                api.removeChild(parent, childElm);
            }
        };
    }
    // 把VNode节点转换成对应的DOM元素,并存储在VNode的elm属性中
    function createElm(vnode, insertedVnodeQueue) {
        var _a, _b;
        let i;
        let data = vnode.data;
        if (data !== undefined) { // 执行用户设置的init钩子函数
            const init = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init;
            if (isDef(init)) {
                init(vnode);
                data = vnode.data;
            }
        }
        // 将VNode转换成真实DOM
        const children = vnode.children;
        const sel = vnode.sel;
        if (sel === "!") { // 创建注释节点
            if (isUndef(vnode.text)) {
                vnode.text = "";
            }
            vnode.elm = api.createComment(vnode.text);
        }
        else if (sel !== undefined) {
            // Parse selector
            const hashIdx = sel.indexOf("#");
            const dotIdx = sel.indexOf(".", hashIdx);
            const hash = hashIdx > 0 ? hashIdx : sel.length;
            const dot = dotIdx > 0 ? dotIdx : sel.length;
            const tag = hashIdx !== -1 || dotIdx !== -1
                ? sel.slice(0, Math.min(hash, dot))
                : sel;
            const elm = (vnode.elm =
                isDef(data) && isDef((i = data.ns))
                    ? api.createElementNS(i, tag, data) // 一般此处创建的是svg
                    : api.createElement(tag, data));
            if (hash < dot)
                elm.setAttribute("id", sel.slice(hash + 1, dot)); // 设置id
            if (dotIdx > 0)
                elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); // 设置类
            for (i = 0; i < cbs.create.length; ++i)
                cbs.create[i](emptyNode, vnode);
            if (is.array(children)) { // 遍历children,创建createElm
                for (i = 0; i < children.length; ++i) {
                    const ch = children[i];
                    if (ch != null) {
                        api.appendChild(elm, createElm(ch, insertedVnodeQueue));
                    }
                }
            }
            else if (is.primitive(vnode.text)) { // 数值和字符串形式的text,创建文本节点
                api.appendChild(elm, api.createTextNode(vnode.text));
            }
            const hook = vnode.data.hook;
            if (isDef(hook)) {
                (_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode);
                if (hook.insert) { // 将insert钩子函数存储到insertedVnodeQueue队列中
                    insertedVnodeQueue.push(vnode);
                }
            }
        }
        else {
            vnode.elm = api.createTextNode(vnode.text); // sel为空,创建文本节点
        }
        return vnode.elm;
    }
    function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
        for (; startIdx <= endIdx; ++startIdx) {
            const ch = vnodes[startIdx];
            if (ch != null) {
                api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
            }
        }
    }
    function invokeDestroyHook(vnode) {
        var _a, _b;
        const data = vnode.data;
        if (data !== undefined) {
            (_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); // 判断用户是否传入destory钩子函数
            for (let i = 0; i < cbs.destroy.length; ++i)
                cbs.destroy[i](vnode); // 遍历执行cbs中的destory钩子
            if (vnode.children !== undefined) {
                for (let j = 0; j < vnode.children.length; ++j) { // 遍历子节点,递归调用invokeDestroyHook进行删除
                    const child = vnode.children[j];
                    if (child != null && typeof child !== "string") {
                        invokeDestroyHook(child);
                    }
                }
            }
        }
    }
    function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
        var _a, _b;
        for (; startIdx <= endIdx; ++startIdx) {
            let listeners;
            let rm;
            const ch = vnodes[startIdx];
            if (ch != null) {
                if (isDef(ch.sel)) {
                    invokeDestroyHook(ch); // 触发destory钩子函数
                    listeners = cbs.remove.length + 1;
                    rm = createRmCb(ch.elm, listeners);
                    for (let i = 0; i < cbs.remove.length; ++i)
                        cbs.remove[i](ch, rm);
                    const removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove;
                    if (isDef(removeHook)) {
                        removeHook(ch, rm); // 用户传入来remove钩子函数时,需要手动调用rm函数
                    }
                    else {
                        rm();
                    }
                }
                else {
                    // Text node
                    api.removeChild(parentElm, ch.elm);
                }
            }
        }
    }
    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
        let oldStartIdx = 0;
        let newStartIdx = 0;
        let oldEndIdx = oldCh.length - 1;
        let oldStartVnode = oldCh[0];
        let oldEndVnode = oldCh[oldEndIdx];
        let newEndIdx = newCh.length - 1;
        let newStartVnode = newCh[0];
        let newEndVnode = newCh[newEndIdx];
        let oldKeyToIdx;
        let idxInOld;
        let elmToMove;
        let before;
        // 同级节点比较
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (oldStartVnode == null) {
                oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
            }
            else if (oldEndVnode == null) {
                oldEndVnode = oldCh[--oldEndIdx];
            }
            else if (newStartVnode == null) {
                newStartVnode = newCh[++newStartIdx];
            }
            else if (newEndVnode == null) {
                newEndVnode = newCh[--newEndIdx];
            }
            // 比较开始节点和结束节点的四种情况
            else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
                oldStartVnode = oldCh[++oldStartIdx];
                newStartVnode = newCh[++newStartIdx];
            }
            else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
                oldEndVnode = oldCh[--oldEndIdx];
                newEndVnode = newCh[--newEndIdx];
            }
            else if (sameVnode(oldStartVnode, newEndVnode)) {
                // Vnode moved right
                patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
                api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
                oldStartVnode = oldCh[++oldStartIdx];
                newEndVnode = newCh[--newEndIdx];
            }
            else if (sameVnode(oldEndVnode, newStartVnode)) {
                // Vnode moved left
                patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
                api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
                oldEndVnode = oldCh[--oldEndIdx];
                newStartVnode = newCh[++newStartIdx];
            }
            else {
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); // 创建老节点的key和Idx关系{ key1: 1, key2: 2... }
                }
                idxInOld = oldKeyToIdx[newStartVnode.key]; // 用新节点的key查找在老节点中对应key的Idx
                if (isUndef(idxInOld)) {
                    // New element
                    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); // 新节点为新元素,插入到老节点开始节点之前
                }
                else {
                    elmToMove = oldCh[idxInOld]; // 将对应的key值的老节点赋值给elmToMove
                    if (elmToMove.sel !== newStartVnode.sel) { // 当老节点和新节点不是sameVnode
                        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); // 新节点为新元素,插入到老节点开始节点之前
                    }
                    else {
                        patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); // 对比新旧节点更新差异
                        oldCh[idxInOld] = undefined;
                        api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); // 移动elmToMove到老节点开始节点之前
                    }
                }
                newStartVnode = newCh[++newStartIdx];
            }
        }
        if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
            if (oldStartIdx > oldEndIdx) { // 新节点中有剩余节点
                before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; // 新增节点插入的位置
                addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
            }
            else { // 老节点中有剩余节点,移除
                removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
            }
        }
    }
    function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
        var _a, _b, _c, _d, _e;
        const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook;
        (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); // 执行传入的prepatch钩子函数
        const elm = (vnode.elm = oldVnode.elm);
        const oldCh = oldVnode.children;
        const ch = vnode.children;
        if (oldVnode === vnode) // 新旧节点相同时直接返回
            return;
        if (vnode.data !== undefined) { // 遍历执行update钩子函数
            for (let i = 0; i < cbs.update.length; ++i)
                cbs.update[i](oldVnode, vnode);
            (_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode);
        }
        if (isUndef(vnode.text)) { // 新节点没有text属性
            if (isDef(oldCh) && isDef(ch)) { // 新旧节点都有子节点
                if (oldCh !== ch) // 新旧节点的子节点不相等
                    updateChildren(elm, oldCh, ch, insertedVnodeQueue); // 对比新旧节点的不同,并更新DOM
            }
            else if (isDef(ch)) { // 新节点有子节点
                if (isDef(oldVnode.text)) // 老节点有text属性时,清空DOM元素的对应的textContent
                    api.setTextContent(elm, "");
                addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); // 插入新节点的子节点
            }
            else if (isDef(oldCh)) { // 老节点有子节点时,清空老节点的子节点
                removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            }
            else if (isDef(oldVnode.text)) { // 老节点有text属性,清空DOM元素的对应的textContent
                api.setTextContent(elm, "");
            }
        }
        else if (oldVnode.text !== vnode.text) { // 新旧节点text属性不相等
            if (isDef(oldCh)) { // 如果老节点有子节点,移除老节点的子节点
                removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            }
            api.setTextContent(elm, vnode.text); // 设置text到节点
        }
        (_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); // 执行传入的postpatch钩子函数
    }
    return function patch(oldVnode, vnode) {
        let i, elm, parent;
        const insertedVnodeQueue = []; // 新插入节点队列
        for (i = 0; i < cbs.pre.length; ++i) // 触发pre钩子进行预处理
            cbs.pre[i]();
        if (!isVnode(oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }
        if (sameVnode(oldVnode, vnode)) { // 通过key和sel属性判断是否相同节点
            patchVnode(oldVnode, vnode, insertedVnodeQueue); // 对比节点的差异
        }
        else { // 不是相同节点
            elm = oldVnode.elm; // 获取老节点元素
            parent = api.parentNode(elm); // 获取父元素
            createElm(vnode, insertedVnodeQueue); // 创建新VNode的DOM元素
            if (parent !== null) {
                api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); // 插入新节点
                removeVnodes(parent, [oldVnode], 0, 0); // 移除老节点
            }
        }
        for (i = 0; i < insertedVnodeQueue.length; ++i) { // 执行insert钩子函数
            insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
        }
        for (i = 0; i < cbs.post.length; ++i)
            cbs.post[i]();
        return vnode;
    };
}

关于列表中的key值
以li为例

  • 当不设置key值时,新旧节点对比时,由于同是li标签,会认为时相同节点,新节点可能就会继承老节点的一些属性,例如当Idx=0的li中checkbox选中时,在Idx=0之前新增一个新节点,此时新节点的checkbox会被选中,而原先的checkbox不会被选中
  • 当设置例key时就不会存在这个问题,此时两个节点不为相同节点

你可能感兴趣的:(拉勾教育学习笔记,vue)