[HTML] HTML Attibutes与DOM Properties
因为有一些属性,在HTML标签上的名字和DOM Properties API中的名字是不一致的,比如最常用的,标签上的class
属性,DOM Properties
中是className
另外,并不是所有 HTML Attributes 都有与之对应的 DOM Properties,例如:
<div aria-valuenow="75"></div>
aria-*
类的 HTML Attributes
就没有与之对应的 DOM Properties
同样的,反过来,并不是所有的DOM Properties
都有与之对应的HTML Attributes
,比如我们可以用el.textContent
来设置元素的文本内容,但是HTML Attributes
中并没有textContent
这样的属性
还有一些情况,会造成一些误解,比如:
<input value="foo" />
我们一般情况下,是通过el.value
,来获取input标签上的值,当然,我们其实也可以通过el.getAttribute('value')
来获取。但是当用户在页面改了值之后,比如:
<input value="bar" />
通过el.value
能够获取修改之后的值bar
,但是el.getAttribute('value')
获取的还是原来的foo
。
这些关系虽然复杂,但其实我们只需要记住一个核心原则即可:HTML Attributes
的作用是设置之对应与的DOM Properties
的初始值。
虚拟DOM导致元素设置的问题
我们现在有这样的代码:
<button disabled>Button</button>
根据我们上面得出结论,HTML Attributes
的作用是设置之对应与的DOM Properties
的初始值。
浏览器在解析这段 HTML 代码时,发现这个按钮存在一个叫作disabled
的 HTML Attributes
,于是浏览器会将该按钮设置为禁用状态,并将它的 el.disabled
这个 DOM Properties
的值设置为true
。
但是这个情况在虚拟DOM中会出现一些问题,因为如果我们直接处理话,我们会写成下面的样子:
const vnode = {
type: 'button',
props: {
disabled: ''
},
children: 'hello world'
}
这当然没有什么问题,这段虚拟DOM现在在我们的代码中能正确的执行。因为在我们的代码中执行,相当于:
el.setAttribute('disabled', '')
但是,大多数情况下,我们对于这种布尔值的情况,会带上true
或者false
,比如,我现在写成disabled: 'false'
,就表示不要禁用了。
const vnode = {
type: 'button',
props: {
disabled: 'false'
},
children: 'hello world'
}
你会发现,el.setAttribute("disabled",false)
, el.setAttribute("disabled",true)
或者el.setAttribute("disabled",'')
结果是不变的,都相当于true
的效果,但是使用el.disabled=true
,el.disabled=false
,或者el.disabled=''
,结果又是不一样的,这很容易给开发者造成心智负担,所以这里我们简单做一下处理
function mountElement(vnode, container) {
// 创建 DOM 元素
const el = createElement(vnode.type);
if (typeof vnode.children === "string") {
// 如果子节点是字符串,说明它是文本节点
setElementText(el, vnode.children);
}
else if(Array.isArray(vnode.children)){
// 如果子节点是数组,说明它是多个子节点,遍历子节点,并且通过patch挂载它们
vnode.children.forEach(child => {
patch(null,child, el);
});
}
// 如果存在props属性,遍历props属性,并且将其设置到el上
if(vnode.props){
for(const key in vnode.props){
// 使用in操作符判断key是否存在在对应的DOM Properties上
if(key in el){
// 判断该DOM Properties的类型
const type = typeof el[key];
const value = vnode.props[key];
// 如果是布尔类型,并且value是空字符串'',那么就是设置为true
if(type === 'boolean' && value === ''){
el[key] = true;
}
else{
//其他情况按照DOM操作本身的规则处理
el[key] = value;
}
}
else{
// 如果key不存在在DOM Properties上,那么就是setAttribute
el.setAttribute(key, vnode.props[key]);
}
}
}
insert(el, container);
}
当然,现在还是有一些问题,因为有一些DOM Properties是只读的,我们并不能设置,比如
<form id="form1"></form>
<input form="form1" />
DOM Properties
的 el.form
并不能直接设置,是只读的,如果要设置只能通过setAttribute
函数来处理,所以,我们这里可以做一下特殊处理。
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === 'form' && el.tagName === 'INPUT') return false
// 判断属性是否在el上,返回true表示是el上的属性
return key in el
}
这里我们需要再修改一下mountElement
函数,如果属性不能作为 DOM Properties
被设置,应该使用 setAttribute 函数来设置
function mountElement(vnode, container) {
// 创建 DOM 元素
const el = createElement(vnode.type);
if (typeof vnode.children === "string") {
// 如果子节点是字符串,说明它是文本节点
setElementText(el, vnode.children);
}
else if(Array.isArray(vnode.children)){
// 如果子节点是数组,说明它是多个子节点,遍历子节点,并且通过patch挂载它们
vnode.children.forEach(child => {
patch(null,child, el);
});
}
// 如果存在props属性,遍历props属性,并且将其设置到el上
if(vnode.props){
for(const key in vnode.props){
const value = vnode.props[key];
// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
if(shouldSetAsProps(el, key, value)){
// 判断该DOM Properties的类型
const type = typeof el[key];
// 如果是布尔类型,并且value是空字符串'',那么就是设置为true
if(type === 'boolean' && value === ''){
el[key] = true;
}
else{
el[key] = value;
}
}
else{
// 如果key不存在在DOM Properties上,那么就是setAttribute
el.setAttribute(key, vnode.props[key]);
}
}
}
insert(el, container);
}
当然,这里的处理,我们最好也放入到平台无关性的配置中,因此我们可以将属性的设置,提取为一个函数patchProps
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === 'form' && el.tagName === 'INPUT') return false
// 兜底
return key in el
}
const options = {
createElement(tag) {
return document.createElement(tag);
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text;
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor);
},
patchProps(el, key, prevValue, nextValue) {
if(shouldSetAsProps(el, key, nextValue)){
// 判断该DOM Properties的类型
const type = typeof el[key];
// 如果是布尔类型,并且value是空字符串'',那么就是设置为true
if(type === 'boolean' && nextValue === ''){
el[key] = true;
}
else{
el[key] = nextValue;
}
}
else{
// 如果key不存在在DOM Properties上,那么就是setAttribute
el.setAttribute(key, nextValue);
}
}
};