虚拟DOM - freddielovekqy/dung-beetle GitHub Wiki

真实DOM和其解析流程

浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting

第一步,用HTML分析器,分析HTML元素,构建一颗DOM树(标记化和树构建)。

第二步,用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。

第三步,将DOM树和样式表,关联起来,构建一颗Render树(这一过程又称为Attachment)。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。

第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。

第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。

通过JS来操作DOM是非常消耗性能的,其中主要的会消耗性能的点有:

  1. 浏览器的 JavaScript 引擎与 DOM 引擎共享一个主线程。任何 DOM API 调用都要先将 JS 数据结构转为 DOM 数据结构,再挂起 JS 引擎并启动 DOM 引擎,执行过后再把可能的返回值反转数据结构,重启 JS 引擎继续执行。这种上下文切换很耗性能,类似的还有单机进程间调用、远程过程调用等。
  2. 页面的回流与重绘

虚拟DOM

用于描述真实DOM的普通JS对象。

虚拟DOM并不是完全为了优化DOM操作而诞生的,而是:

  1. 隔离底层的DOM操作,更加聚焦于业务逻辑的处理
  2. 使用内存开销更小的JS对象操作来代替DOM对象,优化复杂场景的DOM操作

尤大原话在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。

核心原理

虚拟DOM对象

虚拟DOM包含三个核心属性:tagNameattrschildren(属性名可能名称不同) tagName:表示DOM节点的标签类型 attrs:包含所有DOM节点的属性,包含classstylevaluechildren:表示DOM节点的子节点

使用JS对象表示,他的数据模型为

class VElement {
  tagName
  attrs
  children
  key // DOM节点的主键,主要用于diff时的性能优化
  connt // 子节点数量
}

一个简单的真实DOM

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

抽象后的虚拟DOM对象

{
  tagName: 'div',
  attrs: {
    id: 'app'
  },
  chidren: [
    {
      tagName: 'p',
      attrs: {
        'class': 'text'
      },
      chidren: [
        'hello world!!!'
      ]
    }
  ]
}

渲染虚拟DOM

一下介绍虚拟DOM渲染到浏览器上的步骤

render () {
  const ele = window.document.createElement(this.tagName)
  for (const attrKey in this.attrs) {
    this.setAttr(ele, attrKey, this.attrs[attrKey]);
  }
  this.children.map(child => {
    if (child instanceof VElement) {
      ele.append(child.render());
    } else if (child instanceof Element || typeof child === 'string') {
      ele.append(child);
    }
  });
  return ele;
}

/**
 * 给element设置属性
 * @param { Element } node element节点
 * @param { String } key 属性key值
 * @param { String|Object } value 属性value值
 */
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':
      let cssStr = ''
      if (typeof value === 'object') {
        for (const key in value) {
          cssStr += `${key}:${value[key]};`;
        }
      } else if (typeof value === 'string') {
        cssStr = value;
      }
      node.style.cssText = cssStr;
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
}

生成的原生DOM对象插入相应的根节点上即可。

diff算法

上述通过对虚拟DOM对象的渲染,页面能准确的呈现出js表达的虚拟DOM,当发生业务变化,js不直接操作DOM对象,而是修改虚拟DOM对象,并通过变化后的虚拟DOM对象与原先的DOM对象进行比对,获取变化的最小范围,最终通过调用原生的DOM操作方法将业务变化同步到页面中,再次保持虚拟DOM与原生DOM的一致。

当前newNode与oldNode之间的比对算法主要有:virtual-domsanbbdomcito

virtual-dom

他是最早出现的虚拟DOM的dif算法,采用深度优先遍历、同级比较的方式。 由于采用同级比较的方式,同级比较的节点,他们的差异类型主要有:

  • 节点替换:节点的tagName发生变化,如原先的div节点变更为span节点
  • 节点删除:原节点在新节点中不存在
  • 子节点变更:子节点的顺序变更以及增加
  • 属性变更:节点属性变更,如class样式变更
  • 文本修改:节点文本内容变更
DOM树的遍历

以深度优先的方式,遍历所有的树节点,比对所有树节点发生的变化,将所有的变化记录在patches中,一个节点允许有多个patch。 enter image description here enter image description here 定义全局index记录节点发生的位置

let patch = patches[index]

场景1:同层老节点不存在

{ type: PATCH.INSERT, vNode: newNode }

场景2:同层新节点不存在

{ type: PATCH.REMOVE, vNode: null }

场景3:新老节点tag不一致

{ type: PATCH.REPLACE, vNode: newNode }

场景4:新老节点属性不一致

{ type: PATCH.PROPS, patches: propsPatch }

场景5:新老节点文本内容不一致

{ type: PATCH.VTEXT, vNode: newNode }

场景6:新老节点子节点顺序变化

{ type: PATCH.ORDER, moves: sortedSet.moves }

进行子节点的比对时,如果直接进行比对,在如下的场景中,会重复创建原先存在但是位置发生变化的节点。为了优化性能,对新老子节点列表进行重新排序,即有了场景6中的ORDER类型的patch enter image description here

更新DOM

根据前面diff算法获取到变更的信息后,将patches更新到真实DOM浏览器视图上。 遍历真实DOM树,重组成数组对象形式

function domIndex(rootNode) {
  const nodes = [rootNode]
  const children = rootNode.childNodes
  if (children.length) {
    for (let child of children) {
      if (child.nodeType === 1 || child.nodeType === 3) {
        if (child.nodeType === 1) {
          nodes.push(...domIndex(child))
        } else if (child.nodeType === 3) {
          nodes.push(child)
        }
      }
    }
  }
  return nodes
}

根据patch的index获取到原DOM,进行实际DOM操作

patches.forEach((patch, index) => {
  patch && applyPatch(nodes[index], patch)
})

function patchOp(node, patch) {
  const { type, vNode } = patch
  const parentNode = node.parentNode
  let newNode = null
  switch (type) {
    case PATCH.INSERT:
      // 插入新节点
      break
    case PATCH.REMOVE:
      // 删除旧新节点
      break
    case PATCH.REPLACE:
      // 替换节点
      break
    case PATCH.ORDER:
      // 子节点重新排序
      break
    case PATCH.VTEXT:
      // 替换文本节点
      break
    case PATCH.PROPS:
      // 更新节点属性
      break
    default:
      break
  }
}

sanbbdom

enter image description here

不生成patches,在比对的时候直接更新DOM

enter image description here

VUE的渲染

enter image description here

⚠️ **GitHub.com Fallback** ⚠️