什么是虚拟DOM
Virtual dom, 即虚拟DOM节点。它通过JS
的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。
为什么操作 dom 性能开销大
从上图可见,真实的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。当我们频繁的去做 DOM 更新,会产生一定的性能问题。而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。
真实DOM转换成虚拟DOM
虚拟DOM就是一个普通的JavaScript对象,包含了tag
、props
、children
三个属性。
<div id="app">
<p class="text">TXM to SFM</p>
</div>
上面的HTML元素转换为虚拟DOM:
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'TXM to SFM'
]
}
]
}
接下来,我们详细的介绍一下上面的HTML是如何转换成下面的JS树形结构的虚拟DOM对象。
初始化项目
创建项目
我们使用React脚手架创建一个项目,方便调试、编译、开效果…
// 全局安装
sudo npm install -g create-react-app
// 生成项目
create-react-app dom-diff
// 进入项目目录
cd dom-diff
// 编译
npm run start
虚拟DOM
createElement核心方法
createElement 接受type
,props
,children
三个参数创建一个虚拟标签元素DOM的方法。
function createElement(type, props, children) {
return new Element(type, props, children);
}
为了提高代码高度的复用性,我们将创建虚拟DOM元素的核心逻辑代码放到Element
类中。
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
注意:将这些参数挂载到该对象的私有属性上,这样在new时也会有这些属性。
render核心方法
render方法接受一个虚拟节点对象参数,其作用是:将虚拟DOM转换成真实DOM。
function render(eleObj) {
let el = document.createElement(eleObj.type); // 创建元素
for(let key in eleObj.props) {
// 设置属性的方法
setAttr(el, key, eleObj.props[key])
}
eleObj.children.forEach(child => {
// 判断子元素是否是Element类型,是则递归,不是则创建文本节点
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
el.appendChild(child);
});
return el;
}
注意:在将虚拟DOM转换为真实DOM的时,转换属性时要考虑多种情况。像value
、style
等属性需要做特殊处理,具体的处理逻辑请看下方元素设置属性小节。
元素设置属性
在给元素设置属性的公共方法中接受三个参数:node
,key
,value
分别表示给那个元素设置属性、设置的属性名、以及设置属性的值。
function setAttr(node, key, value) {
switch(key) {
case 'value': // node是一个input或者textarea
if(node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
node.value = value;
} else { // 普通属性
node.setAttribute(key, value);
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
以上我们只考虑了三种情况的属性,当我们设置完属性,还要判断children
属性的情况,具体的处理逻辑请看下方递归设置儿子小节。
递归设置儿子
判断子元素是否是Element
元素类型,是则递归
,不是则创建文本节点。注意:我们只考虑了元素类型和文本类型两种。
eleObj.children.forEach(child => {
// 判断子元素是否是Element类型,是则递归,不是则创建文本节点
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
el.appendChild(child);
});
真实DOM渲染到浏览器上
我们都知道,render
方法的作用就是虚拟DOM转换为真实DOM,但是浏览器上为了看到效果,我们需要将真实DOM添加到浏览器上。我们写一个方法接受el
真实DOM和target
渲染目标两个参数。
function renderDom(el, target) {
target.appendChild(el);
}
上诉步骤完成后,就可以将这几个方法导出去供其他使用即可。
export {
createElement,
render,
Element,
renderDom
}