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。

05-12 01:41