JSX 转 VNode 和 Render 函数

手写 JSX 到虚拟 DOM 的转换和渲染函数

问题

给定一段 JSX,写出对应的 VNode 结构和 render 函数,将 VNode 渲染为真实 DOM。

const element = (
  <div className="container">
    <h1>Hello</h1>
    <p>World</p>
  </div>
)

解答

JSX 转 VNode

JSX 会被 Babel 编译为 createElement 调用,最终生成 VNode 对象:

// JSX 编译后的代码
const element = createElement(
  'div',
  { className: 'container' },
  createElement('h1', null, 'Hello'),
  createElement('p', null, 'World')
)

// createElement 函数
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object' ? child : createTextNode(child)
      )
    }
  }
}

// 创建文本节点
function createTextNode(text) {
  return {
    type: 'TEXT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}

生成的 VNode 结构

const vnode = {
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: {
          children: [
            { type: 'TEXT', props: { nodeValue: 'Hello', children: [] } }
          ]
        }
      },
      {
        type: 'p',
        props: {
          children: [
            { type: 'TEXT', props: { nodeValue: 'World', children: [] } }
          ]
        }
      }
    ]
  }
}

Render 函数

// 将 VNode 渲染为真实 DOM
function render(vnode, container) {
  const dom = createDOM(vnode)
  container.appendChild(dom)
}

// 根据 VNode 创建 DOM 元素
function createDOM(vnode) {
  const { type, props } = vnode

  // 创建 DOM 节点
  const dom = type === 'TEXT'
    ? document.createTextNode('')
    : document.createElement(type)

  // 设置属性
  Object.keys(props)
    .filter(key => key !== 'children')
    .forEach(key => {
      if (key === 'className') {
        dom.className = props[key]
      } else if (key.startsWith('on')) {
        // 事件处理
        const eventType = key.toLowerCase().substring(2)
        dom.addEventListener(eventType, props[key])
      } else {
        dom[key] = props[key]
      }
    })

  // 递归渲染子节点
  props.children.forEach(child => {
    dom.appendChild(createDOM(child))
  })

  return dom
}

// 使用
render(vnode, document.getElementById('root'))

完整示例

<!DOCTYPE html>
<html>
<body>
  <div id="root"></div>
  <script>
    // createElement
    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
          children: children.map(child =>
            typeof child === 'object' ? child : createTextNode(child)
          )
        }
      }
    }

    function createTextNode(text) {
      return {
        type: 'TEXT',
        props: { nodeValue: text, children: [] }
      }
    }

    // render
    function render(vnode, container) {
      container.appendChild(createDOM(vnode))
    }

    function createDOM(vnode) {
      const { type, props } = vnode
      const dom = type === 'TEXT'
        ? document.createTextNode('')
        : document.createElement(type)

      Object.keys(props)
        .filter(key => key !== 'children')
        .forEach(key => {
          if (key === 'className') dom.className = props[key]
          else dom[key] = props[key]
        })

      props.children.forEach(child => dom.appendChild(createDOM(child)))
      return dom
    }

    // 模拟 JSX 编译结果
    const vnode = createElement(
      'div',
      { className: 'container' },
      createElement('h1', null, 'Hello'),
      createElement('p', null, 'World')
    )

    render(vnode, document.getElementById('root'))
  </script>
</body>
</html>

关键点

  • JSX 是语法糖,会被编译为 createElement 函数调用
  • VNode 是普通 JS 对象,包含 typeprops 两个属性
  • 文本节点需要特殊处理,用 TEXT 类型标识
  • render 函数递归遍历 VNode 树,创建对应的 DOM 节点
  • 属性处理需要区分 className、事件和普通属性