【JS】Vue-虚拟DOM

Vue-虚拟DOM

一、模板转换成视图的过程

  1. Vue.js通过编译将template 模板转换成渲染函数(h) ,执行渲染函数就可以得到一个虚拟节点树。
  2. 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

【JS】Vue-虚拟DOM

二、Virtual DOM

1.定义

Virtual DOM 其实就是一棵以 VNode 节点作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

简单来说,可以把Virtual DOM 理解为一个简单的JS对象,其中几个比较重要的属性:

  • tag 属性即这个vnode的标签属性
  • data 属性包含了最后渲染成真实dom节点后,节点上的class,attribute,style以及绑定的事件
  • children 属性是vnode的子节点
  • text 属性是文本属性
  • elm 属性为这个vnode对应的真实dom节点
  • key 属性是vnode的标记,在diff过程中可以提高diff的效率,后文有讲解

对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM的一个过程

【JS】Vue-虚拟DOM

2.作用

虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。

为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。

其实虚拟DOM在Vue.js主要做了两件事:

**提供与真实DOM节点所对应的虚拟节点vnode,
将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图**

3.优势:

  • 具备跨平台的优势: 由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

  • 操作 DOM 慢,js运行效率高我们可以将DOM对比操作放在JS层,提高效率: 因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

  • 提升渲染性能: Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

三、diff算法

Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。

diff 算法包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了

四、实现代码

1. template 模板转换成渲染函数(h)

const root = document.getElementById('root');

const oldVnode = h('ul', { id: 'container' },

h('li', { style: { backgroundColor: '#110000' }, key: 'A' }, 'A'),

h('li', { style: { backgroundColor: '#440000' }, key: 'B' }, 'B'),

h('li', { style: { backgroundColor: '#770000' }, key: 'C' }, 'C'),

);

const newVnode = h('ul', { id: 'newContainer' },

h('li', { style: { backgroundColor: '#440000' }, key: 'B' }, 'B1'),

h('li', { style: { backgroundColor: '#110000' }, key: 'A' }, 'A1'),

h('li', { style: { backgroundColor: '#AA0000' }, key: 'C' }, 'C1'),

// h('li', { style: { backgroundColor: '#AA0000' }, key: 'E' }, 'E1'),

);

mount(oldVnode, root);

setTimeout(() => {

patch(oldVnode, newVnode);

}, 1000)

2. 通过渲染函数转化为 虚拟DOM树

function h(type, props, ...children) {

let key, newProps = {};

if (props) {

if (props.key) {

key = props.key;

delete props.key;

}

for (let item in props) {

if (props.hasOwnProperty(item)) {

newProps[item] = props[item];

}

}

}

return vnode(type, key, props, children.map(child => {

// 处理文字节点

if (typeof child == 'string' || typeof child == 'number') {

return vnode(undefined, undefined, undefined, undefined, child);

}

return child;

}))

}

// 生成vnode节点

function vnode(type, key, props = {}, children, text, domElement) {

return {

type, key, props, children, text, domElement

}

}

3.渲染节点

1.把虚拟DOM节点封装成一个真实DOM节点

function createDOMElementFromVnode(vnode) {

let type = vnode.type;

let children = vnode.children;

if (type) {

// 普通节点 eg:div,span

vnode.domElement = document.createElement(vnode.type);

updateProperties(vnode);

if (Array.isArray(children)) {

children.map(child => {

return vnode.domElement.appendChild(createDOMElementFromVnode(child))

})

}

} else {

// 文本节点

vnode.domElement = document.createTextNode(vnode.text);

}

return vnode.domElement;

}

2.更新属性

function updateProperties(vnode, oldProps = {}) {

let newProps = vnode.props;

let domElement = vnode.domElement;

let oldStyle = oldProps.style || {};

let newStyle = newProps.style || {};

// 遍历老属性(样式和属性),查看是否新属性中存在

for (let item in oldProps) {

if (!newProps[item]) {

delete domElement[item];

}

}

for (let item in oldStyle) {

if (!newStyle[item]) {

domElement.style[item] = "";

}

}

// 遍历新属性 样式单独赋值

for (let item in newProps) {

if (item == 'style') {

let styleObj = newProps[item];

for (let styleItem in styleObj) {

domElement.style[styleItem] = styleObj[styleItem];

}

} else {

domElement[item] = newProps[item];

}

}

}

3.渲染节点

function mount(vnode, root) {

root.appendChild(createDOMElementFromVnode(vnode));

}

4.patch更新

1.新老节点的更新

// 新老节点的更新

