前面提到,当vnode.children的值是字符串类型时,会把它设置为元素的文本内容。但是一个元素还可以包含其他元素子节点,并且子节点可以是很多个。为了描述元素的子节点,我们需要将vnode.children定义为数组:
const vnode = {
type: 'div',
children: [
{
type: 'p',
children: 'hello'
}
]
}
可以看到vnode.children是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟DOM树。
为了完成子节点的渲染,需要修改mountElement函数,如下面代码所示:
function mountElement(vnode, container){
const el = createElement(vnode,type)
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)
}
如果是数组,则循环遍历它,并调patch函数挂载数组中的虚拟节点,在挂载子节点时,需要注意以下两点:
- 传递给patch函数的第一个参数是null。这时patch函数执行时,就会递归地调用mountElement函数完成挂载。
- 传递给patch函数的第三个参数是挂载点。由于正在挂载的子元素是div标签的子节点,所以要把刚创建的div元素作为挂载点。
再来看vnode描述一个标签的属性,以及如何渲染这些属性。HTML标签有很多属性,有些属性是特定元素才有的。实际上,渲染一个元素的属性比想象中要复杂,不过我们先来看看最基本的属性处理。
为了描述元素的属性,为虚拟DOM定义新的vnode.props字段,如下面代码所示:
const vnode = {
type: 'div',
// 使用props描述一个元素的属性
props:{
id: 'foo'
},
children: {
{
type: 'p',
children: 'hello'
}
}
}
vnode.props是一个对象,它的键代表元素的属性名称,值代表对应属性的值。这样,就可以通过遍历props对象的方式,把这些属性渲染到对应的元素上,如下面代码所示:
function mountElement(vnode, container){
const el = createElement(vnode,type)
// 省略children的处理
// 如果 vnode.props存在才处理它
if (vnode.props){
// 遍历 vnode.props
for(const key in vnode.props){
// 调用 setAttribute 将属性设置到元素上
el.setAttribute(key, vnode.props[key])
}
}
insert(el,container)
}
知识扩展
setAttribute() 方法创建或改变某个新属性。
element.setAttribute(attributename,attributevalue)
当然也可以通过DOM对象直接设置,如下:
el[key] = vnode.props[key]
其实无论是使用setAttribute函数,还是直接操作DOM对象,都存在缺陷。不过在讨论具体有哪些缺陷前,先要搞清楚HTML Attributes 和 DOM Properties
HTML Attributes 和 DOM Properties
HTML Attributes值的是定义在HTML标签上的属性,例如
<input id="my-input" type="text" value="foo" />
HTML Attribute值的是 id=“my-input” ,type="text"和value=“foo”
当浏览器解析这段HTML代码后,会创建一个与之相符的DOM元素对象,所以可以通过JavaScript代码来读取改DOM对象
const el = document.querySelector('#my-input')
这个DOM包含很多属性,这些属性就是DOM Properties
很多HTML Attributes在DOM对象上有与之同名的DOM Properties,例如id="my-input"对应el.id,type="text"对应el.type,value="foo"对应el.value等,但也不是总是一模一样的,例如
<div class="foo"></div>
class="foo"对应的DOM Properties则是el.className。
此外也不是所有HTML Attributes都有对应的DOM Properties,例如:
<div aria-valuenow="75"></div>
aria.* 类的HTML Attributes就没有与之对应的DOM Properties,反之亦然
同时把这种HTML Attributres和DOM Properties具有相同名称的属性看作是直接映射,但并不是所有HTML Attributes与DOM Propeties之间都是直接映射的关系,例如:
<input value="foo" />
这里如果用户没有修改文本框的内容,那么通过el.value读取对应的DOM Properties的值是‘foo’,如果用户修改了文本框的值,那么el.value的值就是当前文本框的值,例如将文本框的内容修改为’bar’:
console.log(el.value) // bar
但是看下面的代码
console.log(el.getAttribute('value')) // 仍然是foo
可以发现,用户对文本框内容的修改不会影响el.getAttribute(‘value’)的返回值,实际上HTML Attributes的作用是设置与之对应的DOM Properties初始值,一旦改变,那么DOM Properties始终存储的是当前值,而通过getAttribute函数得到的仍然是初始值。
但仍可以通过el.defaultValue来访问初始值
这说明HTML Attributes可能关联多个DOM Properties。
总而言之,只要记住HTML Attributes的作用是设置与之对应的DOM Properties初始值