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 对象,包含
type和props两个属性 - 文本节点需要特殊处理,用
TEXT类型标识 - render 函数递归遍历 VNode 树,创建对应的 DOM 节点
- 属性处理需要区分
className、事件和普通属性
目录