Virtual DOM

Virtual DOM

通过 JS 对象模拟 DOM 对象

绑定了视图和状态

按需更新DOM

JS表示DOM

export default class VNode {
  constructor (
    tag,      // 标签名
    children, // 子节点
    text,     // 文本节点
    elm       // 对应的真实dom对象
  ) {
    this.tag = tag
    this.children = children
    this.text = text
    this.elm = elm
  }
}
import VNode from 'vnode'

// 创建非文本节点
var c = (tag, children) => {
  return VNode(tag, children,'')
}

// 创建文本节点
var t = (text) => {
  return VNode(null, [], text)
}

JS表示DOM

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
let ul =
    c('ul', [
      c('li', [ t("Item 1") ]),
      c('li', [ t("Item 2") ]),
      c('li', [ t("Item 3") ])
    ])

VD渲染为DOM

1. 创建根节点

2. 创建子节点

3. 挂载到父节点

VD渲染为DOM

function createElm (vnode, parentElm, refElm) {
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    vnode.elm = nodeOps.createElement(tag)
    createChildren(vnode, children)
    insert(parentElm, vnode.elm, refElm)
  } else { // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}
function createChildren (vnode, children) {
  for (let i = 0; i < children.length; ++i) {
    createElm(children[i], vnode.elm, null)
  }
}
function insert (parent, elm, ref) {
  if (parent) {
    if (ref) {
      document.insertBefore(parent, elm, ref)
    } else {
      document.appendChild(parent, elm)
    }
  }
}
createElm(
    ul,   // VNode
    document.getElementById("container"),
    null  // refElm
)

DIFF

patch (oldVnode, newVnode)

两个树的完全的 diff 算法的时间复杂度为 O(n^3)

前端中很少跨层移动DOM

Virtual DOM 只会对同一个层级的元素进行对比

prerequisite

DIFF

1. oldVnode和newVode节点不一致

function patch (oldVnode, newVnode) {
  if (sameVnode(oldVnode, newVnode)) {

  } else {
    // 1. 找到 oldVnode 对应的 DOM 节点及其父节点
    const oldElm = oldVnode.elm
    const parentElm = document.parentNode(oldElm)

    // 2. 把当前的 vnode 渲染在原来的父亲节点下
    createElm(
      newVnode,
      parentElm,
      document.nextSibling(oldElm)
    )

    // 3. 把旧的 DOM 节点从原来的 DOM 树上移除
    removeNode(oldVnode.elm)
  }
}

DIFF

2. oldVnode和newVode节点不一致

function patch (oldVnode, newVnode) {
  if (sameVnode(oldVnode, newVnode)) {
    // 如果两个 vnode 节点根一致
    updateChildren(oldVnode.children, newVnode.children)
  } else {

  }

DIFF

列表对比算法

最小编辑距离问题

通过动态规划求解,时间复杂度为 O(M * N)

这里偷个懒,用一个简单的算法来做

DIFF

updateChildren

定义四个指针:

oldStartVnode

oldEndVnode

newStartVnode

newEndVnode

Step 1

DIFF

updateChildren

Step 2

遍历对比

oldStartVnode 和 newStartVnode 相同

function updateChildren (parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if () {

    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
  }
}

DIFF

updateChildren

Step 2

遍历对比

oldEndVnode 和 newEndVnode 相同

function updateChildren (parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if () {

    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
  }
}

DIFF

updateChildren

Step 2

遍历对比

oldStartVnode 和 newEndVnode 相同

oldStartDom 挪到 oldEndDom后面

function updateChildren (parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if () {
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode)
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
  }
}

DIFF

updateChildren

Step 2

遍历对比

oldEndVnode 和 newStartVnode相同

oldEndDom 挪到 oldStartDom 之前

function updateChildren (parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if () {
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode)
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }
  }
}

DIFF

updateChildren

Step 2

遍历对比

其他情况

直接插入

function updateChildren (parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if () {
    } else {
      createElm(newStartVnode, parentElm, oldStartVnode.elm)
      newStartVnode = newCh[++newStartIdx]
    }
  }
}

DIFF

updateChildren

Step 3

处理剩余节点

function updateChildren (parentElm, oldCh, newCh) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

DIFF

辅助函数patchVnode

function patchVnode (oldVnode, vnode, removeOnly) {
  if (oldVnode === vnode) {
    return
  }
  const elm = vnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, removeOnly)
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1)
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
}

DEMO

Virtual DOM

By Joson Chen

Virtual DOM

  • 516