function patch(oldVnode, newVnode){

// 节点不同 直接替换

if(oldVnode.type != newVnode.type){

return oldVnode.domElement.parentNode.replaceChild(createDOMElementFromVnode(newVnode),oldVnode.domElement);

}

//如果新节点是文本节点,那么直接修改文本内容

if(newVnode.text){

return oldVnode.domElement.textContent = newVnode.text;

}

let domElement = newVnode.domElement = oldVnode.domElement;

updateProperties(newVnode, oldVnode.props);

let oldChildren = oldVnode.children;

let newChildren = newVnode.children;

if(oldChildren.length > 0 && newChildren.length>0){

// 新老节点都存在

updateChildren(domElement,oldChildren,newChildren);

}else if(oldChildren.length > 0 ){

// 老节点存在子节点 ,新的没有

oldVnode.domElement.innerHTML = '';

}else if(newChildren.length>0){

// 老节点不存在子节点 ,新的有

for(let i = 0;i< newChildren.length;i++){

oldVnode.domElement.appendChild(createDOMElementFromVnode(newChildren[i]));

}

}

}

2.新老节点都存在,比较更新

// 新老节点都存在,比较更新,主要是列表

function updateChildren(parentDOMElement,oldChildren,newChildren){

let oldStartIndex = 0;

let oldStartNode = oldChildren[0];

let oldEndIndex = oldChildren.length-1;

let oldEndNode = oldChildren[oldEndIndex];

let newStartIndex = 0;

let newStartNode = newChildren[0];

let newEndIndex = newChildren.length-1;

let newEndNode = newChildren[newEndIndex];

let oldKeyToIndexMap = createKeyToIndexMap(oldVnode.children);

while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){

if(!oldStartNode){

// 如果此节点不存在,直接下移

oldStartNode = oldChildren[++oldStartIndex];

}else if(!oldEndNode){

oldEndNode = oldChildren[--oldEndIndex];

}else if(isSameVnode(oldStartNode,newStartNode)){

// 旧头节点 = 新头节点

patch(oldStartNode,newStartNode);

oldStartNode = oldChildren[++oldStartIndex];

newStartNode = newChildren[++newStartIndex];

}else if(isSameVnode(oldEndNode,newEndNode)){

// 旧尾节点 = 新尾节点

patch(oldEndNode,newEndNode);

oldEndNode = oldChildren[--oldEndIndex];

newEndNode = newChildren[--newEndIndex];

}else if(isSameVnode(oldStartNode,newEndNode)){

// 旧头节点 = 新尾节点

patch(oldStartNode,newEndNode);

parentDOMElement.insertBefore(oldStartNode.domElement,oldEndNode.domElement.nextSibling);

oldStartNode = oldChildren[++oldStartIndex];

newEndNode = newChildren[--newEndIndex];

}else if(isSameVnode(oldEndNode,newStartNode)){

// 旧尾节点 = 新头节点

patch(oldEndNode,newStartNode);

parentDOMElement.insertBefore(oldEndNode.domElement,oldStartNode.domElement);

oldEndNode = oldChildren[--oldEndIndex];

newStartNode = newChildren[++newStartIndex];

}else{

let oldIndexByKey = oldKeyToIndexMap[newStartNode.key];

if(oldIndexByKey == null){

// 新元素直接创建

parentDOMElement.insertBefore(createDOMElementFromVnode(newStartNode),oldStartNode.domElement);

}else{

let oldVnodeToMove = oldChildren[oldIndexByKey];

// console.log(oldKeyToIndexMap,oldIndexByKey,oldVnodeToMove, newStartNode);

if(oldVnodeToMove.type === newStartNode.type){

patch(oldVnodeToMove,newStartNode);

oldChildren[oldIndexByKey] = undefined;

oldKeyToIndexMap = createKeyToIndexMap(oldVnode.children); //防止key相同

parentDOMElement.insertBefore(oldVnodeToMove.domElement,oldStartNode.domElement);

}else{

// key相同,type不同,重建

parentDOMElement.insertBefore(createDOMElementFromVnode(newStartNode),oldStartNode.domElement);

}

}

newStartNode = newChildren[++newStartIndex];

}

}

// 处理剩余的新节点

if(oldStartIndex > oldEndIndex){

let beforeDOMElement = newChildren[newStartIndex+1] ? newChildren[newStartIndex+1].domElement:null;

for(let i = newStartIndex; i<= newEndIndex;i++){

parentDOMElement.insertBefore(createDOMElementFromVnode(newChildren[i]),beforeDOMElement);

}

}

// 删除剩余的旧节点

if(newStartIndex > newEndIndex){

for(let i = oldStartIndex; i<= oldEndIndex;i++){

parentDOMElement.removeChild(oldChildren[i].domElement)

}

}

}

// 获取key对应的位置index

function createKeyToIndexMap(children) {

let map = {};

for (let i = 0; i < children.length; i++) {

if (children[i] && children[i].key) {

map[children[i].key] = i;

}

}

return map;

}

//是否是相同的节点 类型相同并且key相同 key可能为null

function isSameVnode(oldVnode, newVnode) {

return oldVnode.key === newVnode.key && oldVnode.type === newVnode.type;

}

以上是 【JS】Vue-虚拟DOM 的全部内容, 来源链接: utcz.com/a/97165.html

回到顶部