什么是虚拟DOM

Virtual dom, 即虚拟DOM节点。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。

为什么操作 dom 性能开销大

从上图可见,真实的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。当我们频繁的去做 DOM 更新,会产生一定的性能问题。而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。

真实DOM转换成虚拟DOM

虚拟DOM就是一个普通的JavaScript对象,包含了tagpropschildren三个属性。

<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的时,转换属性时要考虑多种情况。像valuestyle等属性需要做特殊处理,具体的处理逻辑请看下方元素设置属性小节。

元素设置属性

在给元素设置属性的公共方法中接受三个参数: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
}
posted @ 2020-04-21 10:35  乡乡  阅读(486)  评论(0编辑  收藏  举报