对于普通的HTML文件来说,浏览器解析HTML代码后,会自动分析HTML Attribute并设置合适的DOM Properties。但是用户编写在 Vue.js的单文件组件中的模板不会被浏览器解析,也就是说,本来要浏览器完成的工作,现在需要框架来完成。
以禁用按钮为例,如下:
<button disabled>Buttons</button>
这样按钮是禁用的,并且el.disabled也设置为true
如果同样的代码出现在Vue.js的模板中,情况则不同,首先会编译成如下vnode,等价于:
const button = {
type: 'button',
props: {
disabled: ''
}
}
如果在渲染器中调用setAttribute函数设置属性,相当于:
el.setAttribute('disabled','')
将disabled的属性设置为空字符串,这样做没问题能实现效果,但是考虑如下模板:
<button :disabled="false">Buttons</button>
对应的vnode
const button = {
type: 'button',
props: {
disabled: false
}
}
用户的本意是不禁用,但是用过setAttribute设置后,按钮还是被禁用了,
因为使用setAttribute设置的值总是会被字符串化,所以就相当于
el.setAttribute('disabled','false')
而el.disabled属性值是布尔类型的,且并不关心具体的HTML Attribute的值是什么,只要disabled属性存在,按钮就会被禁用。
那如果优先设置DOM Properties呢,再看这个例子:
<button disabled>Buttons</button>
其对应的vnode是:
const button = {
type: 'button',
props: {
disabled: ''
}
}
这里props.disabled的值是一个空字符串,用空字符串来设置DOM Properties,相当于
el.disabled = ''
但el.disabled是布尔类型,浏览器会把其空字符串矫正为false,这就违背了用户的本意,希望的是禁用按钮,但是el.disabled = false是不禁用按钮。
那么就需要特殊处理,即优先设置元素的DOM Properties,当其为空字符串时,手动将值矫正为true,实现如下
function mountElement(vnode,container){
const el = createElement(vnode,type)
// 省略children的处理
if(vnode.props){
for(const key in vnode.props){
// 用in操作符判断key是否存在对应的DOM Properties
if(key in el){
const type = typeof el[key]
const value = vnode.props[key]
// 如果是布尔类型,并且value是空字符串,则将值矫正为true
if(type === 'boolean' && value === ''){
el[key] = true
}else{
el[key] = value
}
}else{
// 如果要设置的属性没有对应的DOM Properties,则使用setAttribute函数设置属性
el.setAttribute(key, vnode.pops[key])
}
}
}
insert(el,container)
}
但是上面的实现仍然存在问题,比如有些DOM Properties是只读的,如下面代码:
<form id="form1"></form>
<input form="form1" />
上面代码中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,value)){
const type = typeof el[key]
if(type === 'boolean' && value === ''){
el[key] = true
}else{
el[key] = value
}
}else{
el.setAttribute(key, value)
}
}
}
insert(el,container)
}
实际上不仅仅是标签,所有表单元素都具有form属性,都应该作为HTML Attributes设置
当然还有其他类似这种需要特殊处理的情况,在此就不一一列出来,只要掌握处理问题的思路即可。
最后还要把属性的设置变成与平台无关,如下面代码所示:
const renderer = createRenderer({
// 用于创建元素
createElement(tag){
return document.createElement(tag)
},
// 用于设置元素的文本节点
setElementText(el,text){
el.textContent = text
},
// 用于在给定的parent下添加指定元素
insert(el,parent,anchor = null){
parent.insertBefore(el,anchor)
},
// 将属性设置相关操作封装到patchProps函数中,并作为渲染器选项传递
patchPros(el,key,preValue,nextValue){
if(shouldSetAsProps(el,key,nextValue)){
const type = typeof el[key]
if(type === 'boolean' && nextValue=== ''){
el[key] = true
}else{
el[key] = nextValue
}
}else{
el.setAttribute(key, nextValue)
}
}
})
而在mountElement函数中,只需要调用patchProps函数,并传递相应参数即可
function mountElement(vnode,container){
const el = createElement(vnode,type)
// 省略children的处理
if(vnode.props){
for(const key in vnode.props){
//调用patchProps函数即可
patchProps(el,key,null,vnode.props[key])
}
}
insert(el,container)
}