设置 DOM 的属性有两种方法
-
setAttribute
-
直接设置元素的 DOM Properties
但是,无论是使用 setAttribute 函数,还是直接将属性设置在 DOM 对象上,都存在缺陷。
例如有以下模板:
html
<button :disabled="false">Button</button>
编译为 vnode 为:
js
const button = {
type: 'button',
props: {
disabled: false
}
}
用户的本意不是禁用按钮,但如果渲染器仍然使用 setAttribute 函数设置属性值,则会产生意外的效果,即按钮被禁用了:
js
el.setAttribute('disabled', false)
同时,发现使用 setAttribute 函数设置的值总是会被字符串化,用户明明设置的是 boolean 值,通过 setAttribute 函数设置后变成了 string ,所以单独使用 setAttribute 函数设置元素的属性显然是不合理的。
接下来试试直接在 DOM 对象上设置属性值:
js
el.disabled = false
发现按钮没有被禁用了,似乎可行。那再看看下面的模板:
html
<button disabled>Button</button>
上面模板对应的 vnode 是:
js
const button = {
type: 'button',
props: {
disabled: ''
}
}
可以看到,在模板经过编译后得到 vnode 对象中,props.disabled 的值是一个空字符串。如果直接用它设置元素的 DOM Properties,那么相当于:
js
el.disabled = ''
由于 el.disabled 是布尔类型的值,所以当我们将它设置为空字符串时,浏览器会将它的值矫正为布尔类型的值,即 false。所以上面这句代码的执行结果等价于:
js
el.disabled = false
这违背了用户的本意,因为用户希望禁用按钮,而 el.disabled = false
则是不禁用的意思。
综上,无论使用 setAttribute 函数,还是直接设置元素的 DOM Properties ,都存在缺陷。要彻底解决这个问题,只能做特殊处理,即优先设置元素的 DOM Properties,但当值为空字符串时,要手动将值矫正为 true。只有这样,才能保证代码的行为符合预期。
Vue3 源码中,使用 shouldSetAsProp
函数判断属性是否应该作为 DOM Properties 被设置。如果返回 true ,则代表应该作为 DOM Properties 被设置,否则应该使用 setAttribute 函数来设置。
ts
function shouldSetAsProp(
el: Element,
key: string,
value: unknown,
isSVG: boolean
) {
if (isSVG) {
// most keys must be set as attribute on svg elements to work
// ...except innerHTML & textContent
if (key === 'innerHTML' || key === 'textContent') {
return true
}
// or native onclick with function values
if (key in el && nativeOnRE.test(key) && isFunction(value)) {
return true
}
return false
}
// these are enumerated attrs, however their corresponding DOM properties
// are actually booleans - this leads to setting it with a string "false"
// value leading it to be coerced to `true`, so we need to always treat
// them as attributes.
// Note that `contentEditable` doesn't have this problem: its DOM
// property is also enumerated string values.
if (key === 'spellcheck' || key === 'draggable' || key === 'translate') {
return false
}
// #1787, #2840 form property on form elements is readonly and must be set as
// attribute.
if (key === 'form') {
return false
}
// #1526 <input list> must be set as attribute
if (key === 'list' && el.tagName === 'INPUT') {
return false
}
// #2766 <textarea type> must be set as attribute
if (key === 'type' && el.tagName === 'TEXTAREA') {
return false
}
// native onclick with string value, must be set as attribute
if (nativeOnRE.test(key) && isString(value)) {
return false
}
return key in el
}
👆 上面代码摘自 Vue.js 3.2.45 版本
如上面代码所示,对于 svg 元素:
-
innerHTML 、textContent 属性可以直接作为 DOM Properties 设置。
-
如果属性在 DOM Properties 中(
key in el
),原生事件(onclick
、oninput
等)并且是 function 类型,则可以作为 DOM Properties 设置。 -
其他情况都不能作为 DOM Properties 直接设置。
就如代码注释中所说:
- most keys must be set as attribute on svg elements to work...except innerHTML & textContent(对于 svg 元素,大部分属性需要使用 setAttribute 函数设置属性,除了 innerHTML 、textContent)
对于非 svg 元素:
-
spellcheck 、draggable、translate 是枚举类型的属性 。使用 setAttribute 函数来设置,因此返回 false
-
form 属性为只读属性,必须使用 setAttribute 函数
-
list 属性、tagName 为
INPUT
,必须使用 setAttribute 函数 -
type 属性,tagName 为
TEXTAREA
,必须使用 setAttribute 函数 -
原生 dom 事件(
onclick
、oninput
) 并且是字符串类型,使用 setAttribute 函数 -
key in el
,兜底处理,dom (el
)对象上有key
属性,则作为 DOM Properties 直接设置。
在 Vue3 的源码中,使用 patchProp
函数来处理元素的属性更新和设置。
ts
function patchProp(
el,
key,
prevValue,
nextValue
) {
if (key === 'class') {
// class 的处理,非本文重点,略过讲解
} else if (key === 'style') {
// style 的处理,非本文重点,略过讲解
} else if (isOn(key)) {
// 事件的处理,非本文重点,略过讲解
} else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
)
} else {
patchAttr(el, key, nextValue, isSVG, parentComponent)
}
}
👆 上面代码摘自 Vue.js 3.2.45 版本
上面的代码中
-
patchDOMProp
函数的核心逻辑为el[key] = value
-
patchAttr
函数的核心逻辑为el.setAttribute(key, value)
总结
回到一开始提的问题:Vue.js 3 是如何正确设置元素属性的?
Vue3 设置元素属性时,优先使用直接设置 DOM Properties ,对于某些情况做特殊判断处理,例如:当属性为只读时使用 setAttribute 函数、当值为空字符串时,手动将值矫正为 true 。
Vue3 通过结合直接设置 DOM Properties 和 setAttribute 函数的方式来正确地设置元素属性的。
Vue3 也结合了开源社区的反馈,不断地完善设置元素属性的代码逻辑。