渲染器的核心功能:挂载与更新
vnode.children可以是字符串类型的,也可以是数组类型的,如下:
const vnode ={
type: 'div',
children: [
{
type: 'p',
children: 'hello'
}
]
}
可以看到,vnode.children 是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟DOM 树。
为了完成子节点的渲染,我们需要修改 mountElement 函数
,如下面的代码所示:
function mountElement(vnode, container) {
// 创建dom元素
const el = createElement(vnode.type)
console.log(vnode.children)
+ // 处理子元素
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它
vnode.children.forEach(child => {
patch(null, child, el)
});
}
insert(el, container)
}
在上面这段代码中,我们增加了新的判断分支。使用 Array.isArray 函数判断vnode.children 是否是数组,如果是 数组,则循环遍历它,并调 patch 函数挂载数组中的虚拟节点。在挂 载子节点时,需要注意以下两点:
我们知道,HTML 标签有很多属 性,其中有些属性是通用的,例如 id、class 等,而有些属性是特定 元素才有的,例如 form 元素的 action 属性
。实际上,渲染一个元 素的属性比想象中要复杂,不过我们仍然秉承一切从简的原则,先来 看看最基本的属性处理。
为了描述元素的属性,我们需要为虚拟 DOM 定义新的 vnode.props 字段
,如下面的代码所示:
const vnode ={
type: 'div',
// 使用 props 描述一个元素的属性
props: {
id: 'foo'
},
children: [
{
type: 'p',
children: 'hello'
}
]
}
vnode.props 是一个对象,它的键代表元素的属性名称,它的值 代表对应属性的值。这样,我们就可以通过遍历 props 对象的方式, 把这些属性渲染到对应的元素上,如下面的代码所示:
function mountElement(vnode, container) {
// 创建dom元素
const el = createElement(vnode.type)
console.log(vnode.children)
// 处理子元素
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它
vnode.children.forEach(child => {
patch(null, child, el)
});
}
+ // 处理元素属性
insert(el, container)
}
元素的属性分为2种:
如何区分:
key in el 返回值为true则是:DOM Properties,返回false则是HTML Attributes
如何争取的设置到元素上:
el[key] = value
el.setAttribute(key, value)
思路:
1.我们知道元素的属性分为2种,而且这2种的设置方式不一样。因此,我们要特殊处理。
2.处理特殊情况:例如button按钮,它的vnode节点如下:
const button = {
type: 'button',
props: {
disabled: ''
}
}
但是在解析的时候,会出现问题,用户的本意是“不禁用”按钮,但如果渲染器仍然使用 setAttribute 函数设置属性值,则会产生意外的效果,即按钮被禁 用了.
那么应该怎么办呢?一个很自然的思路是,我们可以优先设置 DOM Properties,例如:
el.disabled = false
<form id="form1"></form>
<input form="form1" />
在这段代码中,我们为 标签设置了 form 属性 (HTML Attributes)。它对应的 DOM Properties 是 el.form,但 el.form 是只读的,因此我们只能够通过 setAttribute 函数来设 置它。
function shouldSetAsProps(el, key, value) {
// 特殊处理
if(key === 'form' && el.tagName === 'INPUT') return false
//兜底
return key in el
}
function mountElement(vnode, container) {
const el = createElement(vnode.type)
// 省略 children 的处理
// 处理元素的属性
+ if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
if (shouldSetAsProps(el, key, vaue)) {
if (type === 'boolean' && value === '') {
el[key] = true
} else {
el[key] = false
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
// HTML Attributes
el.setAttribute(key, vnode.props[key])
}
}
}
}
class有三种不同的vnode表示方式,
const vnode = {
type: 'p',
props: {
class: { foor: true, bar: false }
}
}
const vnode = {
type: 'p',
props: {
class: 'foo bar'
}
}
const vnode = {
type: 'p',
props: {
class: [ 'foo bar', { bar: true }]
}
}
因此我们需要一个normalizeClass函数来将不同类型的class值正常化为字符串。
const vnode = {
type: 'p',
props: {
class: normalizeClass([ 'foo bar', { baz: true }])
}
}
处理之后:
const vnode = {
type: 'p',
props: {
class: 'foo bar baz'
}
}
处理之后,设置class的方式也有三种1.className, 2.setAttribute, 3.classList 但是这三种设置的方式不同,性能也是不一样,经过调查发现className的性能是最优的,因此我们使用className设置元素的class。
function mountElement(vnode, container) {
const el = createElement(vnode.type)
// 省略 children 的处理
// 处理元素的属性
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
+ if (key === 'class') {
+ el.className = value || ''
+ } else if (shouldSetAsProps(el, key, vaue)) {
// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
if (type === 'boolean' && value === '') {
el[key] = true
} else {
el[key] = false
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
// HTML Attributes
el.setAttribute(key, vnode.props[key])
}
}
}
}
简化上面的操作,我们可以把处理元素属性的逻辑放在一个函数(patchProps)里面:
function patchProps(el, key, prevValue, nextValue){
// 对class 特殊处理
if (key === 'class') {
el.className = value || ''
} else if (shouldSetAsProps(el, key, vaue)) {
// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
if (type === 'boolean' && value === '') {
el[key] = true
} else {
el[key] = false
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
// HTML Attributes
el.setAttribute(key, vnode.props[key])
}
}
在mountElement函数中调用
function mountElement(vnode, container) {
const el = createElement(vnode.type)
// 省略 children 的处理
// 处理元素的属性
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
+ patchProps(el, key, null, vnode.props[key])
}
}
}
卸载操作的时机: 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
我们不能简单地使用 innerHTML 来完成卸 载操作。正确的卸载方式是,根据 vnode 对象获取与其相关联的真实 DOM 元素,然后使用原生 DOM 操作方法将该 DOM 元素移除。
由于卸载操作是比较常见且基本的操作,所以我们应该将它封装 到 unmount 函数中,以便后续代码可以复用它,如下面的代码所示:
// 卸载
unmount(vnode) {
// 获取 el 的父元素
const parent = vnode.el.parentNode
// 调用 removeChild 移除元素
parent && parent.removeChild(vnode.el)
}
简化render函数
function render(vnode, container) {
if(vnode) {
patch(container._vnode, vnode, container)
} else {
if(container._vnode) {
// 调用 unmount 函数卸载 vnode
+ unmount(container._vnode)
}
}
container._vnode = vnode
}
将卸载操作封装到 unmount 中,还能够带来两点额外的好处: