组件是可复用的Vue实例,拥有属于自己的数据、模板、脚本和样式,可避免繁重的重复性开发。由于组件都是独立的,因此其内部代码不会影响其它组件,但可以包含其它组件,并且相互之间还能通信。

一、注册

  在使用组件之前,需要先将其注册,Vue提供了两种注册方式:全局注册和局部注册。

1)全局注册

  通过Vue.component()方法可注册全局的组件,它能接收两个参数,第一个是组件名称,第二个既可以是扩展过的构造器(即Vue.extend()的返回值),也可以是选项对象(会自动调用Vue.extend()),如下所示。

Vue.component("btn-custom", Vue.extend({ }));    //扩展过的构造器
Vue.component("btn-custom", { });                //选项对象

  组件的选项对象也会包含data、methods、计算属性和生命周期钩子等成员,但不包含挂载目标el选项,并且data选项也不再是一个对象而是一个函数,因为只有这样才能让每个实例维护各自的数据对象,互不影响。注意,只有在组件注册之后才能将其应用于其它Vue根实例的模板中,如下所示。

<div id="container">
  <btn-custom></btn-custom>
</div>
<script>
  Vue.component("btn-custom", {
    data: function() {
      return {
        txt: "提交"
      };
    },
    template: '<button>{{txt}}</button>'
  });
  var vm = new Vue({ 
    el: "#container"
  });
</script>

  渲染出的DOM结构如下所示。

<div id="container">
  <button>提交</button>
</div>

  组件的命名方式除了上面的连字符分隔式之外,还有另一种大驼峰式(例如BtnCustom)。当把组件引用至字符串模板中时,两种命名方式都是有效的;而当把组件直接应用到DOM模板中时(如下所示),就不能用大驼峰命名,因为标签会被自动转换成小写(即<btncustom>),于是就找不到这个组件的定义,进而抛出错误。

<div id="container">
  <BtnCustom></BtnCustom>
</div>

2)局部注册

  Vue的局部注册需要分两步,首先通过创建选项对象的方式来定义组件,如下所示。

var BtnCustom = {
  data: function() {
    return {
      txt: "提交"
    };
  },
  template: "<button>{{txt}}</button>"
};

  然后在Vue根实例的components选项中注册要使用的组件(如下所示),其中属性名就是模板中要使用的自定义元素名,属性值就是组件。

var vm = new Vue({
  el: "#container",
  components: {
    "btn-custom": BtnCustom
  }
});

  注意,当构建一个组件时,其模板中必须包含一个根元素,之前的示例都只有一个元素。如果有多个元素,那么就得像下面这样用一个元素(<div>)包裹其它元素(<span>和<button>)。

var BtnCustom = {
  template: `<div>
      <span>按钮</span>
      <button>提交</button>
    </div>`
};

二、数据传递

  组件的props选项能接收从外部(可以是父组件)传递进来的数据,其值是一个由HTML特性组成的数组或对象,如下所示。

<btn-custom in-html="提交"></btn-custom>
<script>
  Vue.component("btn-custom", {
    props: ["inHtml"],
    template: '<button>{{inHtml}}</button>'
  });
</script>

  由于HTML特性的名称大小写不敏感,因此浏览器会将所有大写字母自动转换成小写。这意味着如果在组件内为props选项添加驼峰式的特性(例如inHtml),那么在DOM模板中需要声明成等价的连字符分隔式的特性(例如in-html),否则在组件内将读取不到该特性。有一点要注意,在字符串模板中使用特性,两种命名方式都是有效的。

1)动态传值

  特性值的类型除了上文的字符串之外,还可以通过v-bind指令动态的将任意类型传递给组件的props选项,例如传入一个数字,如下所示。

<btn-custom :digit="1"></btn-custom>
<script>
  Vue.component("btn-custom", {
    props: ["digit"],
    created: function() {
      typeof this.digit;         //"number"
    }
  });
</script>

  在created钩子中调用typeof运算符计算this.digit,得到的值为“number”,说明数字传递成功。

  如果要传递对象的所有属性,那么不必一个一个声明,只需要不定义v-bind的参数即可,如下所示,两个btn-custom组件是等价的。

<div id="container">
  <btn-custom v-bind="obj"></btn-custom>
  <!-- 相当于 -->
  <btn-custom :id="obj.id" :name="obj.name"></btn-custom>
