前言

本篇文章旨在学习Vue对于相关属性或特性的处理,同时学习HTML特性和DOM属性的区别和联系,实际上涉及到以下内容:

  • HTML特性和DOM属性之间的联系与区别
  • Vue的数据对象中涉及的HTML特性、DOM属性、组件prop、特殊特性
  • Vue中对于4种属性或特性处理所涉及的相关逻辑

HTML特性和DOM属性

实际上对于HTML特性和DOM属性,之前对于其的理解是相同的东西,实际上它们是不同的概念。

Attribute表示特性,Property表示属性(实际上这两个单词都可以翻译成属性)。在《JavaScript高级程序设计》一书中,就存在特性和属性的不同表述。在该书中存在涉及到特性和属性区别有一些描述:

  • 所有HTML元素都由HTMLElement或者更具体的子类型表示的,每个元素都有一个或多个特性

  • 操作特性的DOM方法主要有三个:getAttribute()、setAttribute()和removeAttribute()。这三个方法可以针对任何特性使用,包括哪些以HTMLElement类型属性定义的特性

  • HTMLElement类型直接继承自Element并添加了一些属性。添加的这些属性分别对应着每个HTML元素中都存在的标准特性。

从上面几个的描述中,目前可以得到关于特性和属性的概念:

  • 特性是指在HTML标签上定义的
  • 属性是指DOM对象的,可以通过这些属性来修改对应HTML上的特性值
  • 一个HTML元素的特性和DOM属性是存在交集的,交集的这部分可能名称相同也可能不同,正是这种机制才有了所谓的DOM修改,因为DOM通过操作对应的属性来修改指定的特性值,而修改后的特性值会立即反映在页面上

简单的来说,特性是HTML区别于与之对应DOM对象的属性的表述,不是每一个特性都有与之对应的属性,反之亦然。

实例:class特性、style特性、自定义data-key特性

看看上面几个特性在DOM对象中与之对应的属性,应该相关的DOM操作。

<div
  class="attr"
  style="text-align: center"
  data-key="key"
>
</div>
var app = document.getElementById('app');
var dataKey = app.getAttribute('data-key');
console.log(dataKey); // "key"
// 但是自定义特性没有与之对应的属性,即app.dataKey是不存在的
console.log(app.dataKey);

// 获取class特性
var class = app.getAttribute('class');
// 与之对应的DOM属性
var classOfAttr = app.className;

// 获取style特性,获取到的值是字符串
var style = app.getAttribute('style');
// 与之对应DOM属性,获取到的结果是对象
var styleOfAttr = app.style;

Vue中涉及的属性和特性

实际上主要是渲染函数&JSX提及到的数据对象,其中涉及到的特性和属性有:

{
  // 普通的HTML特性
  attrs: {},
  // DOM属性
  domProps: {},
  // 组件prop
  props: {}
  // 特殊特性
  key: "",
  ref: "",
  slot: "",
  scopedSlots: {
    default: () => {}
  }
  ...
}

由此提出以下系列问题:

  • domProps中属性放在attrs对象中
  • attrs中属性放在domProps中
  • 组件props属性放在attrs或domProps中
  • attr或domProps放在组件Prop中
  • 组件Prop放在最外层
  • 特殊特性放在attrs、domProps或组件props

实际上上面的问题都是Vue对于这种判别逻辑的处理,由于HTML特性和DOM属性会存在相对应的,所以这里就分为两类情况:

  • DOM属性和HTML特性相对应的特性,例如:title
  • 私有的,DOM属性例如innerText,HTML特性例如自定义属性
存在编译步骤

存在template模板内容会调用编译过程的,通过这个过程可以清楚看到Vue对所有属性和特性的分配处理,借此来对比使用JSX的数据对象应该的书写方式。

实例:

<el-button
	class="attr"
  key="app"
  ref="appRef"
  :data="value"
  :text-content.prop="value"
  id="btn"
  type="primary"
 >
 </el-button>

实际上要看还是Vue的编译器部分,不过这里主要注重标签的特性解析,实际上就是主要是parseHTML的start部分,主要如下:

// template就是要解析的文本,实际上就是template中的内容
parseHTML(template, {
  start: function start(tag, attrs,unary) {
    // 主要关注这个函数中解析逻辑
   	processElement(element, options);
  }
});

processElement的内容如下:

function processElement (element, options) {
  // 解析key
  processKey(element);
  // 当前标签是文本
  element.plain = !element.key && !element.attrsList.length;
  // 解析ref
  processRef(element);
  // 解析slot
  processSlot(element);
  // 解析is属性和inline-template
  processComponent(element);
  // 解析attrs
  processAttrs(element);
}

