当咱们操做Dom实际上是一件很是耗性能的事,每一个元素都涵盖了许多的属性,由于浏览器的标准就把 DOM 设计的很是复杂。而Virtual Dom就是用一个原生的JS对象去描述一个DOM节点,即VNode,因此它比建立一个真实的Dom元素所产生代价要小得多。而咱们主流的框架React和Vue正是采用了这种作法,那咱们来看下如何实现一个简单的Virtual Dom。完整代码GitHub。喜欢的话但愿点个小星星哦 ^_^~~~node
首先咱们须要构建vDom, 用js对象来描述真正的dom tree,构建好了vDom以后就须要将其render到咱们的页面上了git
// createElement.js
// give some default value.
export default (tagName, {attrs = {}, children = []} = {}) => {
return {
tagName,
attrs,
children
}
}
// main.js
import createElement from './vdom/createElement'
const createVApp = (count) => createElement('div', {
attrs: {
id: 'app',
dataCount: count
},
children: [
createElement('input'), // dom重绘使得Input失焦
String(count), // 文本节点
createElement('img', {
attrs: {
src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1555610261877&di=6619e67b4f45768a359a296c55ec1cc3&imgtype=0&src=http%3A%2F%2Fimg.bimg.126.net%2Fphoto%2Fmr7DezX-Q4GLNBM_VPVaWA%3D%3D%2F333829322379622331.jpg'
}
})
]
})
let count = 0;
let vApp = createVApp(count);
复制代码
下面这个就是构建的 vDom 啦!github
而后我咱们看看render 方法,这个方法就是将咱们的 vDom 转化成真是的 element.算法
// render.js
const renderElem = ({ tagName, attrs, children}) => {
// create root element
let $el = document.createElement(tagName);
// set attributeds
for (const [k, v] of Object.entries(attrs)) {
$el.setAttribute(k, v);
}
// set children (Array)
for (const child of children) {
const $child = render(child);
$el.appendChild($child);
}
return $el;
}
const render = (vNode) => {
// if element node is text, and createTextNode
if (typeof vNode === 'string') {
return document.createTextNode(vNode);
}
// otherwise return renderElem
return renderElem(vNode);
}
export default render
复制代码
而后咱们回到main.js中数组
// 引入 render.js 模块
const $app = render(vApp); // 开始构建真实的dom
let $rootEl = mount($app, document.getElementById('app'));
// 建立 mount.js
export default ($node, $target) => {
// use $node element replace $target element!
$target.replaceWith($node);
return $node;
}
复制代码
最后你就能够看到效果了. 是否是很帅 ? O(∩_∩)O哈哈 ~~~~浏览器
如今咱们来作一些好玩的事儿。回到 main.js 中,咱们加入以下这段代码:bash
setInterval(() => {
count++;
$rootEl = mount(render(createVApp(count)), $rootEl); // $rootEl 就是整颗real dom
}, 1000)
复制代码
而后回到咱们的页面,发现什么了吗? 你能够尝试在 input 里面输入一些东西,而后发现了什么异常了吗 ?app
查看源代码,原来,每隔一秒咱们就刷新了一次页面。但是咱们只改变了 count ,就重绘一次页面,未免也夸张了吧,假如咱们填写一个表单,填的手都要断了,结果刷新了页面,你猜会怎么着? 会不会想砸电脑呢 ? 别急,diff 算法能帮咱们解决这给使人头疼的问题 !框架
diff 算法的概念我就在这儿就不介绍了,你们能够在网上搜到不少答案。直接上代码 !dom
// diff.js
import render from './render'
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttributes = (oldAttrs, newAttrs) => {
const patches = [];
// set new attributes
// oldAttrs = {dataCount: 0, id: 'app'}
// newAttrs = {dataCount: 1, id: 'app'}
// Object.entries(newAttrs) => [['dataCount', 1], ['id', 'app']]
for(const [k, v] of Object.entries(newAttrs)) {
patches.push($node => {
$node.setAttribute(k, v);
return $node;
})
}
// remove old attribute
for(const k in oldAttrs) {
if (!(k in newAttrs)) {
// $node 是整颗真实的 dom tree
patches.push($node => {
$node.removeAttribute(k);
return $node;
})
}
}
return $node => {
for (const patch of patches) {
patch($node);
}
}
}
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {
childPatches.push(diff(oldVChild, newVChild));
}
const additionalPatches = [];
for (const additionalVChild of additionalPatches.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
})
}
return $parent => {
for (const [patch, child] of zip(childPatches, $parent.childNodes)) {
patch(child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
}
}
const diff = (vOldNode, vNewNode) => {
// remove all
if (vNewNode === 'undefined') {
return $node => {
// Node.remove() 方法,把对象从它所属的DOM树中删除。
$node.remove();
return undefined;
};
}
// when element is textnode (like count)
if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {
if (vOldNode !== vNewNode) {
return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
} else {
return $node => undefined;
}
}
if (vOldNode.tagName !== vNewNode.tagName) {
return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
}
const patchAttrs = diffAttributes(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
// main.js
setInterval(() => {
count++;
// 每隔一秒,重绘一次页面,input失焦(缺点)
// $rootEl = mount(render(createVApp(count)), $rootEl)
// 衍生出 diff 算法
const vNewApp = createVApp(count); // 新的 vDom
const patch = diff(vApp, vNewApp); // 对比差别
$rootEl = patch($rootEl);
vApp = vNewApp; // 每一秒以后都有更新,保存起来以供下次比对。
}, 1000)
复制代码
废话少说,先看效果 (: ~~
能够发现,input 没有状况,也就是说页面没有刷新,setInterval每次将count++, 页面上也只更新了变化了的属性以及文本,这就是diff算法的威力。
diff 函数接收两个参数,vOldNode 和 vNewNode.
比对属性好办,就是拿到新的 vDom 的属性,而后遍历老的 vDom 的属性,判断老的 vDom 的属性是否存在于新的 vDom 中。关键点我将它描述出来
最后就是要对比 children 了。
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {
childPatches.push(diff(oldVChild, newVChild));
}
复制代码
Virtual DOM 最核心的部分就是 diff 算法了,这里仍是比较复杂的,须要多加练习反复琢磨,好了,今天的介绍就到这了,若是喜欢你就点点赞哦 !