</div>
<script>
  Vue.component("btn-custom", {
    props: ["id"],
    template: '<button>{{id}}</button>'
  });
  var vm = new Vue({
    el: "#container",
    data: {
      obj: { id: 1, name: "strick" }
    }
  });
</script>

  注意,在props选项中声明的是id或name,而不是obj。

2)数据流

  在Vue中,组件之间的数据是自顶向下单向流动的(即单向数据流),父组件通过props将数据传递给子组件。一旦父组件的数据有所更新,那么子组件也会自动更新,如果在子组件中修改接收的props(例如下面的digit特性),那么Vue会抛出错误警告,避免改变父组件的状态。

Vue.component("btn-custom", {
  props: ["digit"],
  created: function() {
    this.digit = 2;
  }
});

  很多需要改变props的情况,其实都能以另一种更合理的方式解决,例如将其保存到组件的data属性中或定义成一个计算属性等。

3)校验特性

  组件的props能以对象的形式指定值类型,其键是接收的特性名称,值是类型构造函数。这样既有助于阅读,也可以避免传递无效的值。在下面的示例中,指定了digit必须是数字,而number既可以是数字也可以是字符串。

Vue.component("btn-custom", {
  props: {
    digit: Number,
    number: [Number, String]
  }
});

  除了Number和String之外,内置的构造函数还有Boolean、Array、Object、Date、Function和Symbol。不仅如此,还可以自定义构造函数,通过instanceof运算符来检查。在下面的示例中,验证man特性是否是通过new Person()创建的。

Vue.component("btn-custom", {
  props: {
    man: People
  }
});
function People(name) {
  this.name = name;
}

  除了基础的类型检查之外,组件还允许自定义验证函数、添加必填标记和附带默认值,如下所示。

Vue.component("btn-custom", {
  props: {
    digit: {        
      type: Number,
      required: true    //必填
    },
    number: {        
      type: Number,
      default: 100      //数字默认值
    },
    people: {        
      type: Object,
      default: function() {           //对象默认值
        return { name: "strick" };
      }
    },
    name: {            
      validator: function(value) {    //验证函数
        return value.length > 5;
      }
    }
  }
});

  在使用这些校验规则时,有两点需要注意:

  (1)当默认值是对象或数组时,需要从函数中获取。

  (2)由于props会在组件实例创建之前进行验证,因此在default()和validator()函数中不能使用组件的属性,例如data、computed、methods等。

4)未在props中的特性

  组件可以声明任意多个特性,而那些没有在props中定义的特性不但会被保存到实例属性$attrs中,还会被添加到根元素上。注意,class和style两个特性未包含在$attrs属性中,并且它们会与原特性进行合并,而不是替换。以下面的btn-custom组件为例,根元素<button>会接收type和class两个特性。

<btn-custom type="submit" class="size"></btn-custom>
<script>
  Vue.component("btn-custom", {
    props: ["digit"],
    created: function() {
      console.log(this.$attrs);         //{type: "submit"}
    },
    template: '<button type="button" class="warning">{{digit}}</button>'
  });
</script>

  渲染出的<button>元素如下所示,其中type的值被替换成了“submit”,而class的值变成了“warning size”。

<button type="submit" class="warning size"></button>

  如果不想让根元素继承特性,那么可以将组件的inheritAttrs选项设为false,但要注意,inheritAttrs不会影响class和style的传递。还是以btn-custom组件为例,props和template两个选项与之前相同。

<btn-custom type="submit" class="size"></btn-custom>
<script>
  Vue.component("btn-custom", {
    inheritAttrs: false
  });
</script>

  渲染出的<button>元素如下所示,其中type的值未被替换,而class的值仍然是“warning size”。

<button type="button" class="warning size"></button>

三、混入

  混入(mixin)是一种代码复用技术,一个混入对象可包含任意组件选项,并能将其与普通组件混合在一起。

1)选项合并策略

  当组件和混入对象包含同名选项时,这些选项将会通过2种策略进行合并。

  (1)当数据对象或值为对象的选项(例如methods、components等)发生冲突时,同名的属性将以组件的为准。如下代码所示,虽然混入对象Mixin的数据对象也包含name属性,但是依然会被btn-custom组件中的name属性所覆盖,并且它的getName()也会被替换。

