3.3 组件的本质
组件就是一组 DOM 元素的封装,本质是「返回虚拟 DOM 的函数或对象」。
一、两种组件形态
| 形态 | 写法 | 取渲染内容的方式 |
|---|---|---|
| 函数式组件 | 函数直接返回 vnode | vnode.tag() |
| 对象式组件 | 对象带 render 方法 | vnode.tag.render() |
// 函数式组件:函数返回虚拟 DOM
const MyComponent = function () {
return {
tag: 'div',
props: { onClick: () => alert('hello') },
children: 'MyComponent click me'
}
}
// 对象式组件:对象 + render 方法
const MyComponent1 = {
render() {
return {
tag: 'div',
props: { onClick: () => alert('hello 1') },
children: 'MyComponent1 click me'
}
}
}
二、用 vnode.tag 的类型区分「元素」与「组件」
const vnode = {
tag: MyComponent1 // 不是字符串,而是组件
}
渲染时据此分流:
typeof vnode.tag | 含义 | 处理 |
|---|---|---|
'string' | 普通标签元素 | mountElement |
'function' | 函数式组件 | mountComponent |
'object' | 对象式组件 | mountComponent |
function renderer(vnode, container) {
if (typeof vnode.tag === 'string') {
mountElement(vnode, container) // vnode 描述的是标签元素
} else if (typeof vnode.tag === 'function') {
mountComponent(vnode, container) // vnode 描述的是组件
} else if (typeof vnode.tag === 'object') {
mountComponent(vnode, container) // vnode 描述的是组件
}
}
三、mountElement 与 mountComponent
// 渲染标签元素:与 3.2 的最简实现一致
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
for (const key in vnode.props) {
if (/^on/.test(key)) {
el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
}
}
if (typeof vnode.children === 'string') {
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => renderer(child, el))
}
container.appendChild(el)
}
// 渲染组件:先拿到子树,再交给渲染器递归处理
function mountComponent(vnode, container) {
let subtree
if (typeof vnode.tag === 'object') {
subtree = vnode.tag.render() // 对象式组件
} else {
subtree = vnode.tag() // 函数式组件
}
renderer(subtree, container) // 递归渲染组件返回的虚拟 DOM
}
组件的返回值还是虚拟 DOM,所以渲染组件只是「取出子树再交给渲染器」,渲染器的核心逻辑完全复用。
相关
- 3.2 初识渲染器:渲染器如何把 vnode 变成真实 DOM
- 3.4 模板的工作原理:手写渲染函数之外,模板是怎么来的