1,VUE中虚拟DOM原理
用JS对象表示DOM结构。
DOM很慢,而javascript很快。
用javascript对象可以很容易地表示DOM节点。
DOM节点包括标签、属性和子节点。根据虚拟DOM树构建出真实的DOM树。
具体思路:根据虚拟DOM节点的属性和子节点递归地构建出真实的DOM树。
用JS对象表示DOM结构,那么当数据状态发生变化而需要改变DOM结构时,我们先通过JS对象表示的虚拟DOM计算出实际DOM需要做的最小变动,然后再操作实际DOM,从而避免了粗放式的DOM操作带来的性能问题。
那么我们来看下如何实现一个虚拟DOM吧,嘤嘤嘤,这个问题面试官似乎很爱问。
同样是小陈阅读的文章,我觉得很好。来自于:
从零开始一步一步写一个简单的Virtual DOM实现https://segmentfault.com/a/1190000005659033
那么,我们开始吧。
1,首先是如何用JS对象表示出节点
假设存在:
<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 1'] }
] }
DOM中的纯文本节点会被表示为普通的JavaScript中的字符串。
对于大型的树,需要一个辅助函数来构造结构:
例如:
function h(type,props,...children){
return { type, props, children };
}
注意:…children这个是JS中的浅拷贝方法哦。
把例子中的DOM树用辅助函数表示:
h( 'ul', { 'class': 'list' },
h('li', { }, 'item 1') ,
h('li', { }, 'item 2' )
)
这种结构和转换方式很像JSX。
以Babel解释器为例,它会把上面提及的DOM树转化为如下结构:
React.createElement(‘ul’, { className: ‘list’ },
React.createElement(‘li’, {}, ‘item 1’),
React.createElement(‘li’, {}, ‘item 2’),
);
完成的 babel可编译代码为:
/** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
const a = (
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
);
console.log(a);
现在我们已经能将DOM节点用JS对象表示,接下来将虚拟DOM结构转化到真实的DOM中;
2,虚拟DOM转化到真实DOM中
注意:下文的表述中!
1,真实的DOM,譬如元素和文本节点,以$开头描述,例如
2,所有的虚拟DOM都用变量node描述
3,只有一个根节点存在
编写函数createElement,将输入的虚拟DOM转化为真实。
最简单的函数实现:
function creatElement(node){
//如果是文本节点,那么则创建文本节点
if (typeof node === 'string' ) {
return document.creatTextNode(node);
}
//否则
return document.creatElement(node.type);
}
接下来我们继续完善:
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;
}
这里的Array.prototype.map方法使用为:
方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
一个新数组,每个元素都是回调函数的结果。
节点的子节点调用createElement方法,获取了各自的生成的节点。
并对每个生成的节点使用appendChild方法,追加为父节点的子节点。
3,差异检测
我们已经将虚拟DOM节点转化为真实DOM节点,接下来是虚拟DOM的核心算法差异检测。
先从简单的虚拟DOM比较算法开始,保证只对真实DOM节点做最小改动。
改动的情况有:1.添加部分节点,2,删除部分节点,3,替换部分节点,4,节点标签发生变化,或者挂载到别处
针对以上四种变化,采用updataElement()对DOM树进行更新。
该函数会传入三个参数:
- $parent 代表Virtual DOM挂载在DOM树上的根节点
- newNode 新的Virtual DOM
- oldNode 老的Virtual DOM
初始化时候没有老的Virtual DOM情况
如果oldNode直接为空,那么我们只要简单地创建新的节点即可:
function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}
整个newNode被置空,即从DOM树中移除了:
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
}
完成更新函数:
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]
);
}
}
上面提及的算法里并没有对子节点进行检查,而在实际情况下,我们不仅要检查根节点,还要递归检查子节点是否发生了变化,即递归找到变化的那个节点,在编写代码之前,我们脑中要清楚以下几点:
1,只有对元素节点才需要进行子节点对比,文本节点是没有子节点的
2,递归过程中,会不断传入当前节点作为子节点对比的根节点处理
3,上面说的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
);
}
}
}
注:https://segmentfault.com/a/1190000004029168
在该文章中,函数传入一个数组patches用于记载根节点旗下,哪个节点发生了变化。
这个虚拟DOM实现只是简单的思路,真实的更为复杂,要记录哪些节点变化,并把记录节点变化的数组传入更新数组,去真正更新真实的DOM。