通过编写简易虚拟DOM,来学习虚拟DOM 的原理
Posted 前端大联盟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过编写简易虚拟DOM,来学习虚拟DOM 的原理相关的知识,希望对你有一定的参考价值。
茫茫人海中与你相遇
相信未来的你不会很差
来源:https://segmentfault.com/a/1190000037599333
-
Virtual DOM 是真实DOM的映射 -
当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差 异,然后只需要在真实的 DOM 上做出相应的改变。
用JS对象模拟DOM树
<ul class=”list”>
<li>item 1</li>
<li>item 2</li>
</ul>
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
-
用如下对象表示DOM元素
{ type: ‘…’, props: { … }, children: [ … ] }
-
用普通 JS 字符串表示 DOM 文本节点
function h(type, props, …children) {
return { type, props, children };
}
h(‘ul’, { ‘class’: ‘list’ },
h(‘li’, {}, ‘item 1’),
h(‘li’, {}, ‘item 2’),
);
<ul className=”list”>
<li>item 1</li>
<li>item 2</li>
</ul>
编译成:
React.createElement(‘ul’, { className: ‘list’ },
React.createElement(‘li’, {}, ‘item 1’),
React.createElement(‘li’, {}, ‘item 2’),
);
是不是看起来有点熟悉?如果能够用我们刚定义的 h(...)
函数代替 React.createElement(…)
,那么我们也能使用JSX 语法。其实,只需要在源文件头部加上这么一句注释:
/** @jsx h */
<ul className=”list”>
<li>item 1</li>
<li>item 2</li>
</ul>
h(...)
函数代替
React.createElement(…)
,然后 Babel 就开始编译。'
h(...)
函数代替
React.createElement(…)
,然后 Babel 就开始编译。'
/** @jsx h */
const a = (
<ul className=”list”>
<li>item 1</li>
<li>item 2</li>
</ul>
);
const a = (
h(‘ul’, { className: ‘list’ },
h(‘li’, {}, ‘item 1’),
h(‘li’, {}, ‘item 2’),
);
);
h
const a = (
{ type: ‘ul’, props: { className: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
);
从Virtual DOM 映射到真实 DOM
-
使用以' $
'开头的变量表示真正的DOM节点(元素,文本节点),因此 $parent 将会是一个真实的DOM元素 -
虚拟 DOM 使用名为 node
的变量表示
createElement(…)
,它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑
props
和
children
属性:
function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
return document.createElement(node.type);
}
上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:
{ type: ‘…’, props: { … }, children: [ … ] }
createElement
传入虚拟文本节点和虚拟元素节点——这是可行的。
appendChild()
添加到我们的元素中:
function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
哇,看起来不错。先把节点 props
属性放到一边。待会再谈。我们不需要它们来理解虚拟DOM的基本概念,因为它们会增加复杂性。
完整代码如下:
/** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
const a = (
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
);
const $root = document.getElementById('root');
$root.appendChild(createElement(a));
比较两棵虚拟DOM树的差异
-
添加新节点,使用 appendChild(…) 方法添加节点
-
移除老节点,使用 removeChild(…) 方法移除老的节点
-
节点的替换,使用 replaceChild(…) 方法
$parent
、newNode 和 oldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。
添加新节点
function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}
移除老节点
$parent.removeChild(…)
方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 $parent.childNodes[index] 得到该节点的引用。
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
}
}
节点的替换
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === ‘string’ && node1 !== node2 ||
node1.type !== node2.type
}
现在,当前的节点有了 index 属性,就可以很简单的用新节点替换它:
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
}
}
比较子节点
-
当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 ) -
我们需要传递当前的节点的引用作为父节点 -
我们应该一个一个的比较所有的子节点,即使它是 undefined
也没有关系,我们的函数也会正确处理它。 -
最后是 index,它是子数组中子节点的 index
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
完整的代码
Babel+JSX
/* @jsx h /
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === 'string' && node1 !== node2 ||
node1.type !== node2.type
}
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
// ---------------------------------------------------------------------
const a = (
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
);
const b = (
<ul>
<li>item 1</li>
<li>hello!</li>
</ul>
);
const $root = document.getElementById('root');
const $reload = document.getElementById('reload');
updateElement($root, a);
$reload.addEventListener('click', () => {
updateElement($root, b, a);
});
HTML
<button id="reload">RELOAD</button>
<div id="root"></div>
CSS
#root {
border: 1px solid black;
padding: 10px;
margin: 30px 0 0 0;
}
打开开发者工具,并观察当按下“Reload”按钮时应用的更改。
总结
-
设置元素属性(props)并进行 diffing/updating -
处理事件——向元素中添加事件监听 -
让虚拟 DOM 与组件一起工作,比如React -
获取对实际DOM节点的引用 -
使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件
我们在虚拟的空间与你相遇,期待可以碰撞出不一样的火花
以上是关于通过编写简易虚拟DOM,来学习虚拟DOM 的原理的主要内容,如果未能解决你的问题,请参考以下文章
前端技能树,面试复习第 27 天—— React Diff 算法的原理,和 Vue 有什么区别 | 虚拟 DOM | key 的原理,为什么要用