1.真实 DOM 和虚拟 DOM
真实 DOM 是 html 页面中的 DOM 元素。虚拟 DOM 是用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。
一个虚拟 DOM 的属性如下:
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 如下:
当然 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。
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 函数并进行了简单实现,处理的是新旧节点不是同一个节点的情况。函数的实现并非官方源码,只对大体的逻辑流程进行了代码实现,而忽略了很多细节问题,仅可作学习使用。