虚拟 DOM 和 diff 算法(上)【转】尚硅谷


1.真实 DOM 和虚拟 DOM

真实 DOM 是 html 页面中的 DOM 元素。虚拟 DOM 是用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。
一个虚拟 DOM 的属性如下:
VirtualNode

2.h 函数

snabbdom 是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码借鉴了 snabbdom;

官方 git:https://github.com/snabbdom/snabbdom

h 函数用来产生虚拟节点(vnode),比如像这样调用 h 函数,

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

得到的 virtual Node 如下:

vnode

当然 h 函数也可以嵌套使用,比如这样嵌套使用 h 函数:

h('ul', {}, [ h('li', {}, '牛奶'),
h('li', {}, '咖啡'),
h('li', {}, '可乐')
]);

将得到这样的虚拟 DOM 树:

{
"sel": "ul",
"data": {},
"children": [ { "sel": "li", "text": "牛奶" },
{ "sel": "li", "text": "咖啡" },
{ "sel": "li", "text": "可乐" } ] }

模仿源码简单实现一个 h 函数,

vnode.js

// 该函数的作用就是把传入的5个参数组合成对象返回
export default function(sel, data, children, text, elm) {
    const key = data.key;
    return {
        sel, data, children, text, elm, key
    };
}

h.js

import vnode from './vnode.js';

// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可
// 相当于它的重载功能较弱。
// 也就是说,调用的时候形态必须是下面的三种之一:
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
export default function h(sel, data, c) {
    // 检查参数的个数
    if (arguments.length != 3)
        throw new Error('h函数必须传入3个参数,我们是低配版h函数');
    // 检查参数c的类型
    if (typeof c == 'string' || typeof c == 'number') {
        // 说明现在调用h函数是形态①
        return vnode(sel, data, undefined, c, undefined);
    } else if (Array.isArray(c)) {
        // 说明现在调用h函数是形态②
        let children = [];
        // 遍历c,收集children,遍历的时候数组中各项已经由h函数生成了vnode
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个由h函数生成的vnode
            if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
                throw new Error('传入的数组参数中有项不是h函数');
            children.push(c[i]);
        }
        // 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它有children属性的
        return vnode(sel, data, children, undefined, undefined);
    } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
        // 说明现在调用h函数是形态③
        // 即,传入的c是唯一的children。
        let children = [c];
        return vnode(sel, data, children, undefined, undefined);
    } else {
        throw new Error('传入的第三个参数类型不对');
    }
};

3.patch 函数

patch函数用来更新真实 DOM 节点,该函数传入两个参数,一个是旧的 vnode,一个是新的 vnode。

patch 函数

diff 处理新旧节点不是同一个节点时会直接删除旧的 dom,并插入新的 dom。如何定义“同一个节点”?旧节点的 sel 属性要和新节点的 sel 属性相同且旧节点的 key 要和新节点的 key 相同。key 属性是节点的唯一标识。

来看一下源码中对同一节点的定义

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;
}

patch.js

import vnode from './vnode.js';
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点?
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }

    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        // 调用patchVnode函数进行diff
        patchVnode(oldVnode, newVnode);
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        // 调用createElement函数,该函数将传入的虚拟dom转为真实dom返回
        let newVnodeElm = createElement(newVnode);

        // 插入到旧节点之前
        oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        // 删除旧节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
};

4.createElement 函数

createElement 函数用于将虚拟 dom 转为真实 dom。

createElement.js

// 该函数的作用就是将传入的虚拟dom转为真实dom返回
export default function createElement(vnode) {
    // 创建一个真实DOM节点,这个节点现在还是孤儿节点
    let domNode = document.createElement(vnode.sel);
    // 判断该节点是否有children
    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++) {
            // 得到当前这个children
            let ch = vnode.children[i];
            // 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
            let chDOM = createElement(ch);
            // 将子节点添加到父节点上,操作的是真实dom
            domNode.appendChild(chDOM);
        }
    }
    // 添加elm属性,elm属性是一个真实DOM对象
    vnode.elm = domNode;

    return vnode.elm;
};

本文主要介绍了虚拟 dom 和 diff 算法相关的 h 函数、patch 函数、createElement 函数并进行了简单实现,处理的是新旧节点不是同一个节点的情况。函数的实现并非官方源码,只对大体的逻辑流程进行了代码实现,而忽略了很多细节问题,仅可作学习使用。


文章作者: Cubby
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cubby !
  目录