被问到过虚拟DOM和DOM-diff算法是如何实现的
创建虚拟DOM
- type: 指定元素的标签类型,如'li', 'div', 'a'等
- props: 表示指定元素身上的属性,如class, style, 自定义属性等
- children: 表示指定元素是否有子节点,参数以数组的形式传入
function createElement(type, props, children) {
return new Element(type, props, children);
}
渲染虚拟DOM
function render(domObj) {
// 根据type类型来创建对应的元素
let el = document.createElement(domObj.type);
// 再去遍历props属性对象,然后给创建的元素el设置属性
for (let key in domObj.props) {
// 设置属性的方法
setAttr(el, key, domObj.props[key]);
}
// 遍历子节点
// 如果是虚拟DOM,就继续递归渲染
// 不是就代表是文本节点,直接创建
domObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
// 添加到对应元素内
el.appendChild(child);
});
return el;
}
DOM-diff
给定任意两棵树,采用先序深度优先遍历的算法找到最少的转换步骤,DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。
作用: 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新DOM
function diff(oldTree, newTree) {
// 声明变量patches用来存放补丁的对象
let patches = {};
// 第一次比较应该是树的第0个索引
let index = 0;
// 递归树 比较后的结果放到补丁里
walk(oldTree, newTree, index, patches);
return patches;
}
比较规则
- 新的DOM节点不存在{type: 'REMOVE', index}
- 文本的变化{type: 'TEXT', text: 1}
- 当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包{type: 'ATTR', attr: {class: 'list-group'}}
- 节点类型不相同,直接采用替换模式{type: 'REPLACE', newNode}
function walk(oldNode, newNode, index, patches) {
// 每个元素都有一个补丁
let current = [];
if (!newNode) { // rule1
current.push({ type: 'REMOVE', index });
} else if (isString(oldNode) && isString(newNode)) {
// 判断文本是否一致
if (oldNode !== newNode) {
current.push({ type: 'TEXT', text: newNode });
}
} else if (oldNode.type === newNode.type) {
// 比较属性是否有更改
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
current.push({ type: 'ATTR', attr });
}
// 如果有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
} else { // 说明节点被替换了
current.push({ type: 'REPLACE', newNode});
}
// 当前元素确实有补丁存在
if (current.length) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
}
patch补丁更新
打补丁需要传入两个参数,一个是要打补丁的元素,另一个就是所要打的补丁了,那么直接看代码
let index = 0; // 默认哪个需要打补丁
patch做了什么
-
doPatch打补丁方法会根据传递的patches进行遍历
-
判断补丁的类型来进行不同的操作
1.1属性ATTR forin去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr; 如果不存在对应的key值那就直接删除这个key键的属性1.2
文字TEXT直接将补丁的text赋值给node节点的textContent即可1.3
替换REPLACE新节点替换老节点,需要先判断新节点是不是Element的实例,是的话调用render方法渲染新节点; 不是的话就表明新节点是个文本节点,直接创建一个文本节点就OK了。 之后再通过调用父级parentNode的replaceChild方法替换为新的节点1.4
删除REMOVE直接调用父级的removeChild方法删除该节点
function doPatch(node, patches) {
// 遍历所有打过的补丁
patches.forEach(patch => {
switch (patch.type) {
case 'ATTR':
for (let key in patch.attr) {
let value = patch.attr[key];
if (value) {
setAttr(node, key, value);
} else {
node.removeAttribute(key);
}
}
break;
case 'TEXT':
node.textContent = patch.text;
break;
case 'REPLACE':
let newNode = patch.newNode;
newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
node.parentNode.replaceChild(newNode, node);
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
default:
break;
}
});
}
总结
整个DOM-diff的过程:
- 用JS对象模拟DOM(虚拟DOM)
- 把此虚拟DOM转成真实DOM并插入页面中(render)
- 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
- 把差异对象应用到真正的DOM树上(patch)