这篇文章上次修改于 268 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

        Virtual Dom,就是一个js对象,具体点就是一个使用javascript模拟了DOM结构的树形结构对象,这个树结构包含整个DOM结构的信息。

        真实dom的开销是很大的,在我们改变页面的某个元素的状态时,浏览器会重新绘制整个render Dom tree,比如用js同时改变十处节点的状态时,浏览器就会有十次的重绘操作,效率是非常低下的。而virtual Dom不会重新构建整个dom tree,它只会去更新改变的节点。大体就是在Vdom挂载到页面后,会将真实的dom存放在old virtual dom的一个属性中,这样在newVnodeoldVnode Diff(打补丁)时,若发生了改变,就只去更新当前vNodeDom的状态,而不是重新去构建整个Dom Tree,这样大大提高了性能。

        该篇文章只是简单手写一个vue的dom diff,跟源码不一样,看者慎看,源码链接

构建虚拟dom

源码很复杂,这里就简单的给构建方法传三个参数:节点类型,配置属性,内容

举个例子,下面我们要构建一个简单的dom

<div id="wrap" style="color: red">
    <h1>Virtual</h1>
    Dom
</div>

可以分解为

{
    tag: 'div',
    config: {
       id: 'wrap',
       style: {color: 'red'}
    },
    children: [
        {
            tag: 'h1',
            config: {},
            children: ['Virtual']
        },
        'Dom'
    ]
}

然后通过调用createVDom来创建Vnode(虚拟Dom节点),具体解释可以看看如下代码:

const hasOwnProperty = Object.prototype.hasOwnProperty;
function createVDom(tag, config, ...children) {
  let props = {};  // 存放dom属性
  let key;
  if(config) { // 保存节点的key
    if(config.key) {
      key = config.key;
    }
  }

  for(let prop in config) { // 遍历config,将属性存放在props对象中
    if (hasOwnProperty.call(config, prop) && prop !== 'key') {
      props[prop] = config[prop];
    }
  }
  
  // 调用createVNode创建Vnode的树结构对象,并返回
  return createVNode(tag, key, props, children.map((child, index) => (
    typeof child ==='number' || typeof child === 'string' ? createVNode(
      undefined, undefined, undefined, undefined, child
    ) : child
  )))
}

function createVNode (tag, key, props={}, children, text, domElement) {
  // 创建基本的Vnode的对象结构
  return {
    _tag: VIRTUAL_NODE, tag, key, props, children, text, domElement
  }
}

用户通过调用上述默认导出的方法(命名为createVDom),就能创建Vnode

const vNode = createVDom(
   'div',
   { style: { color: 'red' }, id: 'wrap'},
   createVDom('h1', {}, ''}, 'Virtual')),
   'Dom'
)

具体结构如下

挂载Vnode

创建一个render方法,用来将Vnode渲染到页面上

function render(vNode, container) {
  let newDomElement = createNewDomElement(vNode);
  container.appendChild(newDomElement)
}

再创建两个方法(后面diff会用到,所以要抽出来,单独声明)

  • createNewDomElement 用来创建dom,分两种情况

    • 文本节点,出字符串或数字
    • 普通html标签节点(若该节点存在儿子节点,则接着递归遍历children创建)
  • updateDomProperties 用来更新dom的属性(新、老节点比较),后面diff会用到,先说明一下:

    • 首先更新样式(首次创建的直接略过)

      • 老有新无,要删除老的
      • 老无新有,要添加新的
    • 删除新中没有老的属性
    • 将配置属性添加到dom
// 更新dom的属性
// oldProps设默认值是因为首次没有oldVnode
const updateDomProperties = function(vNode, oldProps={}) {
  const { props, domElement } = vNode;
  let oldStytle = oldProps.style || {};
  let newStyle = props.style;
  // 更新样式
  // 老有新无,要删除;老无新有,要添加
  for(let oldStyleAttr in oldStytle) {
    if(!newStyle[oldStyleAttr]) {
      domElement.style[oldStyleAttr] = '';
    }
  }
  // 更新属性
  //删除新中没有老的属性
  for(let oldPropName in oldProps) {
    if(!props[oldPropName]) {
      delete domElement[oldPropName];
    }
  }
  // 把新属性添加和更新到真实dom上
  for(let newPropName in props) {
    if(newPropName === 'style') {
      let newStyleObj = props.style;
      for(let newStyleName in newStyleObj) {
        domElement.style[newStyleName] = newStyleObj[newStyleName];
      }
    } else {
      domElement[newPropName] = props[newPropName];
    }
  }
}

const createNewDomElement = function(vNode) { // 创建real dom
  const { tag, children } = vNode;
  // 判断是否是文本节点
  if (tag) {
    let domElement = vNode.domElement = document.createElement(tag);
    updateDomProperties(vNode);
    if(Array.isArray(children)) {
      children.map(child => domElement.appendChild(createNewDomElement(child)));
    }
  } else {
    vNode.domElement = document.createTextNode(vNode.text);
  }
  return vNode.domElement;
}

调用render方法生成real dom(真实dom)

const root = document.getElementById('root');
render(vNode, root);

挂载后结果,如下图

准备工作做完了,下一篇文章,将进行diff算法,点击进入