Vue之虚拟DOM

151 阅读4分钟

Virtual DOM

浏览器中真正的 DOM 元素是非常庞大的,频繁去做 DOM 更新,会产生一定的性能问题。

虚拟 DOM(Virtual DOM)是对 DOM 的抽象,本质上是 JavaScript 对象.

优势:

  • 减少 JavaScript 操作真实 DOM 带来的性能消耗。
  • 抽象了原本的渲染过程,实现了跨平台的能力。

虚拟 DOM 就是用原生 JS 对象描述 DOM 节点,包含tagpropschildren 三个属性。

<div id="app">
  <p class="text">hello world!!!</p>
</div>

这一段用虚拟 DOM 描述为:

{
  tag: 'div',
  props: {
    id: 'app'
  },
  chidren: [
    {
      tag: 'p',
      props: {
        className: 'text'
      },
      chidren: [
        'hello world!!!'
      ]
    }
  ]
}

虚拟 DOM 提升性能的点在于 DOM 发生变化的时候,通过 diff 算法比对 JavaScript 原生对象,计算出需要变更的 DOM,然后只对变化的 DOM 进行操作,而不是更新整个视图。

在 Vue 中,Virtual DOM 是用 VNode 这么一个 Class 去描述,VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法。

创建

// 虚拟DOM元素的类,构建实例对象,用来描述DOM
class Element {
    constructor(type, props, children) {
        this.type = type;
        this.props = props;
        this.children = children;
    }
}
// 创建虚拟DOM,返回虚拟节点(object)
function createElement(type, props, children) {
    return new Element(type, props, children);
}
// 调用createElement
let virtualDom = createElement('ul', {class: 'list'}, [
    createElement('li', {class: 'item'}, ['aa']),
    createElement('li', {class: 'item'}, ['bb']),
    createElement('li', {class: 'item'}, ['cc'])
]);

渲染

// 渲染
function render(domObj) {
    // 根据type类型来创建对应的元素
    let el = document.createElement(domObj.type);
    
    // 遍历props属性对象,然后给创建的元素el设置属性
    for (let key in domObj.props) {
        // 设置属性的方法
        setAttr(el, key, domObj.props[key]);
    }
    
    // 遍历子节点,如果是虚拟DOM,就继续递归渲染,不是就代表是文本节点,直接创建
    domObj.children.forEach(child => {
        child = (child instanceof Element) ? render(child) : document.createTextNode(child);
        // 添加到对应元素内
        el.appendChild(child);
    });

    return el;
}

// 设置属性
function setAttr(node, key, value) {
    switch(key) {
        case 'value':
            // node是一个input或者textarea就直接设置其value
            if (node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea') {
                node.value = value;
            } else {
                node.setAttribute(key, value);
            }
            break;
        case 'style':
            // 直接赋值行内样式
            node.style.cssText = value;
            break;
        default:
            node.setAttribute(key, value);
            break;
    }
}

// 将元素插入到页面内
function renderDom(el, target) {
    target.appendChild(el);
}

// 调用
let el = render(virtualDom); // 渲染虚拟DOM得到真实的DOM结构
renderDom(el, document.getElementById('root'));

DOM-diff

DOM-diff 是比较两个虚拟 DOM 的区别,找出需要变更的 DOM。

// 比较两颗dom树
function diff(oldTree, newTree) {
   // 声明变量patches用来存放变更的内容
   let patches = {};
   // 第一次比较应该是树的第0个索引
   let index = 0;
   // 递归树 比较后的结果放到patches里
   walk(oldTree, newTree, index, patches);

   return patches;
}