首先看看key的获取,即processKey函数主要逻辑:

function processKey(el) {
  /**
  	getBindingAttr就是查找当前标签解析出来的所有特性(过程变量,会存在attrMap和attrList)是否包含
  	:key、v-bind:key、key这三种特性,实际上就是判断是否是动态prop,不是就判断是否是静态prop,然后获取对应值并删除attrList中对应的属性
  */
  var exp = getBindingAttr(el, 'key');
  el.key = exp;
}

ref的处理也是如此,slot的处理就稍微复杂些,需要判断slot还是slot-scope然后获取相关name属性或设置作用域(这边不是主要专注点本期就不展开)。

最主要就是processAttrs函数的处理逻辑了,HTML属性、domProps和自定义组件prop都是在这里处理(前提是存在解析器步骤)。
在这里插入图片描述

上图是processAttrs的主要的解析步骤,本次需要关注的是v-bind或:的解析逻辑和普通属性的解析逻辑。

普通属性的都是存放在attrs对象内,即调用addAttr函数。

v-bind或:开头的prop的处理相对想说就比较复杂,首先通过Vue.js的官方文档,可知v-bind支持3种修饰符:

  • .prop:被用于绑定DOM属性
  • .camel:将kebaba-case特性名转换成camelCase
  • .sync:语法糖,会绑定事件处理v-on:update的处理

这里需要注意的是.prop修饰符,在实际生产中不常用,该属性支持将DOM属性通过特性方式来修改。实际官方文档就给出了一个例子:

<template>
	<div :text-content.prop="text"></div>
	<div :inner-html.prop="text"></div>
</template>

<script>
  export default {
    data() {
      return {
        text: '测试'
      };
    }
  }
</script>

上面的例子HTML特性 text-content和inner-html不能看成自定义特性,可以看成DOM属性textContent 和 innerHTML,而实际效果跟DOM插入是相同:

两个div都存在内容,内容文本是text的值,即测试

而对于这种Vue的处理是将其看成prop,言外之意就是普通的prop实际上Vue会将其存储到attrs对象中

从之前上图的逻辑中也可以看出,只有满足:isProp || 特殊属性,才会调用addProp将属性保存到props对象。

isProp就是标识存在.prop修饰符的情况,而特殊属性这里简单提及下,下一篇的文章会具体该部分的梳理

实际上特殊属性的校验是调用下面函数:

var acceptValue = makeMap('input,textarea,option,select,progress');
var mustUseProp = function (tag, type, attr) {
  return (
    (attr === 'value' && acceptValue(tag)) && type !== 'button' ||
    (attr === 'selected' && tag === 'option') ||
    (attr === 'checked' && tag === 'input') ||
    (attr === 'muted' && tag === 'video')
  )
};

从上面可以看出特殊属性具体是:

存在value属性的元素、radio、checkbox、select、option、video

基本上都是存在复杂处理的表单元素,之所以这些需要当成动态prop来处理,是因为Vue对这些元素存在着特殊处理,实际上主要是v-model语法糖的实现(video排除在外,video muted这个是另外的问题,代码中有特殊注释),下一篇文章会具体看看v-model语法糖的实现(包含了这些特殊的表单元素)。

至此可以得到结论:

存在.prop修饰和指定标签的特殊属性,会被保存到Vue的props对象中。处了前面情况,其他属性都在Vue的attrs对象中,包含不存在.prop修饰符的动态prop

上面是解析AST过程中涉及到属性的处理逻辑,实际上在构建render还有生成code的处理逻辑,数据对象的生成在这里会有具体的逻辑,主要是:

generate -> genElement -> genData$2

genData$2就是构建大部分数据对象的处理函数,具体代码如下:

function genData$2 (el, state) {
  var data = '{';
  // 指令
  var dirs = genDirectives(el, state);
  if (dirs) { data += dirs + ','; }

  // key
  if (el.key) {
    data += "key:" + (el.key) + ",";
  }
  // ref
  if (el.ref) {
    data += "ref:" + (el.ref) + ",";
  }
  // refInFor
  if (el.refInFor) {
    data += "refInFor:true,";
  }
  // pre
  if (el.pre) {
    data += "pre:true,";
  }
  // component动态组件
  if (el.component) {
    data += "tag:\"" + (el.tag) + "\",";
  }
  // module data generation functions
  for (var i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el);
  }
  // attributes
  if (el.attrs) {
    data += "attrs:{" + (genProps(el.attrs)) + "},";
  }
  // DOM props,注意这里,所有props都是domProps
  if (el.props) {
    data += "domProps:{" + (genProps(el.props)) + "},";
  }
  // event handlers
  if (el.events) {
    data += (genHandlers(el.events, false, state.warn)) + ",";
  }
  if (el.nativeEvents) {
    data += (genHandlers(el.nativeEvents, true, state.warn)) + ",";
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += "slot:" + (el.slotTarget) + ",";
  }
  // scoped slots
  if (el.scopedSlots) {
    data += (genScopedSlots(el.scopedSlots, state)) + ",";
  }
  return data
}

