zoukankan      html  css  js  c++  java
  • Vue.js 学习笔记 第7章 组件详解

    本篇目录:

    7.1 组件与复用

    7.2 使用props传递数据

    7.3 组件通讯

    7.4 使用slot分发内容

    7.5 组件高级用法

    7.6 其他

    7.7 实战:两个常用组件的开发

    组件(Component)是Vue.js最核心的功能,也是整个框架设计最精彩的地方,当然也是最难掌握的。
    本章将带领你由浅入深地学习组件的全部内容,并通过几个实战项目熟练使用Vue组件。

    7.1 组件与复用

    7.1.1 为什么使用组件

    在正式介绍组件前,我们先来看一个简单的场景,如图7-1所示:

    图7-1中是一个很常见的聊天界面,有一些标准的控件,比如右上角的关闭按钮、输入框、发送按钮等。

    你可能要问了,这有什么难的,不就是几个<div><input>吗?
    好,那现在需求升级了,这几个控件还有别的地方要用到。
    没问题,复制粘贴呗。

    那如果输入框要带数据验证,按钮的图标支持自定义呢?
    这样用JavaScript封装后一起复制吧。

    那等到项目快完结时,产品经理说,所有使用输入框的地方,都要改成支持回车键提交。
    好吧,给我一天的事件,我一个一个加上去。

    上面的需求虽然有点变态,但却是业务中很常见的,那就是一些控件、JavaScript能力的复用。
    没错,Vue.js的组件就是提高重用性的,让代码可重用。
    当学习完组件后,上面的问题就可以分分钟搞定了,再也不用害怕铲平经理的奇葩需求。

    我们先看一下图7-1中的示例用组件来编写是怎么的,示例代码如下:

     1 <Card style="350px;">
     2     <p slot="title">与 xxx 聊天中</p>
     3     <a href="#" slot="extra">
     4         <Icon type="android-close" size="18"></Icon>
     5     </a>
     6     <div style="height:100px;"></div>
     7     <div>
     8         <Row :gutter="16">
     9             <i-col span="17">
    10                 <i-input v-model="value" placeholder="请输入..."></i-input>
    11             </i-col>
    12             <i-col span="4">
    13                 <i-button v-model="primary" icon="paper-airplane">发送</i-button>
    14             </i-col>
    15         </Row>
    16     </div>
    17 </Card>

    是不是很奇怪,有很多我们从来都没有见过的标签,比如<Card><Row><i-col><input><i-button>等。
    而且整段代码除了内联的几个样式外,一句CSS代码也没有,但最终实现的UI就是图7-1的效果。

    这些没见过的自定义标签就是组件,每个标签代表一个组件,在任何使用Vue的地方都可以直接使用。
    接下来,我们就看看组件的具体用法。

    7.1.2 组件用法

    回顾一下我们创建Vue实例的方法:

    1 var app = new Vue({
    2     el: "#app"
    3 });

    组件与之类似,需要注册之后才可以使用。注册有全局注册和局部注册两种方式。
    全局注册后,任何Vue实例都可以使用。全局注册示例代码如下:

    1 Vue.component("my-component", {
    2     // 选项
    3 });

    my-component就是注册的组件自定义标签名称,推荐使用小写加减号分割的形式命名。

    要在父实例中使用这个组件,必须要在实例创建前注册。
    之后就可以用<my-component></my-component>的形式来使用组件了。
    实例代码如下:

     1 <div id="app">
     2     <my-component></my-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("my-component", {
     7         // 选项
     8     });
     9     
    10     var app = new Vue({
    11         el: "#app"
    12     });
    13 </script>

    此时打开页面还是空白的,因为我们注册的组件没有任何内容。
    在组件选项中添加template就可以显示组件内容了。
    实例代码如下:

    1 Vue.component("my-component", {
    2     template: "<div>这里是组件的内容</div>"
    3 });

    渲染后的结果是:

    1 <div id="app">
    2     <div>这里是组件的内容</div>
    3 </div>

    template的DOM结构必须被一个元素包含,如果直接写成“这里是组件的内容”,不带<div></div>是无法渲染的。

    在Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。
    组件也可以使用components选项来注册组件,该组件可以嵌套。
    示例代码如下:

     1 <div id="app">
     2     <my-component></my-component>
     3 </div>
     4 
     5 <script>
     6     var Child = {
     7         template: "<div>局部注册组件的内容</div>"
     8     };
     9     
    10     var app = new Vue({
    11         el: "#app",
    12         components: {
    13             "my-component": Child
    14         }
    15     });
    16 </script>

    Vue组件的模板在某些情况下回收到HTML的限制,比如<table>内规定只允许是<tr><td><th>等这些表格元素,所以在<table>内直接使用组件是无效的。
    这种情况下,可以使用特殊的is属性来挂载组件,示例代码如下:

     1 <div id="app">
     2     <table>
     3         <tbody is="my-component"></tbody>
     4     </table>
     5 </div>
     6 
     7 <script>
     8     Vue.component("my-component", {
     9         template: "<div>这里是组件的内容</div>"
    10     });
    11     
    12     var app = new Vue({
    13         el: "#app"
    14     });
    15 </script>

    <tbody>在渲染时,会被替换为组件的内容。
    常见的限制元素还有<ul><ol><select>

    提示:
    如果使用的是字符串模板,是不受限制的,比如后面章节介绍的.vue单文件用法等。

    除了template选项外,组件还可以像Vue实例那样使用其他的选项,比如datacomputedmethod等。
    但是在使用data时,和实例稍有区别,data必须是函数,然后将数据return出去。
    例如:

     1 <div id="app">
     2     <my-component></my-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("my-component", {
     7         template: "<div>{{message}}</div>",
     8         data: function() {
     9             return {
    10                 message: "组件内容"
    11             };
    12         }
    13     });
    14     
    15     var app = new Vue({
    16         el: "#app"
    17     });
    18 </script>

    JavaScript对象是引用关系,所以如果return出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。
    比如下面的示例:

     1 <div id="app">
     2     <my-component></my-component>
     3     <my-component></my-component>
     4     <my-component></my-component>
     5 </div>
     6 
     7 <script>
     8     var data = {
     9         counter: 0
    10     };
    11     
    12     Vue.component("my-component", {
    13         template: "<button @click='counter++'>{{counter}}</button>",
    14         data: function() {
    15             return data;
    16         }
    17     });
    18     
    19     var app = new Vue({
    20         el: "#app"
    21     });
    22 </script>

    组件使用了3次,但是点击任意一个<button>,3个按钮的数字都会加1。
    那是因为组件的data引用的是外部的对象,这肯定不是我们期望的效果。
    所以给组件返回一个新的data对象来独立,示例代码如下:

     1 <div id="app">
     2     <my-component></my-component>
     3     <my-component></my-component>
     4     <my-component></my-component>
     5 </div>
     6 
     7 <script>
     8     Vue.component("my-component", {
     9         template: "<button @click='counter++'>{{counter}}</button>",
    10         data: function() {
    11             return {
    12                 counter: 0
    13             };
    14         }
    15     });
    16     
    17     var app = new Vue({
    18         el: "#app"
    19     });
    20 </script>

    这样,点击3个按钮就互不影响了,完全达到复用的目的。

    7.2 使用props传递数据

    7.2.1 基本用法

    组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。
    通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。
    这个正向传递数据的过程就是通过props来实现的。

    在组件中,使用选项props来声明需要从父级接收的数据。
    props的值可以是两种,一种是字符串数组,一种是对象,本小节先介绍数组的用法。
    比如我们构造一个数组,接收一个来自父级的数据message,并把它在组件模板中渲染,示例代码如下:

     1 <div id="app">
     2     <my-component message="来自父组件的数据"></my-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("my-component", {
     7         props: ["message"],
     8         template: "<div>{{message}}</div>"
     9     });
    10     
    11     var app = new Vue({
    12         el: "#app"
    13     });
    14 </script>

    渲染后的结果为:

    1 <div id="app">
    2     <div>来自父组件的数据</div>
    3 </div>

    props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板template及计算属性computed和方法methods中使用。
    上例的数据message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称,如果要传递多个数据,在props数组中添加项即可。

    由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名(camelCase)的props名称要转为短横分隔命名(kebab-case)。例如:

     1 <div id="app">
     2     <my-component warning-text="提示信息"></my-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("my-component", {
     7         props: ["warningText"],
     8         template: "<div>{{warningText}}</div>"
     9     });
    10     
    11     var app = new Vue({
    12         el: "#app"
    13     });
    14 </script>

    提示:
    如果使用的是字符串模板,仍然可以忽略这些限制。

    有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这是可以使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。示例代码如下:

     1 <div id="app">
     2     <input type="text" v-model="parentMessage">
     3     <my-component :message="parentMessage"></my-component>
     4 </div>
     5 
     6 <script>
     7     Vue.component("my-component", {
     8         props: ["message"],
     9         template: "<div>{{message}}</div>"
    10     });
    11     
    12     var app = new Vue({
    13         el: "#app",
    14         data: {
    15             parentMessage: ""
    16         }
    17     });
    18 </script>

    这里用v-model绑定了父级的数据parentMessage
    当通过输入框任意输入时,子组件接收的props(message)也会实时响应,并更新组件模板。

    提示:
    注意,如果你要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅仅是字符串,尝试下面的示例来对比。

     1 <div id="app">
     2     <my-component message="[1,2,3]"></my-component>
     3     <my-component :message="[1,2,3]"></my-component>
     4 </div>
     5 <script>
     6     Vue.component("my-component", {
     7         props: ["message"],
     8         template: "<div>{{message.length}}</div>"
     9     });
    10     var app = new Vue({
    11         el: "#app"
    12     });
    13 </script>

    同一个组件使用了两次,区别仅仅是第二个使用的是v-bind

    渲染后的结果:第一个是7,第二个才是数组的长度3。

    7.2.2 单向数据流

    Vue 2.x与Vue 1.x比较大的一个改变就是,Vue 2.x通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。
    而在Vue 1.x里提供了.sync修饰符来支持双向绑定。
    之所以这样设计,是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。

    业务中经常用到两种需要改变prop的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。
    何种情况可以在组件data内再声明一个数据,引用父组件的prop,实例代码如下:

     1 <div id="app">
     2     <my-component :init-count="1"></my-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("my-component", {
     7         props: ["initCount"],
     8         template: "<div>{{count}}</div>",
     9         data: function() {
    10             return {
    11                 count: this.initCount
    12             };
    13         }
    14     });
    15     
    16     var app = new Vue({
    17         el: "#app"
    18     });
    19 </script>

    组件中声明了数据count,它在组件初始化时会获取来自父组件的initCount,之后就与之无关了,只用维护count,这样就可以避免直接操作initCount

    另一种情况就是prop作为需要转变的原始值传入。
    这种情况用计算属性就可以了,示例代码如下:

     1 <div id="app">
     2     <my-component :width="100"></my-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("my-component", {
     7         props: ["width"],
     8         template: "<div :style='style'>组件内容</div>",
     9         computed: {
    10             style: function() {
    11                 return {
    12                      this.width + "px"
    13                 };
    14             }
    15         }
    16     });
    17     
    18     var app = new Vue({
    19         el: "#app"
    20     });
    21 </script>

    因为用CSS传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。

    提示:
    注意,在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变时会影响父组件的。

    7.2.3 数据验证

    我们上面所介绍的props选项的值都是一个数组,一开始也介绍过,除了数组外,还可以是对象,当prop需要验证时,就需要对象写法。

    一般当你的组件需要提供给别人使用时,推荐都进行数据验证。
    比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。

    一下是几个prop的示例:

     1 Vue.component("my-component", {
     2     props: {
     3         // 必须是数字类型
     4         propA: Number,
     5         // 必须是字符串或数字类型
     6         propB: [String, Number],
     7         // 布尔值,如果没有定义,默认值就是true
     8         propC: {
     9             type: Boolean,
    10             default: true
    11         },
    12         // 数字,而且是必传
    13         propD: {
    14             type: Number,
    15             required: true
    16         },
    17         // 如果是数组或对象,默认值必须是一个函数来返回
    18         propE: {
    19             type: Array,
    20             default: function() {
    21                 rturn[];
    22             }
    23         },
    24         // 自定义一个验证函数
    25         propF: {
    26             validator: function(value) {
    27                 return value > 10;
    28             }
    29         }
    30     }
    31 });

    验证的type类型可以是:

    • String
    • Number
    • Boolean
    • Object
    • Array
    • Function

    type也可以是一个自定义构造器,使用instanceof检测。
    prop验证失败时,在开发版本下会在控制台抛出一条警告。

    7.3 组件通信

    我们已经知道,从父组件向子组件通信,通过props传递数据就可以了。
    但Vue组件通信的场景不止有这一种,归纳起来,组件之间通信可以用图7-2表示。

    组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。
    本节将介绍组中组件之间通信的方法。

    7.3.1 自定义事件

    当子组件需要向父组件传递数据时,就要用到自定义事件。
    我们在介绍指令 v-on 时有提到,v-on除了监昕DOM事件外,还可以用于组件之间的自定义事件。

    如果你了解过JavaScript的设计模式一一观察者模式,一定知道dispatchEventaddEventListener这两个方法。
    Vue组件也有与之类似的一套模式,子组件用$emit()来触发事件,父组件用$on()来监昕子组件的事件。

    父组件也可以直接在子组件的自定义标签上使用v-on来监昕子组件触发的自定义事件,示例代码如下:

     1 <div id="app">
     2     <p>总数:{{total}}</p>
     3     <my-component @increase="handleGetTotal" @reduce="handleGetTotal"></my-component>
     4 </div>
     5 
     6 <script>
     7     Vue.component("my-component", {
     8         template: "<div><button @click='handleIncrease'>+1</button><button @click='handleReduce'>-1</button></div>",
     9         data: function() {
    10             return {
    11                 counter: 0
    12             }
    13         },
    14         methods: {
    15             handleIncrease: function() {
    16                 this.counter++;
    17                 this.$emit("increase", this.counter);
    18             },
    19             handleReduce: function() {
    20                 this.counter--;
    21                 this.$emit("reduce", this.counter);
    22             }
    23         }
    24     });
    25     
    26     var app = new Vue({
    27         el: "#app",
    28         data: {
    29             total: 0
    30         },
    31         methods: {
    32             handleGetTotal: function(total) {
    33                 this.total = total;
    34             }
    35         }
    36     });
    37 </script>

    上面示例中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data(counter)后,通过$emit()再把它传递给父组件,父组件用v-on:increasev-on:reduce(示例使用的是语法糖)。
    $emit()方法的第一个参数是自定义事件的名称,例如示例的increasereduce后面的参数都是要传递的数据,可以不填或填写多个。

    除了用v-on在组件上监听自定义事件外,也可以监听DOM事件,这时可以使用.native修饰符表示监听的是一个原生事件,监听的是该组件的根元素,实例代码如下:

    1 <my-component v-on:click.native="handleClick"></my-component>

    7.3.2 使用v-model

    Vue 2.x可以在自定义组件上使用v-model指令,我们先来看一个示例:

     1 <div id="app">
     2     <p>总数:{{total}}</p>
     3     <my-component v-model="total"></my-component>
     4 </div>
     5 
     6 <script>
     7     Vue.component("my-component", {
     8         template: "<button @click='handleClick'>+1</button>",
     9         data: function() {
    10             return {
    11                 counter: 0
    12             }
    13         },
    14         methods: {
    15             handleClick: function() {
    16                 this.counter++;
    17                 this.$emit("input", this.counter);
    18             }
    19         }
    20     });
    21     var app = new Vue({
    22         el: "#app",
    23         data: {
    24             total: 0
    25         }
    26     });
    27 </script>

    仍然是点击按钮加1的效果,不过这次组件$emit()的事件名是特殊的input,在使用组件的父级,并没有在<my-component>上使用@input='handler',而是直接用了v-model绑定的一个数据total
    这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:

     1 <div id="app">
     2     <p>总数:{{total}}</p>
     3     <my-component @input="handleGetTotal"></my-component>
     4 </div>
     5 
     6 <script>
     7     Vue.component("my-component", {
     8         template: "<button @click='handleClick'>+1</button>",
     9         data: function() {
    10             return {
    11                 counter: 0
    12             }
    13         },
    14         methods: {
    15             handleClick: function() {
    16                 this.counter++;
    17                 this.$emit("input", this.counter);
    18             }
    19         }
    20     });
    21     
    22     var app = new Vue({
    23         el: "#app",
    24         data: {
    25             total: 0
    26         },
    27         methods: {
    28             handleGetTotal: function(total) {
    29                 this.total = total;
    30             }
    31         }
    32     });
    33 </script>

    v-model还可以用来创建自定义的表单输入组件,进行数据双向绑定,例如:

     1 <div id="app">
     2     <p>总数:{{total}}</p>
     3     <my-component v-model="total"></my-component>
     4     <button @click="handleReduce">-1</button>
     5 </div>
     6 
     7 <script>
     8     Vue.component("my-component", {
     9         props: ["value"],
    10         template: "<input :value='value' @input='updateValue'>",
    11         methods: {
    12             updateValue: function(event) {
    13                 this.$emit("input", event.target.value);
    14             }
    15         }
    16     });
    17     
    18     var app = new Vue({
    19         el: "#app",
    20         data: {
    21             total: 0
    22         },
    23         methods: {
    24             handleReduce: function() {
    25                 this.total--;
    26             }
    27         }
    28     });
    29 </script>

    实现这样一个具有双向绑定的v-model组件要满足下面两个要求:

    • 接收一个value属性。
    • 在有新的value时触发input事件。

    7.3.3 非父子组件通信

    在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种:兄弟组件和跨多级组件。
    为了更加彻底地了解Vue.js 2.x中的通信方法,我们先来看一下在Vue.js 1.x中是如何实现的,这样便于我们了解Vue.js的设计思想。

    在Vue.js 1.x中,除了$emit()方法外,还提供了$dispatch()$broadcast()这两个方法。
    $dispatch()用于向上级派发事件,只要是它的父级(一般或多级以上),都可以在Vue实例的events选项内接收,示例代码如下:

     1 <!-- 注意:该示例需要使用Vue.js 1.x的版本 -->
     2 <div id="app">
     3     {{message}}
     4     <my-component></my-component>
     5 </div>
     6 
     7 <script>
     8     Vue.component("my-component", {
     9         template: "<button @click='handleDispatch'>源发事件</button>",
    10         methods: {
    11             handleDispatch: function() {
    12                 this.$dispatch("on-message", "来自内部组件的数据");
    13             }
    14         }
    15     });
    16     
    17     var app = new Vue({
    18         el: "#app",
    19         data: {
    20             message: ""
    21         },
    22         methods: {
    23             "on-message": function(msg) {
    24                 this.message = msg;
    25             }
    26         }
    27     });
    28 </script>

    同理,$broadcast()是由上级向下级广播事件的,用法完全一致,只是方向相反。

    这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回true

    这两个方法虽然看起来很好用,但是在Vue.js 2.x中都废弃了,因为基于组件树结构的事件流方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱,并且不能解决兄弟组件通信的问题。

    在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。
    为了更形象地了解它,我们举一个生活中的例子。

    比如你需要租房子,你可能会找房产中介来等级你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你。
    整个过程中,买家和卖家并没有任何交流,都是通过中间人来传话的。

    或者你最近可能要换房了,你会找房产中介登记你的信息,订阅与你找房需求相关的资讯。
    一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。

    这两个例子中,你和出租者担任的就是两个跨级的组件,而房产中介就是这个中央事件总线(bus)。
    比如下面的示例代码:

     1 <div id="app">
     2     {{message}}
     3     <my-component-a></my-component-a>
     4 </div>
     5 
     6 <script>
     7     var bus = new Vue();
     8     
     9     Vue.component("my-component-a", {
    10         template: "<button @click='handleEvent'>传递事件</button>",
    11         methods: {
    12             handleEvent: function() {
    13                 bus.$emit("on-message", "来自组件 component-a 的内容");
    14             }
    15         }
    16     });
    17     
    18     var app = new Vue({
    19         el: "#app",
    20         data: {
    21             message: ""
    22         },
    23         mounted: function() {
    24             var _this = this;
    25             // 在实例初始化时,监听来自bus示例的事件
    26             bus.$on("on-message", function(msg) {
    27                 _this.message = msg;
    28             });
    29         }
    30     });
    31 </script>

    首先创建了一个名为bus的空Vue实例,里面没有任何内容;然后全局定义了组件component-a;最后创建Vue实例app
    app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus的事件on-message
    而在组件component-a中,点击按钮会通过bus把事件on-message发出去,
    此时app就会接受到来自bus的事件,进而在回调里完成自己的业务逻辑。

    这种方法巧妙而清凉地实现了任何组件间的通信,包括父子、兄弟、跨级,而且Vue 1.x和Vue 2.x都适用。
    如果深入使用,可以扩展bus实例,给它添加datamethodscomputed等选项,这些都是可以公用的。
    在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息。
    比如用户登录的昵称、性别、邮箱等,还有用户的授权token等,只需在初始化时让bus获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,我们会在进阶篇中逐步介绍这些内容。

    当你的项目比较大,有更多的小伙伴参与开发时,也可以你选择更好的状态管理解决方案vuex,在进阶篇里会详细介绍关于它的用法。

    除了中央事件总线bus外,还有两种方法可以实现组件间通信:父链和子组件索引。

    父链

    在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
    示例代码如下:

     1 <div id="app">
     2     {{message}}
     3     <my-component-a></my-component-a>
     4 </div>
     5 
     6 <script>
     7     Vue.component("my-component-a", {
     8         template: "<button @click='handleEvent'>通过父链直接修改数据</button>",
     9         methods: {
    10             handleEvent: function() {
    11                 // 访问到父链后,可以做任何操作,比如直接修改数据
    12                 this.$parent.message = "来自组件 component-a 的内容";
    13             }
    14         }
    15     });
    16     
    17     var app = new Vue({
    18         el: "#app",
    19         data: {
    20             message: ""
    21         }
    22     });
    23 </script>

    尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。
    父子组件最好还是通过props$emit()来通信。

    子组件索引

    当子组件较多时,通过this.$children来一一遍历我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称。
    示例代码如下:

     1 <div id="app">
     2     <button @click="handleRef">通过ref获取子组件实例</button>
     3     <component-a ref="comA"></component-a>
     4 </div>
     5 
     6 <script>
     7     Vue.component("component-a", {
     8         template: "<div>子组件</div>",
     9         data: function() {
    10             return {
    11                 message: "子组件内容"
    12             };
    13         }
    14     });
    15     
    16     var app = new Vue({
    17         el: "#app",
    18         methods: {
    19             handleRef: function() {
    20                 // 通过$refs来访问指定的实例
    21                 var msg = this.$refs.comA.message;
    22                 console.log(msg);
    23             }
    24         }
    25     });
    26 </script>

    在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过this.$refs来访问指定名称的子组件。

    提示:
    (refs只在组件渲染完成后才填充,并且它是非响应式的。
    它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用)refs。

    与Vue 1.x不同的是,Vue 2.x将v-elv-ref合并为了ref,Vue会自动去判断是普通标签还是组件。
    可以尝试补全下面的代码,分别打印出两个ref看看都是什么:

    1 <div id="app">
    2     <p ref="p">内容</p>
    3     <child-component ref="child"></child-component>
    4 </div>

    7.4 使用slot分发内容

    7.4.1 什么是slot

    我们先看一个比较常规的网站布局,如图7-3所示。

    这个网站由一级导航、二级导航、左侧列表、正文以及底部版权信息5个模块组成。
    如果要将它们都组件化,这个结构可能会是:

    1 <app>
    2     <menu-main></menu-main>
    3     <menu-sub></menu-sub>
    4     <div class="container">
    5         <menu-left></menu-left>
    6         <container></container>
    7     </div>
    8     <app-footer></app-footer>
    9 </app>

    当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot,这个过程叫做内容分发(transclusion)。
    <app>为例,它有两个特点:

    • <app>组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
    • <app>组件很可能有它自己的模板。

    props传递数据、events触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。

    7.4.2 作用域

    正式介绍slot前,需要先知道一个概念:编译的作用域。
    比如父组件有如下模板:

    1 <child-component>
    2     {{message}}
    3 </child-component>

    这里的message就是一个slot
    但是它绑定的是父组件的数据,而不是组件<child-component>的数据。

    父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。
    例如下面的代码示例:

     1 <div id="app">
     2     <child-component v-show="showChild"></child-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("child-component", {
     7         template: "<div>子组件</div>"
     8     });
     9     var app = new Vue({
    10         el: "#app",
    11         data: {
    12             showChild: true
    13         }
    14     })
    15 </script>

    这里的状态showChild绑定的是父组件的数据,如果想在子组件上绑定,那应该是:

     1 <div id="app">
     2     <child-component></child-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("child-component", {
     7         template: "<div v-show='showChild'>子组件</div>",
     8         data: function() {
     9             return {
    10                 showChild: true
    11             };
    12         }
    13     });
    14     
    15     var app = new Vue({
    16         el: "#app"
    17     })
    18 </script>

    因此,slot分发的内容,作用域是在父组件上的。

    7.4.3 solt用法

    单个Slot

    在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入子组件标签内的所有内容将替代子组件的<slot>标签及它的内容。
    示例代码如下:

     1 <div id="app">
     2     <child-component>
     3         <p>分发的内容</p>
     4         <p>更多分发的内容</p>
     5     </child-component>
     6 </div>
     7 
     8 <template id="template">
     9     <div>
    10         <slot>
    11             <p>如果父组件没有插入内容,我将作为默认出现</p>
    12         </slot>
    13     </div>
    14 </template>
    15 
    16 <script>
    17     Vue.component("child-component", {
    18         template: "#template"
    19     });
    20     var app = new Vue({
    21         el: "#app"
    22     })
    23 </script>

    子组件child-component的模板内定义了一个<slot>元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本;如果写入了slot,那就回替换整个<slot>
    所以上例渲染后的结果为:

    1 <div id="app">
    2     <div>
    3         <p>分发的内容</p>
    4         <p>更多分发的内容</p>
    5     </div>
    6 </div>

    提示:
    注意,子组件<slot>内的备用内容,它的作用域是子组件本身。

    具名Slot

    <slot>元素指定一个name后可以分发多个内容,具名Slot可以与单个Slot共存。
    例如下面的示例:

     1 <div id="app">
     2     <child-component>
     3         <h2 slot="header">标题</h2>
     4         <p>正文内容</p>
     5         <p>更多的正文内容</p>
     6         <div slot="footer">底部信息</div>
     7     </child-component>
     8 </div>
     9 
    10 <template id="template">
    11     <div class="container">
    12         <div class="header">
    13             <slot name="header"></slot>
    14         </div>
    15         <div class="main">
    16             <slot></slot>
    17         </div>
    18         <div class="footer">
    19             <slot name="footer"></slot>
    20         </div>
    21     </div>
    22 </template>
    23 
    24 <script>
    25     Vue.component("child-component", {
    26         template: "#template"
    27     });
    28     var app = new Vue({
    29         el: "#app"
    30     })
    31 </script>

    子组件内声明了3个<slot>元素,其中在<div name="main">内的<slot>没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将会出现在这里。

    如果没有指定默认的匿名slot,父组件内多余的内容片断都将被抛弃。

    上例最终渲染后的结果为:

     1 <div id="app">
     2     <div class="container">
     3         <div class="header">
     4             <h2>标题</h2>
     5         </div>
     6         <div class="main">
     7             <p>正文内容</p>
     8             <p>更多的正文内容</p>
     9         </div>
    10         <div class="footer">
    11             <div>底部信息</div>
    12         </div>
    13     </div>
    14 </div>

    在组合使用组件时,内容分发API至关重要。

    7.4.4 作用域插槽

    作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。
    概念比较难理解,我们先看一个简单的示例来了解它的基本用法。
    示例代码如下:

     1 <div id="app">
     2     <child-component>
     3         <template scope="props">
     4             <p>来自父组件的内容</p>
     5             <p>{{props.msg}}</p>
     6         </template>
     7     </child-component>
     8 </div>
     9 
    10 <template id="template">
    11     <div class="container">
    12         <slot msg="来自子组件的内容"></slot>
    13     </div>
    14 </template>
    15 
    16 <script>
    17     Vue.component("child-component", {
    18         template: "#template"
    19     });
    20     var app = new Vue({
    21         el: "#app"
    22     })
    23 </script>

    观察子组件的模板,在<slot>元素上有一个类似props传递数据给组件的写法msg="xxx",数据传到了插槽。
    父组件中使用了<template>元素,而且拥有一个scope="props"的特性,这里的props只是一个临时变量,就像v-for="item in items"里面的item一样。
    template内可以通过临时变量props访问来自子组件插槽的数据msg

    将上面的示例渲染后的最终结果为:

    1 <div id="app">
    2     <div class="container">
    3         <p>来自父组件的内容</p>
    4         <p>来自子组件的内容</p>
    5     </div>
    6 </div>

    作用域插槽根据代表性的用力是列表组件,允许组件自定义应该如何渲染列表每一项。
    示例代码如下:

     1 <div id="app">
     2     <my-list v-bind:books="books">
     3         <template slot="book" scope="props">
     4             <li>{{props.bookName}}</li>
     5         </template>
     6     </my-list>
     7 </div>
     8 
     9 <template id="template">
    10     <ul>
    11         <slot name="book" v-for="book in books" v-bind:book-name="book.name">
    12             <!-- 这里也可以写默认slot内容 -->
    13         </slot>
    14     </ul>
    15 </template>
    16 
    17 <script>
    18     Vue.component("my-list", {
    19         props: {
    20             books: {
    21                 type: Array,
    22                 default: function() {
    23                     return {};
    24                 }
    25             }
    26         },
    27         template: "#template"
    28     });
    29     
    30     var app = new Vue({
    31         el: "#app",
    32         data: {
    33             books: [
    34                 {name:"《Vue.js实战》"},
    35                 {name:"《JavaScript语言精粹》"},
    36                 {name:"《JavaScript高级程序设计》"}
    37             ]
    38         }
    39     });
    40 </script>

    子组件my-list接收一个来自父级的prop数组books,并且将它在namebookslot上使用v-for指令循环,同时暴露一个变量bookName

    如果你仔细揣摩上面的用法,你可能会产生这样的疑问:我直接在父组件用v-for不就好了吗?为什么还要绕一步,在子组件里面循环呢?
    的确,如果只是针对上面的示例,这样写是多此一举的。此例的用意主要是介绍作用域插槽的用法,并没有加入使用场景,而作用域插槽的适用场景就是既可以复用子组件的slot,又可以使slot内容不一致。
    如果上例还在其他组件内使用,<li>的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)子组件内获取。

    7.4.5 访问slot

    在Vue.js 1.x中,想要获取某个slot是比较麻烦的,需要用v-el间接获取。
    而Vue.js 2.x提供了用来访问被slot分发的内容的方法$slots,请看下面的示例:

     1 <div id="app">
     2     <child-component>
     3         <h2 slot="header">标题</h2>
     4         <p>正文内容</p>
     5         <p>更多的正文内容</p>
     6         <div slot="footer">底部信息</div>
     7     </child-component>
     8 </div>
     9 
    10 <template id="template">
    11     <div class="container">
    12         <div class="header">
    13             <slot name="header"></slot>
    14         </div>
    15         <div class="main">
    16             <slot></slot>
    17         </div>
    18         <div class="footer">
    19             <slot name="footer"></slot>
    20         </div>
    21     </div>
    22 </template>
    23 
    24 <script>
    25     Vue.component("child-component", {
    26         template: "#template",
    27         mounted: function() {
    28             var header = this.$slots.header;
    29             var main = this.$slots.main;
    30             var footer = this.$slots.footer;
    31             console.log(footer);
    32             console.log(footer[0].elm.innerHTML);
    33         }
    34     });
    35     
    36     var app = new Vue({
    37         el: "#app"
    38     });
    39 </script>

    通过$slots可以访问某个具名slot,this.$slots.default包括了所有没有被包含在具名slot中的节点。
    尝试编写代码,查看两个console打印的内容。

    $slots在业务中几乎用不到,在用render函数(进阶篇中将介绍)创建组件时会比较有用,但主要还是用于独立组件开发中。

    7.5 组件高级用法

    本节会介绍组件的一些高级用法,这些用法在实际业务中不是很常用,但会独立组件开发时可能会用到。
    如果你感觉以上内容已经足骨完成你的业务开发了,可以跳过本节;如果你想继续探索Vue组件的奥秘,读完本节会对你有很大的启发。

    7.5.1 递归组件

    组件在它的模板内可以递归地调用自己,只要给组件设置name的选项就可以了。
    示例代码如下:

     1 <div id="app">
     2     <child-component v-bind:count="1"></child-component>
     3 </div>
     4 
     5 <template id="template">
     6     <div class="child">
     7         <child-component v-bind:count="count+1" v-if="count<3"></child-component>
     8     </div>
     9 </template>
    10 
    11 <script>
    12     Vue.component("child-component", {
    13         name: "child-component",
    14         props: {
    15             count: {
    16                 type: Number,
    17                 default: 1
    18             }
    19         },
    20         template: "#template"
    21     });
    22     
    23     var app = new Vue({
    24         el: "#app"
    25     });
    26 </script>

    设置name后,在组件模板内就可以递归使用了。
    不过需要注意的是,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded

    组件递归使用可以用来开发一些具有未知层级关系的阻力组件,比如级联选择器和树形控件等。
    如图7-4和图7-5所示:

    在实战篇里,我们会详细介绍级联选择器的实现。
     

    7.5.2 内联模板

    组件的模板一般都是在template选项内自定义的,Vue提供了一个内联模板的功能,在使用组件时,给组件标签使用inline-template特性,组件就会把它的内容当做模板,而不是把它当内容分发,这让模板更灵活。
    示例代码如下:

     1 <div id="app">
     2     <child-component inline-template :data="message">
     3         <div>
     4             <h2>在父组件中定义了组件的模板</h2>
     5             <p>{{data}}</p>
     6             <p>{{msg}}</p>
     7         </div>
     8     </child-component>
     9 </div>
    10 
    11 <script>
    12     Vue.component("child-component", {
    13         props: ["data"],
    14         data: function() {
    15             return {
    16                 msg: "在子组件声明的数据"
    17             };
    18         }
    19     });
    20     
    21     var app = new Vue({
    22         el: "#app",
    23         data: {
    24             message: "在父组件声明的数据"
    25         }
    26     });
    27 </script>

    渲染后的结果为:

    1 <div id="app">
    2     <div>
    3         <h2>在父组件中定义了组件的模板</h2> 
    4         <p>在父组件声明的数据</p> 
    5         <p>在子组件声明的数据</p>
    6     </div>
    7 </div>

    7.5.3 动态组件

    Vue.js提供了一个特殊的元素<component>用来动态地挂载不同的组件,使用is特性来选择要挂载的组件。
    示例代码如下:

     1 <div id="app">
     2     <input type="text" placeholder="请输入组件名称: A/B/C">
     3     <button @click="handleChangeView">确定切换</button>
     4     <component v-bind:is="currentView"></component>
     5 </div>
     6 
     7 <script>
     8     var app = new Vue({
     9         el: "#app",
    10         components: {
    11             comA: {
    12                 template: "<div>组件A</div>"
    13             },
    14             comB: {
    15                 template: "<div>组件B</div>"
    16             },
    17             comC: {
    18                 template: "<div>组件C</div>"
    19             }
    20         },
    21         data: {
    22             currentView: "comA"
    23         },
    24         methods: {
    25             handleChangeView: function() {
    26                 var value = document.querySelector("input").value;
    27                 this.currentView = "com" + value;
    28             }
    29         }
    30     });
    31 </script>

    动态地改变currentView的值就可以动态挂载组件了。
    也可以直接绑定在组件对象上:

     1 <div id="app">
     2     <component v-bind:is="currentView"></component>
     3 </div>
     4 
     5 <script>
     6     var Home = {
     7         template: "<p>Welcome home!</p>"
     8     };
     9     var app = new Vue({
    10         el: "#app",
    11         data: {
    12             currentView: Home
    13         }
    14     });
    15 </script>

    7.5.4 异步组件

    当你的工程足够大,使用的组件足够多时,是时候考虑下性能的问题了,因为一开始把所有的组件都加载是没必要的一笔开销。
    好在Vue.js允许将组件定义为一个工厂函数,动态地解析组件。
    Vue.js值在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
    例如下面的示例:

     1 <div id="app">
     2     <child-component></child-component>
     3 </div>
     4 
     5 <script>
     6     Vue.component("child-component", function(resolve, reject) {
     7         window.setTimeout(function() {
     8             resolve({
     9                 template: "<div>我是异步渲染的</div>"
    10             });
    11         }, 2000);
    12     });
    13     var app = new Vue({
    14         el: "#app"
    15     });
    16 </script>

    工厂函数接收一个resolve回调,在收到从服务器下载的组件定义时调用。
    也可以调用reject(reason)指示加载失败。
    这里setTimeout只是为了演示异步,具体的下载逻辑可以自己决定,比如把组件配置写成一个对象配置,通过Ajax来请求,然后调用resolve传入配置选项。

    在进阶篇里,我们还会介绍主流的打包编译工具webpack和.vue单文件的用法,更优雅地实现异步组件(路由)。

    7.6 其他

    7.6.1 $nextTick

    我们先来看这样一个场景:
    有一个div,默认用v-if将它隐藏,点击一个按钮后,改变v-if的值,让它显示出来,同时拿到这个div的文本内容。
    如果v-if的值是false,直接去获取div的内容是获取不到的,因为此时div还没有被创建出来。
    那么应该在点击按钮后,改变v-if的值为true, div才会被创建,此时再去获取。
    示例代码如下:

     1 <div id="app">
     2     <div id="div" v-if="showDiv">这是一段文本</div>
     3     <button v-on:click="getText">获取div内容</button>
     4 </div>
     5 
     6 <script>
     7     var app = new Vue({
     8         el: "#app",
     9         data: {
    10             showDiv: false
    11         },
    12         methods: {
    13             getText: function() {
    14                 this.showDiv = true;
    15                 var text = document.getElementById("div").innerHTML;
    16                 console.log(text);
    17             }
    18         }
    19     });
    20 </script>

    这段代码并不难理解,但是运行后在控制台会抛出一个错误:Cannot read property 'innerHTML' of null。意思就是获取不到div元素。这里就涉及Vue一个重要的概念:异步更新队列。

    Vue在观察到数据变化时并不是直接更新DOM,从而避免不必要的计算和DOM操作。
    然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。
    所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。

    Vue会根据当前浏览器环境优先使用原生的Promise.thenMutationObserver,如果都不支持,就会采用setTimeout代替。

    知道了Vue异步更新DOM的原理,上面示例的报错也就不难理解了。
    事实上,在执行this.showDiv=true时,div仍然还是没有被创建出来,直到下一个Vue事件循环时,才开始创建。
    $nextTick就是用来知道什么时候DOM更新完成的,所以上面的示例代码需要修改为:

     1 <div id="app">
     2     <div id="div" v-if="showDiv">这是一段文本</div>
     3     <button v-on:click="getText">获取div内容</button>
     4 </div>
     5 
     6 <script>
     7     var app = new Vue({
     8         el: "#app",
     9         data: {
    10             showDiv: false
    11         },
    12         methods: {
    13             getText: function() {
    14                 this.showDiv = true;
    15                 this.$nextTick(function() {
    16                     var text = document.getElementById("div").innerHTML;
    17                     console.log(text);
    18                 });
    19             }
    20         }
    21     });
    22 </script>

    这时再点击按钮,控制台就打印出div的内容“这时一段文本了”。

    理论上,我们应该不用去主动操作DOM,因为Vue的核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如popper.jsswiper等,这些基于原生JavaScript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick

    7.6.2 X-Templates

    如果你没有使用webpack、gulp等工具,试想一下你的组件template的内容很冗长、复杂,如果都在JavaScript里拼接字符串,效率是很低的,因为不能像写HTML那样舒服。
    Vue提供了另一种定义模板的方式,在<script>标签里使用text/x-template类型,并且指定一个id,将这个id赋给template。示例代码如下:

     1 <div id="app">
     2     <my-component></my-component>
     3     <script type="text/x-template" id="my-component">
     4         <div>这是组件的内容</div>
     5     </script>
     6 </div>
     7 
     8 <script>
     9     Vue.component("my-component", {
    10         template: "#my-component"
    11     });
    12     var app = new Vue({
    13         el: "#app"
    14     });
    15 </script>

    <script>标签里,你可以愉快地编写HTML代码,不用考虑执行等问题。

    很多刚接触Vue开发的新手会非常喜欢这个功能,因为用它,再加上组件知识,就可以很轻松地完成交互相对复杂的页面和应用了。如果再配合一些构建工具(gulp)组织好代码结构,开发一些中小型产品是没有问题的。
    不过,Vue的初衷并不是滥用它,因为它将模板和组件的其他定义隔离了。
    在进阶篇里,我们会介绍如何使用webpack来编译.vue的单文件,从而优雅地解决HTML书写的问题。

    7.6.3 手动挂载实例

    我们现在所创建的实例都是通过new Vue()的形式创建出来的。
    在一些非常特殊的情况下,我们需要动态地去创建Vue实例,Vue提供了Vue.extend$mount两个方法来手动挂载一个实例。

    Vue.extend是基础Vue构造器,创建一个“子类”,参数是一个包含组件选项的对象。

    如果Vue实例在实例化时没有收到el选项,它就处于“未挂载”状态,没有关联的DOM元素。
    可以使用$mount()手动地挂载一个未挂载的实例。
    这个方法返回实例自身,因而可以链式调用其他实例方法。
    示例代码如下:

     1 <div id="mount-div"></div>
     2 
     3 <script>
     4     var MyComponent = Vue.extend({
     5         template: "<div>Hello: {{name}}</div>",
     6         data: function() {
     7             return {
     8                 name: "Jack"
     9             };
    10         }
    11     });
    12     
    13     new MyComponent().$mount("#mount-div");
    14 </script>

    运行后,idmount-div的div元素会被替换为组件MyComponenttemplate的内容:

    1 <div>Hello: Jack</div>

    除了这种写法外,以下两种写法也是可以的:

    1 new MyComponent().$mount("#mount-div");
    2 // 同上
    3 new MyComponent({
    4     el: "#mount-div"
    5 });
    6 // 或者,在文档之外渲染并且随后挂载
    7 var component = new MyComponent().$mount();
    8 document.getElementById("mount-div").appendChild(component.$el);

    手动挂载实例(组件)是一种比较极端的高级用法,在业务中几乎用不到,只是开发一些复杂的独立组件时可能会使用,所以只做了解就好。

    7.7 实战:两个常用组件的开发

    本节以组件知识为基础,整合指令、事件等前面两章的内容,开发两个业务中常用的组件,即数字输入框和标签页。

    7.7.1 开发一个数字输入框组件

    数字输入框时对普通输入框的扩展,用来快捷输入一个标准的数字。
    如图7-6所示:

    数字输入框只能输入数字,而且有两个快捷按钮,可以直接减1或加1.
    除此之外,还可以设置初始值、最大值、最小值,在数值改变时,触发一个自定义事件来通知父组件。

    了解了基本需求后,我们先定义目录文件:

    • index.html 入口页
    • input-number.js 数字输入框组件
    • index.js 根实例

    因为该示例是以交互功能为主,所以就不写CSS美化样式了。

    首先写入基本的结构代码,初始化项目。

    index.html:

     1 <!DOCTYPE html>
     2 <html lang="zh">
     3 <head>
     4     <meta charset="UTF-8">
     5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6     <meta http-equiv="X-UA-Compatible" content="ie=edge">
     7     <title>数字输入框组件</title>
     8 </head>
     9 <body>
    10     <div id="app"></div>
    11     
    12     <script src="vue.js"></script>
    13     <script src="input-number.js"></script>
    14     <script src="index.js"></script>
    15 </body>
    16 </html>

    index.js:

    1 var app = new Vue({
    2     el: "#app"
    3 });

    input-number.js:

     1 Vue.component("input-number", {
     2     template: "
     3         <div class='input-number'> 
     4             
     5         </div>",
     6     props: {
     7         max: {
     8             type: Number,
     9             default: Infinity
    10         },
    11         min: {
    12             type: Number,
    13             default: -Infinity
    14         },
    15         value: {
    16             type: Number,
    17             default: 0
    18         }
    19     }
    20 });

    该示例的主角是input-number.js,所有的组件配置都在这里面定义。
    现在template里面定义了组件的根节点,因为是独立组件,所以应该对每个prop进行校验。
    这里面根据需求有最大值、最小值、默认值(也就是绑定值)3个prop,maxmin都是数字类型,默认值是正无限大和负无限大;value也是数字类型,默认值是0。

    接下来,我们先在父组件引入input-number组件,并给它一个默认值5,最大值10,最小值0。

    index.js:

    1 var app = new Vue({
    2     el: "#app",
    3     data: {
    4         value: 5
    5     }
    6 });

    index.html:

    1 <div id="app">
    2     <input-number v-model="value" :max="10" :min="0"></input-number>
    3 </div>

    value是一个关键的绑定至,所以用了v-model,这样既优雅地实现了双向绑定,也让API看起来很合理。
    大多数的表单类组件都应该有一个v-model,比如输入框、单选框、多选框、下拉选择器等。

    剩余的代码量就都聚焦到了input-number.js上。

    我们之前介绍过,Vue组件是单向数据流,所以无法从组件内部直接修改prop:value的值。
    解决办法也介绍过,就是给组件声明一个data,默认引用value的值,然后在组件内部维护这个data

    1 Vue.component("input-number", {
    2     // ...
    3     data: function() {
    4         return {
    5             currentValue: this.value
    6         };
    7     }
    8 });

    这样只解决了初始化时引用父组件value的问题。
    但是如果从父组件修改了value,input-number组件的currentValue也要一起更新。
    为了实现这个功能,我们需要用到一个新的概念,监听(watch)。

    watch选项用来监听某个prop或data的改变,当他们发生变化时,就会触发watch配置的函数,从而完成我们的业务逻辑。
    在本例中,我们要监听两个量:valuecurrentValue
    监听value是要知晓从父组件修改了value,监听currentValue是为了当currentValue改变时,更新value
    相关代码如下:

     1 Vue.component("input-number", {
     2     // ...
     3     data: function() {
     4         return {
     5             currentValue: this.value
     6         };
     7     },
     8     watch: {
     9         currentValue: function(val) {
    10             this.$emit("input", val);
    11             this.$emit("on-change", val);
    12         },
    13         value: function(val) {
    14             this.updateValue(val);
    15         }
    16     },
    17     methods: {
    18         updateValue: function(val) {
    19             if (val > this.max) {
    20                 val = this.max;
    21             }
    22 
    23             if (val < this.min) {
    24                 val = this.min;
    25             }
    26 
    27             this.currentValue = val;
    28         }
    29     },
    30     mounted: function() {
    31         this.updateValue(this.value);
    32     }
    33 });

    从父组件传递过来的value有可能是不符合当前条件的(大于max或小于min),所以在选项methods里写了一个方法updateValue,用来过滤出一个正确的currentValue

    watch监听的数据的回调函数有2个参数可用,第一个是新的智,第二个是旧的值,这里没有太复杂的逻辑,就只用了第一个参数。
    再回调函数里,this是指向当前组件实例的,所以可以直接调用this.updateValue(),因为Vue代理了propsdatacomputedmethods

    监听currentValue的回调里:

    • this.$emit("input", val)是在使用v-model时改变value的;
    • this.$emit("on-change", val)是触发自定义事件on-change,用于告知父组件数字输入框的值有所改变(示例中没有使用该事件〉。

    在生命周期mounted钩子里也调用了updateValue()方法,是因为第一次初始化时,也对value进行了过滤。
    这里也有另一种写法,在data选项返回对象前进行过滤:

     1 Vue.component("input-number", {
     2     // ...
     3     data: function () {
     4         var val = this.value;
     5         
     6         if (val > this.max) {
     7             val = this.max;
     8         }
     9 
    10         if (val < this.min) {
    11             val = this.min;
    12         }
    13         
    14         return {
    15           currentValue: val
    16         };
    17     }
    18 });

    实现的效果是一样的。

    最后剩余的就是补全模板template,内容是一个输入框和两个按钮。
    相关代码如下:

     1 function isValueNumber(value) {
     2     return (/(^-?[0-9]+.(1)d+$)|(^-?[1-9][0-9]*$)|(^-?0{1})/.test(value + ""));
     3 }
     4 
     5 Vue.component("input-number", {
     6     // ...
     7     template: "
     8         <div class='input-number'> 
     9             <input type='text' :value='currentValue' @change='handleChange'/> 
    10             <button @click='handleDown' :disabled='currentValue<=min'>-</button> 
    11             <button @click='handleUp' :disabled='currentValue>=max'>+</button> 
    12         </div>",
    13     methods: {
    14         handleDown: function() {
    15             if (this.currentValue <= this.min) {
    16                 return;
    17             }
    18             this.currentValue -= 1;
    19         },
    20         handleUp: function() {
    21             if (this.currentValue >= this.max) {
    22                 return;
    23             }
    24             this.currentValue += 1;
    25         },
    26         handleChange: function(event) {
    27             var val = event.target.value.trim();
    28             var max = this.max;
    29             var min = this.min;
    30 
    31             if (isValueNumber(val)) {
    32                 val = Number(val);
    33                 this.currentValue = val;
    34                 if (val > max) {
    35                     this.currentValue = max;
    36                 } else if (val < min) {
    37                     this.currentValue = min;
    38                 }
    39             } else {
    40                 event.target.value = this.currentValue;
    41             }
    42         }
    43     }
    44 });

    input绑定了数据currentValue和原生的change事件,在句柄handleChange函数中,判断了当前输入的是否是数字。
    注意,这里绑定的currentValue也是单向数据流,并没有用v-model,所以在输入时,currentValue的值并没有实时改变。
    如果输入的不是数字(比如英文和汉字等〉,就将输入的内容重置为之前的currentValue。
    如果输入的是符合要求的数字,就把输入的值赋给
    currentValue`。

    数字输入框组件的核心逻辑就是这些。
    回顾一下我们设计一个通用组件的思路,首先,在写代码前一定要明确需求,然后规划好API。
    一个Vue组件的API只来自propseventsslots,确定好这3部分的命名、规则,剩下的逻辑即使第一版没有做好,后续也可以迭代完善。但是API如果没有设计好,后续再改对使用者成本就很大了。

    完整的示例代码如下:

    index.html:

     1 <!DOCTYPE html>
     2 <html lang="zh">
     3 <head>
     4     <meta charset="UTF-8">
     5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6     <meta http-equiv="X-UA-Compatible" content="ie=edge">
     7     <title>数字输入框组件</title>
     8 </head>
     9 <body>
    10     <div id="app">
    11         <input-number v-model="value" :max="10" :min="0"></input-number>
    12     </div>
    13     
    14     <script src="vue.js"></script>
    15     <script src="input-number.js"></script>
    16     <script src="index.js"></script>
    17 </body>
    18 </html>

    index.js:

    1 var app = new Vue({
    2     el: "#app",
    3     data: {
    4         value: 5
    5     }
    6 });

    input-number.js:

     1 function isValueNumber(value) {
     2     return (/(^-?[0-9]+.(1)d+$)|(^-?[1-9][0-9]*$)|(^-?0{1})/.test(value + ""));
     3 }
     4 
     5 Vue.component("input-number", {
     6     template: "
     7         <div class='input-number'> 
     8             <input type='text' :value='currentValue' @change='handleChange'/> 
     9             <button @click='handleDown' :disabled='currentValue<=min'>-</button> 
    10             <button @click='handleUp' :disabled='currentValue>=max'>+</button> 
    11         </div>",
    12     props: {
    13         max: {
    14             type: Number,
    15             default: Infinity
    16         },
    17         min: {
    18             type: Number,
    19             default: -Infinity
    20         },
    21         value: {
    22             type: Number,
    23             default: 0
    24         }
    25     },
    26     data: function() {
    27         return {
    28             currentValue: this.value
    29         };
    30     },
    31     watch: {
    32         currentValue: function(val) {
    33             this.$emit("input", val);
    34             this.$emit("on-change", val);
    35         },
    36         value: function(val) {
    37             this.updateValue(val);
    38         }
    39     },
    40     methods: {
    41         handleDown: function() {
    42             if (this.currentValue <= this.min) {
    43                 return;
    44             }
    45             this.currentValue -= 1;
    46         },
    47         handleUp: function() {
    48             if (this.currentValue >= this.max) {
    49                 return;
    50             }
    51             this.currentValue += 1;
    52         },
    53         handleChange: function(event) {
    54             var val = event.target.value.trim();
    55             var max = this.max;
    56             var min = this.min;
    57 
    58             if (isValueNumber(val)) {
    59                 val = Number(val);
    60                 this.currentValue = val;
    61                 if (val > max) {
    62                     this.currentValue = max;
    63                 } else if (val < min) {
    64                     this.currentValue = min;
    65                 }
    66             } else {
    67                 event.target.value = this.currentValue;
    68             }
    69         },
    70         updateValue: function(val) {
    71             if (val > this.max) {
    72                 val = this.max;
    73             }
    74 
    75             if (val < this.min) {
    76                 val = this.min;
    77             }
    78 
    79             this.currentValue = val;
    80         }
    81     },
    82     mounted: function() {
    83         this.updateValue(this.value);
    84     }
    85 });
    • 练习1: 在输入框聚焦时,增加对键盘上下键的支持,相当于加1和减1;
    • 练习2: 增加一个控制步伐的prop:step,比如设置为10,点击加号按钮,一次增加10。

    7.7.2 开发一个标签组件

    本小节将开发一个比较有挑战的组件:标签页组件。
    标签页(即选项卡切换组件)是网页和布局中经常用到的元素,常用于平级区域大块内容的收纳和展现。
    如图7-7所示:

    根据上个示例的经验,我们先分析业务需求,制定出API,这样不至于一上来就无从下手。

    每个标签页的主体内容肯定是由使用组件的父级控制的,所以这部分是一个slot,而且slot的数量决定了标签切换按钮的数量。
    假设我们有3个标签页,点击每个标签按钮时,另外两个标签对应的slot应该被隐藏。
    一般这个时候,比较容易想到的解决办法是,在slot里写3个div,在接收到切换通知时,显示和隐藏相关div。
    这样设计没有问题,只不过提现不出组件的价值来,因为我们还是一些了一些与业务无关的业务逻辑,而这部分逻辑最好组件本身帮忙处理了,我们只用聚焦在slot内容本身,这才是我们业务最相关的。
    这种情况下,我们在定义一个子组件panel,嵌套在标签页组件tabs里,我们的业务代码都放在panel的slot内,而3个panel组件作为整体成为tabs的slot。

    由于tabs和panel两个组件是分离的,但是tabs组件上的标题应该由panel组件来定义,因为slot是卸载panel里,因此在组件初始化(及标签标题动态改变)时,tabs要从panel里获取标题,并保存起来,自己使用。

    确定好了结构,我们先创建所需的文件:

    • index.html 入口页
    • style.css 样式表
    • tabs.js 标签页外层的组件 tabs
    • panel.js 标签页嵌套的组件 panel

    先初始化各个文件:

    index.html:

     1 <!DOCTYPE html>
     2 <html lang="zh">
     3     <head>
     4         <meta charset="UTF-8">
     5         <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6         <meta http-equiv="X-UA-Compatible" content="ie=edge">
     7         <title>标签页组件</title>
     8         <link rel="stylesheet" type="text/css" href="style.css">
     9     </head>
    10     <body>
    11         <div id="app"></div>
    12 
    13         <script src="vue.js"></script>
    14         <script src="panel.js"></script>
    15         <script src="tabs.js"></script>
    16         <script>
    17             var app = new Vue({
    18                 el: "#app"
    19             });
    20         </script>
    21     </body>
    22 </html>

    tabs.js:

     1 Vue.component("tabs", {
     2     template: "
     3         <div class='tabs'> 
     4             <div class='tabs-bar'> 
     5                 <!-- 标签页标题,这里要用v-for --> 
     6             </div> 
     7             <div class='tabs-content'> 
     8                 <!-- 这里的slot就是嵌套的panel --> 
     9                 <slot></slot> 
    10             </div> 
    11         </div>"
    12 });

    panel.js

    1 Vue.component("panel", {
    2     name: "panel",
    3     template: "
    4         <div class='panel'> 
    5             <slot></slot> 
    6         </div>"
    7 });

    panel需要控制标签页内容的显示与隐藏。
    设置一个data:show,并且用v-show指令来控制元素:

     1 Vue.component("panel", {
     2     name: "panel",
     3     template: "
     4         <div class='panel' v-show='show'> 
     5             <slot></slot> 
     6         </div>",
     7     data: function() {
     8         return {
     9             show: true
    10         };
    11     }
    12 });

    当点击到这个panel对应的标签页标题按钮时,此panel的show值设置为true,否则应该是false
    这步操作是在tabs组件完成的,我们稍后再介绍。

    既然要单击对应的标签页标题按钮,那应该有一个唯一的值来标识这个panel,我们可以设置一个prop:name让用户来设置,但它不是必须的,如果使用者不设置,可以默认从0开始自动设置,这不操作仍然是tabs执行的,因为panel本身并不知道自己是第几个。
    除了name,还需要标签页标题的prop:label,tabs组件需要将它显示在标签页标题里。
    这部分代码如下:

    1 props: {
    2     name: {
    3         type: String
    4     },
    5     label: {
    6         type: String,
    7         default: ""
    8     }
    9 }

    上面的prop:label用户是可以动态调整的,所以在panel初始化以及label更新时,都要通知父组件也更新,因为是独立组件,所以不能依赖像bus.js或vuex这样的状态管理办法,我们可以直接通过this.$parent访问tabs组件的实例来调用它的方法更新标题,该方法暂定为updateNav
    注意,在业务中尽可能不要使用$parent来操作父链,这种方法适合于标签页这样的独立组件。
    这部分代码如下:

     1 methods: {
     2     updateNav() {
     3         this.$parent.updateNav();
     4     }
     5 },
     6 watch: {
     7     label() {
     8         this.updateNav();
     9     }
    10 },
    11 mounted() {
    12     this.updateNav();
    13 }

    在生命周期mounted,也就是panel初始化时,调用一遍tabs的updateNav方法。
    同时监听了prop:label,在label更新时,同样调用。

    剩余任务就是完成tabs.js组件。

    首先需要把panel组件设置的标题动态渲染出来,也就是当panel触发tabs的updateNav方法时,更新标题内容。
    我们先看一下这部分的代码:

     1 Vue.component("tabs", {
     2     // ...
     3     data: function() {
     4         return {
     5             // 用于渲染tabs的标题
     6             navList: []
     7         };
     8     },
     9     methods: {
    10         getTabs() {
    11             // 通过遍历子组件,得到所有的panel组件
    12             return this.$children.filter(function(item) {
    13                 return item.$options.name === "panel";
    14             });
    15         },
    16         updateNav() {
    17             this.navList = [];
    18             // 设置对this的引用,在function回调里,this指向的并不是Vue实例
    19             var _this = this;
    20             this.getTabs().forEach(function(panel, index) {
    21                 _this.navList.push({
    22                     label: panel.label,
    23                     name: panel.name || index
    24                 });
    25                 // 如果没有给panel设置name,默认设置它的索引
    26                 if (!panel.name) {
    27                     panel.name = index;
    28                 }
    29                 // 设置当前选中的tab的索引,在后面介绍
    30                 if (index === 0) {
    31                     if (!_this.currentValue) {
    32                         _this.currentValue = panel.name || index;
    33                     }
    34                 }
    35             });
    36         },
    37         updateStatus() {
    38             var tabs = this.getTabs();
    39             var _this = this;
    40             // 显示当前选中的tab对应的panel组件,隐藏没有选中的
    41             tabs.forEach(function(tab) {
    42                 return tab.show = tab.name === _this.currentValue;
    43             });
    44         }
    45     }
    46 });

    getTabs是一个公用的方法,使用this.$children来拿到所有的panel组件实例。

    需要注意的是,在methods里使用了有function回调的方法时(例如遍历数组的方法forEach),在回调内的this不再执行当前的Vue实例,也就是tabs组件本身,所以要在外层设置一个_this=this的局部变量来间接使用this
    如果你熟悉ES2015,也可以直接使用箭头函数=>,我们会在实战篇里介绍相关的用法。

    遍历了每一个panel组件后,把它的labelname提取出来,构成一个Object并添加到数据navList数组里,后面我们会在template里用到它。

    设置完navList数组后,我们调用了updateStatus方法,又将panel组件遍历了以便,不过这时是为了将当前选中的tab对应的panel组件内容显示出来,把没有选中的隐藏掉。
    因为在上一步操作里,我们有可能需要设置currentValue来标识当前选中项的name(在用户没有设置value时,才会自动设置),所以必须要遍历2次才可以。

    拿到navList后,就需要对它用v-for指令把tab的标题渲染出来,并且判断每个tab当前的状态。
    这部分代码如下:

     1 Vue.component("tabs", {
     2     template: "
     3         <div class='tabs'> 
     4             <div class='tabs-bar'> 
     5                 <div :class='tabCls(item)' v-for='(item,index) in navList' @click='handleChange(index)'>{{item.label}}</div> 
     6             </div> 
     7             <div class='tabs-content'> 
     8                 <slot></slot> 
     9             </div> 
    10         </div>",
    11     props: {
    12         // 这里的value是为了可以使用v-model
    13         value: {
    14             type: [String, Number]
    15         }
    16     },
    17     data: function() {
    18         return {
    19             // 因为不能修改value,所以复制一份自己维护
    20             currentValue: this.value,
    21             navList: []
    22         };
    23     },
    24     methods: {
    25         tabCls: function(item) {
    26             return [
    27                 "tabs-tab",
    28                 {
    29                     // 给当前选中的tab加一个class
    30                     "tabs-tab-active": item.name === this.currentValue
    31                 }
    32             ];
    33         },
    34         // 点击tab标题时触发
    35         handleChange: function(index) {
    36             var nav = this.navList[index];
    37             var name = nav.name;
    38             // 改变当前选中的tab,并触发下面的watch
    39             this.currentValue = name;
    40             // 更新value
    41             this.$emit("input", name);
    42             // 触发一个自定义事件,供父级使用
    43             this.$emit("on-click", name);
    44         }
    45     },
    46     watch: {
    47         value: function(val) {
    48             this.currentValue = val;
    49         },
    50         currentValue: function() {
    51             // 在当前选中的tab发生变化时,更新panel的显示状态
    52             this.updateStatus();
    53         }
    54     }
    55 });

    在使用v-for指令循环显示tab标题时,使用v-bind:class指向了一个名为tabClsmethods来动态设置class名称。
    因为计算属性不能接收参数,无法知道当前tab是否是选中的,所以这里我们才用到methods。
    不过要知道,methods是不缓存的,可以回顾关于计算属性的章节。

    点击每个tab标题时,会触发handleChange方法来改变当前选中tab的索引,也就是panel组件的name。
    在watch选项里,我们监听了currentValue,当其发生变化时,触发updateStatus方法来更新panel组件的显示状态。

    以上就是标签页组件的核心代码分解。
    总结一下该示例的技术难点:

    • 使用了组件嵌套的方式,将一系列panel组件作为tabs组件的slot;
    • tabs组件和panel组件通信上,使用了$parent$children的方法访问父链和子链;
    • 定义了prop:value和data:currentValue,使用$emit("input")来实现v-model的用法。

    以下是标签页组件的完整代码:

    index.html:

     1 <!DOCTYPE html>
     2 <html lang="zh">
     3     <head>
     4         <meta charset="UTF-8">
     5         <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6         <meta http-equiv="X-UA-Compatible" content="ie=edge">
     7         <title>标签页组件</title>
     8         <link rel="stylesheet" type="text/css" href="style.css">
     9     </head>
    10     <body>
    11         <div id="app">
    12             <tabs v-model="activeKey">
    13                 <panel label="标签一" name="1">标签一的内容</panel>
    14                 <panel label="标签二" name="2">标签二的内容</panel>
    15                 <panel label="标签三" name="3">标签三的内容</panel>
    16             </tabs>
    17         </div>
    18 
    19         <script src="vue.js"></script>
    20         <script src="panel.js"></script>
    21         <script src="tabs.js"></script>
    22         <script>
    23             var app = new Vue({
    24                 el: "#app",
    25                 data: {
    26                     activeKey: "1"
    27                 }
    28             });
    29         </script>
    30     </body>
    31 </html>

    panel.js:

     1 Vue.component("panel", {
     2     name: "panel",
     3     template: "
     4         <div class='panel' v-show='show'> 
     5             <slot></slot> 
     6         </div>",
     7     props: {
     8         name: {
     9             type: String
    10         },
    11         label: {
    12             type: String,
    13             default: ""
    14         }
    15     },
    16     data: function() {
    17         return {
    18             show: true
    19         };
    20     },
    21     methods: {
    22         updateNav() {
    23             this.$parent.updateNav();
    24         }
    25     },
    26     watch: {
    27         label() {
    28             this.updateNav();
    29         }
    30     },
    31     mounted() {
    32         this.updateNav();
    33     }
    34 });

    tabs.js:

     1 Vue.component("tabs", {
     2     template: "
     3         <div class='tabs'> 
     4             <div class='tabs-bar'> 
     5                 <div :class='tabCls(item)' v-for='(item,index) in navList' @click='handleChange(index)'>{{item.label}}</div> 
     6             </div> 
     7             <div class='tabs-content'> 
     8                 <slot></slot> 
     9             </div> 
    10         </div>",
    11     props: {
    12         value: {
    13             type: [String, Number]
    14         }
    15     },
    16     data: function() {
    17         return {
    18             currentValue: this.value,
    19             navList: []
    20         };
    21     },
    22     methods: {
    23         tabCls: function(item) {
    24             return [
    25                 "tabs-tab",
    26                 {
    27                     "tabs-tab-active": item.name === this.currentValue
    28                 }
    29             ];
    30         },
    31         // 点击tab标题时触发
    32         handleChange: function(index) {
    33             var nav = this.navList[index];
    34             var name = nav.name;
    35             // 改变当前选中的tab,并触发下面的watch
    36             this.currentValue = name;
    37             // 更新value
    38             this.$emit("input", name);
    39             // 触发一个自定义事件,供父级使用
    40             this.$emit("on-click", name);
    41         },
    42         getTabs() {
    43             // 通过遍历子组件,得到所有的panel组件
    44             return this.$children.filter(function(item) {
    45                 return item.$options.name === "panel";
    46             });
    47         },
    48         updateNav() {
    49             this.navList = [];
    50             // 设置对this的引用,在function回调里,this指向的并不是Vue实例
    51             var _this = this;
    52             this.getTabs().forEach(function(panel, index) {
    53                 _this.navList.push({
    54                     label: panel.label,
    55                     name: panel.name || index
    56                 });
    57                 // 如果没有给panel设置name,默认设置它的索引
    58                 if (!panel.name) {
    59                     panel.name = index;
    60                 }
    61                 // 设置当前选中的tab的索引,在后面介绍
    62                 if (index === 0) {
    63                     if (!_this.currentValue) {
    64                         _this.currentValue = panel.name || index;
    65                     }
    66                 }
    67             });
    68         },
    69         updateStatus() {
    70             var tabs = this.getTabs();
    71             var _this = this;
    72             // 显示当前选中的tab对应的panel组件,隐藏没有选中的
    73             tabs.forEach(function(tab) {
    74                 return tab.show = tab.name === _this.currentValue;
    75             });
    76         }
    77     },
    78     watch: {
    79         value: function(val) {
    80             this.currentValue = val;
    81         },
    82         currentValue: function() {
    83             // 在当前选中的tab发生变化时,更新panel的显示状态
    84             this.updateStatus();
    85         }
    86     }
    87 });

    style.css:

     1 [v-cloak]{display:none;}
     2 
     3 .tabs{font-size:14px; color:#657180;}
     4 .tabs-bar:after{
     5     content:""; display:block; 
     6     width:100%; height:1px; 
     7     background:#D7DDE4; margin-top:-1px;
     8 }
     9 .tabs-tab{
    10     display:inline-block; cursor:pointer; position:relative;
    11     padding:4px 16px; margin-right:6px; 
    12     background:#FFF; border:1px solid #D7DDE4;
    13 }
    14 .tabs-tab-active{
    15     cursor:#3399FF; 
    16     border-top:1px solid #3399FF; border-bottom:1px solid #FFF;
    17 }
    18 .tabs-tab-active:before{
    19     content:""; display:block; height:1px; background:#3399FF; 
    20     position:absolute; top:0; left:0; right:0;
    21 }
    22 .tabs-content{padding:8px 0;}

    练习1: 给panel组件新增一个prop:closable的布尔值,来支持是否可以关闭这个panel,如果开启,在tabs的标签标题上会有一个关闭的按钮;

    提示:
    在初始化panel时,我们是在mounted里通知的。
    关闭时,你会用到beforeDestroy。

    练习2: 尝试在切换panel的显示与隐藏时,使用滑动的动画。提示:可以使用CSS3的transform:translateX

  • 相关阅读:
    Redis 常用命令
    docker安装与配置nginx详细过程
    docker安装与配置redis详细过程
    kettle 查询 tinyint 值为 Y,kettle 查询 tinyint 为布尔值
    kettle 乱码问题处理方案
    Vue响应式原理
    ES6学习笔记1
    xlxs转成Unicode编码的json文件
    移动、PC图片拖拽缩放2
    util
  • 原文地址:https://www.cnblogs.com/geeksss/p/10810698.html
Copyright © 2011-2022 走看看