[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=trueel.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);
    }
  }
};
 
posted @ 2025-05-24 17:57  Zhentiw  阅读(14)  评论(0)    收藏  举报