通过这边的处理,再对比通过JSX书写的数据对象:

{
  // 普通的HTML特性
  attrs: {},
  // DOM属性
  domProps: {},
  // 组件prop
  props: {}
  // 特殊特性
  key: "",
  ref: "",
  slot: "",
  scopedSlots: {
    default: () => {}
  }
	...
}

二者方式最主要的区别是JSX少了解析template阶段,手动调用$createElement来实现组件相关逻辑。

而在上面的解析阶段,提到了attrs和props对象,需要注意的是这两个对象的生命周期结束于render函数构建出来之后,即render函数构建完成后这两个对象就被丢弃了。

<!-- template形式 -->
<el-button
	class="attr"
  key="app"
  ref="appRef"
  :text-content.prop="value"
  id="btn"
  type="primary"
>
</el-button>

<!-- jsx -->
<script>
  export default {
    data() {
      return {
        value: [1, 2]
      };
    },
    render() {
    	const h = this.$createElement;
      return h('el-button',{
      	key: 'app',
        ref: 'appRef',
        'class': {
           attr: true
         },
         attrs: {
           id: 'btn'
         },
          domProps: {
            textContent: this.value
          },
          props: {
            type: 'primary'
          }
        });
      }
  }
</script>

效果:
在这里插入图片描述
实际上面两种形式在生成VNode时是存在不同的,实际上是data属性值不同。具体如下:

在这里插入图片描述
左图是template形式构建的VNode的data属性,有图是render形式构建的VNode的data属性。实际上还是存在差别的:

  • 综合上面的种种,template形式下编译器存在特殊处理:

    1、class内部是staticClass,即静态class,反而言之,也存在动态class,即Vue支持写法

    <div class="attr" :class="value"></div>
    

    2、props如果子组件存在对应的prop,就不会被解析到attrs对象中,会在initState函数中对props有相关处理

  • JSX形式的,VNode的data属性传递的数据对象不会变动,会在此基础上添加相关属性

到此,可以来看看下面几个问题:

domProps与attrs

由于DOM属性存在dom特有的,也存在与HTML特性相对应的,实际上:

  • 如果将HTML特性放在domProps中也会生效
  • dom属性只能放在domProps才能生效
组件props属性与attrs或domProps

render函数构建之后,就会执行render函数生成VNode,无论是template形式还是手动构建render函数,都会在背后调用$createElement来创建节点,而这个过程会涉及到一个非常重要的函数:extractPropsFromVNodeData。

function extractPropsFromVNodeData (
  data,
  Ctor,
  tag
) {
  // 组件的props选项
  var propOptions = Ctor.options.props;
  if (isUndef(propOptions)) {
    return
  }
  var res = {};
  // attrs、props
  var attrs = data.attrs;
  var props = data.props;
  if (isDef(attrs) || isDef(props)) {
    // 遍历props中属性
    for (var key in propOptions) {
      // 其他逻辑
      // 这里会检查attrs对象的属性,这里的逻辑意味着props中的属性可以写到attrs中
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false);
    }
  }
  return res
}

即render函数传递数据对象时,props中的内容可以写到attrs对象中。

特殊特性放在attrs、domProps或组件props

特殊特性key、ref、slot、scopedSlots是Vue的组件属性,只能放在最外层,不能放在props、attrs、domProps。

总结

template形式下总结:

  • 非.prop修饰符的prop只会保存在attrs中
  • .prop修饰符的prop和表单元素等特殊属性的会被保存props中,构建出来对应domProps

JSX形式下总结:

  • DOM属性只能数据对象domProps属性中,attrs和domProps相对应的可以写在这两个属性中
  • prop属性可以写在props和attrs属性中,template形式.prop修饰的prop需要写在domProps中
  • 特殊特性只能写在最外层,不能写在attrs、props、domProps中

下一篇文章会具体看v-model语法糖的实现逻辑,其中就会涉及到特殊属性例如表单元素的value等。

Logo

前往低代码交流专区

更多推荐