框架给我们提供了屏蔽底层dom书写的方式,减少频繁的整更新dom,同时也使得数据驱动视图
当有一个表格需要做排序功能时,有出生年月、性别等排序方式可选,当选择某排序方式时,表格将按该方式重新排序。
真实 DOM:排序操作需要将表格内的所有 DOM 树删除后新建;
虚拟 DOM:使用 diff 算法得到需要修改的部分,仅更新需要发生修改的 DOM 节点;
从上可知,虚拟 DOM 通过 diff 算法,帮助我们大量的减少 DOM 操作。
从另一个角度看,虚拟 DOM 为我们提供了函数式的编程方式,使代码可读性和可维护性更高。符合React的核心思想。
const App = () => {
return (
<div>
</div>
)
}
通过虚拟DOM,渲染到DOM(web)之外的平台。比如ReactNative,Weex
说到diff
那一定要清楚其存在的意义,给定任意两棵树,采用先序深度优先遍历
的算法找到最少的转换步骤
diff
比较两个虚拟DOM
的区别,也就是在比较两个对象的区别。
作用:在patch
子vnode
过程中,找到与新vnode
对应的老vnode
,复用真实的DOM
节点,避免不必要的性能开销。
// diff.js
function diff(oldTree, newTree) {
// 声明变量patches用来存放补丁的对象
let patches = {};
// 第一次比较应该是树的第0个索引
let index = 0;
// 递归树 比较后的结果放到补丁里
walk(oldTree, newTree, index, patches);
return patches;
}
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;
}
}
function isString(obj) {
return typeof obj === 'string';
}
function diffAttr(oldAttrs, newAttrs) {
let patch = {};
// 判断老的属性中和新的属性的关系
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key]; // 有可能还是undefined
}
}
for (let key in newAttrs) {
// 老节点没有新节点的属性
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key];
}
}
return patch;
}
// 所有都基于一个序号来实现
let num = 0;
function diffChildren(oldChildren, newChildren, patches) {
// 比较老的第一个和新的第一个
oldChildren.forEach((child, index) => {
walk(child, newChildren[index], ++num, patches);
});
}
// 默认导出
export default diff;
{type: 'REMOVE', index}
{type: 'TEXT', text: 1}
{type: 'ATTR', attr: {class: 'list-group'}}
{type: 'REPLACE', newNode}
每个元素都有一个补丁,所以需要创建一个放当前补丁的数组
if (!newNode) {
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 > 0) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
打补丁需要传入两个参数,一个是要打补丁的元素,另一个就是所要打的补丁
import { Element, render, setAttr } from './element';
let allPatches;
let index = 0; // 默认哪个需要打补丁
function patch(node, patches) {
allPatches = patches;
// 给某个元素打补丁
walk(node);
}
function walk(node) {
let current = allPatches[index++];
let childNodes = node.childNodes;
// 先序深度,继续遍历递归子节点
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current); // 打上补丁
}
}
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;
}
});
}
export default patch;
// index.js
import { createElement, render, renderDom } from './element';
// +++ 引入diff和patch方法
import diff from './diff';
import patch from './patch';
// +++
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
let el = render(virtualDom);
renderDom(el, window.root);
// +++
// 创建另一个新的虚拟DOM
let virtualDom2 = createElement('ul', {class: 'list-group'}, [
createElement('li', {class: 'item active'}, ['七里香']),
createElement('li', {class: 'item'}, ['一千年以后']),
createElement('li', {class: 'item'}, ['需要人陪'])
]);
// diff一下两个不同的虚拟DOM
let patches = diff(virtualDom, virtualDom2);
console.log(patches);
// 将变化打补丁,更新到el
patch(el, patches);