var Mixin = {
  data: function() {
    return { name: "strick" };
  },
  methods: {
    getName: function() {
      console.log("mixin");
    }
  }
};
Vue.component("btn-custom", {
  mixins: [Mixin],
  data: function() {
    return { name: "freedom" };
  },
  methods: {
    getName: function() {
      console.log("component");
    }
  }
});

  (2)当生命周期钩子发生冲突时,同名的钩子将合并成一个数组,混入对象的钩子在前,组件的钩子在后,如下所示,先输出“mixin”,再输出“component”。

var Mixin = {
  created: function() {
    console.log("mixin");
  }
};
Vue.component("btn-custom", {
  mixins: [Mixin],
  created: function() {
    console.log("component");
  }
});

2)全局混入

  通过Vue.mixin()方法可注册全局的混入对象,如下所示。

Vue.mixin({
  created: function () {
    console.log("global");
  }
});

  全局混入会影响所有的Vue实例,包括自定义的组件或第三方组件,因此要谨慎使用。大部分情况下它只适合自定义的选项,在官方的代码风格指南中,为混入中的这些选项制订了专门的命名规范,即以“$_”和自定义的命名空间为前缀(例如$_namespace_),从而避免与其它实例中的选项相冲突,下面是一个简单的示例。

Vue.mixin({
  $_namespace_getAge: function () {
    return 28;
  }
});

3)自定义选项合并策略

  除了预定义的合并策略之外,Vue还允许自定义合并策略,只需在Vue.config.optionMergeStrategies中添加一个包含合并逻辑的函数即可。

  下面是一个示例,首先在混入对象和组件中都声明了一个自定义的age选项;然后在Vue.config.optionMergeStrategies中添加一个同名的age()函数,并且需要在组件之前声明合并函数;最后在created钩子中调用实例属性$options,读取到的age值为28,符合age()函数中的合并规则。

var Mixin = {
  age: 28
};
Vue.config.optionMergeStrategies.age = function(toVal, fromVal) {
  return fromVal > toVal ? toVal : fromVal;
};
Vue.component("btn-custom", {
  mixins: [Mixin],
  created: function() {
    this.$options.age;        //28
  },
  age: 30
});

四、动态组件

  Vue内置的<component>元素可渲染一个元组件为动态组件,通过它的is特性来决定使用哪个组件。下面用一个例子来演示<component>元素的用法,首先全局注册两个组件tab1和tab2;然后将它们合并成数组赋给vm实例的tabs属性,而另一个current属性记录了当前要渲染的组件,默认值为tab1;最后将该属性值传递给is特性,并在DOM模板中创建两个按钮,每个按钮都注册了点击事件,可更改要渲染的组件。

<div id="container">
  <button v-for="tab in tabs" @click="current = tab">{{ tab }}</button>
  <component :is="current"></component>
</div>
<script>
  Vue.component("tab1", {
    template: '<input type="text"/>'
  });
  Vue.component("tab2", {
    template: '<input type="text"/>'
  });
  var vm = new Vue({
    el: "#container",
    data: {
      current: "tab1",
      tabs: ["tab1", "tab2"]
    }
  });
</script>

1)<keep-alive>

  虽然可以动态切换组件,但是组件的状态无法保持,例如在tab1组件的文本框中输入字符,来回切换后,这些字符就消失了。如果要缓存组件的状态,那么可以用Vue提供的另一个内置的<keep-alive>元素,如下所示,用它来包裹<component>元素,就不会销毁失活的组件,从而提升渲染性能。

<keep-alive>
  <component :is="current"></component>
</keep-alive>

  注意,<keep-alive>元素自身不会渲染成一个DOM元素,并且其可与任意元素配合,但子元素只能渲染一个。由此可知,<keep-alive>元素内可包含条件指令(如下所示),但不能包含v-for指令。

<keep-alive>
  <tab1 v-if="current == 'tab1'"></tab1>
  <tab2 v-else></tab2>
<keep-alive>

  有两个与<keep-alive>元素相关的生命周期钩子:activated和deactivated。以之前的tab1组件为例,为其添加这两个钩子(如下代码所示),它被包裹在<keep-alive>元素中。当激活tab1组件时,会触发activated钩子;而当停用tab1组件时,会触发deactivated钩子。

Vue.component("tab1", {
  template: '<input type="text"/>',
  activated: function() {
    console.log("activated");
  },
  deactivated: function() {
    console.log("deactivated");
  }
});

 

 posted on 2019-10-28 08:27  咖啡机(K.F.J)  阅读(499)  评论(0编辑  收藏  举报