虚拟DOM转真实DOM

实现 render 函数将虚拟DOM转换为真实DOM

问题

实现一个 render 函数,将虚拟 DOM 对象转换为真实 DOM 节点。

解答

虚拟 DOM 结构

// 虚拟 DOM 的数据结构
const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    class: 'container'
  },
  children: [
    {
      tag: 'h1',
      props: { style: 'color: red' },
      children: ['Hello']
    },
    {
      tag: 'p',
      props: null,
      children: ['World']
    }
  ]
}

实现 render 函数

function render(vnode) {
  // 处理文本节点
  if (typeof vnode === 'string' || typeof vnode === 'number') {
    return document.createTextNode(vnode)
  }

  const { tag, props, children } = vnode

  // 创建元素
  const el = document.createElement(tag)

  // 设置属性
  if (props) {
    for (const key in props) {
      const value = props[key]

      if (key.startsWith('on')) {
        // 事件绑定:onClick -> click
        const eventName = key.slice(2).toLowerCase()
        el.addEventListener(eventName, value)
      } else if (key === 'style' && typeof value === 'object') {
        // style 对象处理
        Object.assign(el.style, value)
      } else {
        // 普通属性
        el.setAttribute(key, value)
      }
    }
  }

  // 递归处理子节点
  if (children) {
    children.forEach(child => {
      el.appendChild(render(child))
    })
  }

  return el
}

完整示例

const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    class: 'container'
  },
  children: [
    {
      tag: 'h1',
      props: {
        style: { color: 'red', fontSize: '24px' }
      },
      children: ['Hello Virtual DOM']
    },
    {
      tag: 'button',
      props: {
        onClick: () => alert('clicked!')
      },
      children: ['Click me']
    },
    {
      tag: 'ul',
      props: null,
      children: [
        { tag: 'li', props: null, children: ['Item 1'] },
        { tag: 'li', props: null, children: ['Item 2'] },
        { tag: 'li', props: null, children: ['Item 3'] }
      ]
    }
  ]
}

// 渲染到页面
const realDOM = render(vnode)
document.body.appendChild(realDOM)

支持 Fragment 和空节点

function render(vnode) {
  // 空节点
  if (vnode == null || typeof vnode === 'boolean') {
    return document.createTextNode('')
  }

  // 文本节点
  if (typeof vnode === 'string' || typeof vnode === 'number') {
    return document.createTextNode(vnode)
  }

  // 数组(Fragment)
  if (Array.isArray(vnode)) {
    const fragment = document.createDocumentFragment()
    vnode.forEach(child => {
      fragment.appendChild(render(child))
    })
    return fragment
  }

  const { tag, props, children } = vnode
  const el = document.createElement(tag)

  // 设置属性
  if (props) {
    for (const key in props) {
      setProp(el, key, props[key])
    }
  }

  // 递归子节点
  if (children) {
    children.forEach(child => {
      el.appendChild(render(child))
    })
  }

  return el
}

function setProp(el, key, value) {
  if (key.startsWith('on')) {
    const eventName = key.slice(2).toLowerCase()
    el.addEventListener(eventName, value)
  } else if (key === 'style') {
    if (typeof value === 'object') {
      Object.assign(el.style, value)
    } else {
      el.setAttribute('style', value)
    }
  } else if (key === 'className') {
    el.setAttribute('class', value)
  } else {
    el.setAttribute(key, value)
  }
}

关键点

  • 虚拟 DOM 是描述 DOM 结构的 JS 对象,包含 tagpropschildren
  • 文本节点用 createTextNode,元素节点用 createElement
  • 属性需要区分处理:事件(on 开头)、style(对象或字符串)、普通属性
  • 子节点递归调用 render 处理
  • 可扩展支持 Fragment、空节点、组件等场景