// 深度优先遍历
function walk(oldNode, newNode, index, patches) {
   // 每个元素都有一个变量,用来存放变更的内容
   let current = [];

   if (!newNode) { // 新的DOM节点不存在
       current.push({ type: 'REMOVE', index });
   } else if (isString(oldNode) && isString(newNode)) {
       // 判断文本是否一致
       if (oldNode !== newNode) {
           current.push({ type: 'TEXT', text: newNode });
       }

   } else if (oldNode.type === newNode.type) {
       // 比较属性是否有更改
       let attr = diffAttr(oldNode.props, newNode.props);
       // Object.keys 返回对象的所有属性
       if (Object.keys(attr).length > 0) {
           current.push({ type: 'ATTR', attr });
       }
       // 如果有子节点,遍历子节点
       diffChildren(oldNode.children, newNode.children, patches);
   } else {    // type不同,说明节点被替换了
       current.push({ type: 'REPLACE', newNode});
   }
   
   // 当前元素确实有变化存在
   if (current.length) {
       // 将元素和变量对应起来,放到patches中
       patches[index] = current;
   }
}

function isString(obj) {
   return typeof obj === 'string';
}

function diffAttr(oldAttrs, newAttrs) {
   let patch = {};
   // 判断老的属性和新的属性的关系
   for (let key in oldAttrs) {
       if (oldAttrs[key] !== newAttrs[key]) {
           patch[key] = newAttrs[key]; // 有可能还是undefined
       }
   }

   for (let key in newAttrs) {
       // 老节点没有新节点的属性
       if (!oldAttrs.hasOwnProperty(key)) {
           patch[key] = newAttrs[key];
       }
   }
   return patch;
}

// 定义一个索引
let num = 0;
// 遍历子节点
function diffChildren(oldChildren, newChildren, patches) {
   // 比较老的第一个和新的第一个
   oldChildren.forEach((child, index) => {
       walk(child, newChildren[index], ++num, patches);
   });
}

比较规则总结:

  • 新节点不存在,节点删除:{ type: 'REMOVE', index }
  • 对于文本节点,比较文本不同:{ type: 'TEXT', text: newNode }
  • 类型type相同时,比较属性:{ type: 'ATTR', attr }
  • 类型不同时,节点被替换:{ type: 'REPLACE', newNode}

更新

需要传入两个参数,要更新的dom元素,和更新的内容

let allPatches;
let index = 0;  // 默认索引

function patch(node, patches) {
    allPatches = patches;
    walkPatches(node);
}

function walkPatches(node) {
    let current = allPatches[index++];
    let childNodes = node.childNodes;

    // 先序深度,继续遍历递归子节点
    childNodes.forEach(child => walkPatches(child));

    if (current) {
        doPatch(node, current); // 更新
    }
}

function doPatch(node, patches) {
    // 遍历所有更新的内容
    patches.forEach(patch => {
        switch (patch.type) {
            case 'ATTR':
                for (let key in patch.attr) {
                    let value = patch.attr[key];
                    if (value) {
                        setAttr(node, key, value);
                    } else {
                        node.removeAttribute(key);
                    }
                }
                break;
            case 'TEXT':
                node.textContent = patch.text;
                break;
            case 'REPLACE':
                let newNode = patch.newNode;
                // 新节点如果是对象实例,就渲染成dom,如果不是就是文本节点,直接创建
                newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
                node.parentNode.replaceChild(newNode, node);
                break;
            case 'REMOVE':
                node.parentNode.removeChild(node);
                break;
            default:
                break;
        }
    });
}

最终使用

// 创建虚拟DOM
let virtualDom = createElement('ul', {class: 'list'}, [
    createElement('li', {class: 'item'}, ['aa']),
    createElement('li', {class: 'item'}, ['bb']),
    createElement('li', {class: 'item'}, ['cc'])
]);

// 渲染成真实的DOM
let el = render(virtualDom);
renderDom(el, window.root);

// 创建另一个新的虚拟DOM
let virtualDom2 = createElement('ul', {class: 'list-group'}, [
    createElement('li', {class: 'item active'}, ['ddd']),
    createElement('li', {class: 'item'}, ['eee']),
    createElement('li', {class: 'item'}, ['fff'])    
]);
// diff一下两个不同的虚拟DOM
let patches = diff(virtualDom, virtualDom2);
// 更新视图
patch(el, patches);

参考:

juejin.cn/post/684490…

github.com/livoras/blo…