虚拟DOM的三个组成部分

虚拟DOM的结构、diff算法和patch过程

问题

虚拟DOM由哪三个部分组成?各自的作用是什么?

解答

虚拟DOM由三个部分组成:VNode(虚拟节点)diff 算法patch(打补丁)

1. VNode - 虚拟节点

用 JavaScript 对象描述真实 DOM 结构。

// 真实 DOM
// <div id="app" class="container">
//   <span>Hello</span>
// </div>

// 对应的 VNode
const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    class: 'container'
  },
  children: [
    {
      tag: 'span',
      props: {},
      children: ['Hello']
    }
  ]
}

// 创建 VNode 的函数
function h(tag, props, children) {
  return {
    tag,
    props: props || {},
    children: children || []
  }
}

// 使用
const vnode2 = h('div', { id: 'app' }, [
  h('span', null, ['Hello'])
])

2. diff 算法

比较新旧 VNode 的差异,找出需要更新的部分。

// 简化版 diff
function diff(oldVNode, newVNode) {
  const patches = []
  
  // 节点类型不同,直接替换
  if (oldVNode.tag !== newVNode.tag) {
    patches.push({ type: 'REPLACE', newVNode })
    return patches
  }
  
  // 比较属性
  const propsPatches = diffProps(oldVNode.props, newVNode.props)
  if (Object.keys(propsPatches).length > 0) {
    patches.push({ type: 'PROPS', props: propsPatches })
  }
  
  // 比较子节点
  const childrenPatches = diffChildren(oldVNode.children, newVNode.children)
  patches.push(...childrenPatches)
  
  return patches
}

function diffProps(oldProps, newProps) {
  const patches = {}
  
  // 找出修改和新增的属性
  for (const key in newProps) {
    if (oldProps[key] !== newProps[key]) {
      patches[key] = newProps[key]
    }
  }
  
  // 找出删除的属性
  for (const key in oldProps) {
    if (!(key in newProps)) {
      patches[key] = null
    }
  }
  
  return patches
}

function diffChildren(oldChildren, newChildren) {
  const patches = []
  const len = Math.max(oldChildren.length, newChildren.length)
  
  for (let i = 0; i < len; i++) {
    if (!oldChildren[i]) {
      patches.push({ type: 'ADD', index: i, vnode: newChildren[i] })
    } else if (!newChildren[i]) {
      patches.push({ type: 'REMOVE', index: i })
    } else {
      patches.push(...diff(oldChildren[i], newChildren[i]))
    }
  }
  
  return patches
}

3. patch - 打补丁

将 diff 产生的差异应用到真实 DOM 上。

// 根据 VNode 创建真实 DOM
function createElement(vnode) {
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode)
  }
  
  const el = document.createElement(vnode.tag)
  
  // 设置属性
  for (const key in vnode.props) {
    el.setAttribute(key, vnode.props[key])
  }
  
  // 递归创建子节点
  vnode.children.forEach(child => {
    el.appendChild(createElement(child))
  })
  
  return el
}

// 应用补丁
function patch(el, patches) {
  patches.forEach(p => {
    switch (p.type) {
      case 'REPLACE':
        const newEl = createElement(p.newVNode)
        el.parentNode.replaceChild(newEl, el)
        break
        
      case 'PROPS':
        for (const key in p.props) {
          if (p.props[key] === null) {
            el.removeAttribute(key)
          } else {
            el.setAttribute(key, p.props[key])
          }
        }
        break
        
      case 'ADD':
        el.appendChild(createElement(p.vnode))
        break
        
      case 'REMOVE':
        el.removeChild(el.childNodes[p.index])
        break
    }
  })
}

完整示例

// 初始渲染
const oldVNode = h('div', { id: 'app' }, [
  h('p', { class: 'text' }, ['Hello'])
])

const container = document.getElementById('root')
const el = createElement(oldVNode)
container.appendChild(el)

// 更新
const newVNode = h('div', { id: 'app' }, [
  h('p', { class: 'text-bold' }, ['Hello World'])
])

const patches = diff(oldVNode, newVNode)
patch(el, patches)

关键点

  • VNode:用 JS 对象描述 DOM,包含 tag、props、children 三个属性
  • diff:同层比较,找出节点的增删改,时间复杂度 O(n)
  • patch:根据 diff 结果操作真实 DOM,最小化 DOM 操作
  • 优势:减少直接 DOM 操作,批量更新,跨平台渲染
  • key 的作用:在列表 diff 时标识节点,提高复用效率