6.组件 ★

1.组件(Component)是Vue.js最核心的功能。
2.组件的注册有全局注册和局部注册两种方式。全局注册后,任何Vue示例都可以使用。
全局注册示例:

Vue.component('my-component',{
  //选项
})

my-component 就是注册的组件自定义标签名称,推荐使用小写加减号分割的形式命名。
要在富实例中使用这个组件,必须要在示例创建前注册,之后就可以用<my-component></my-component>的形式来使用组件了,示例:

<div id="app">
    <my-component></my-component>
</div>
<script>
    Vue.component('my-component',{
        template:'<div>这里是组件的内容</div>'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

template的DOM结构必须被一个元素饱含,如果直接写内容,不带<div></div> 是无法渲染的。
在Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。组件中可以使用components选项来注册组件,使组件可以嵌套。示例:

<div id="app">
    <my-component></my-component>
</div>
<script>
    var child = {
        template:'<div>这里是组件的内容</div>'
    }
    var app = new Vue({
        el:'#app',
        components:{
            'my-component':child
        }
    })
</script>

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

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>
<script>
    Vue.component('my-component',{
        template:'<div>这里是组件的内容</div>'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

tbody在渲染时,会被替换为组件的内容。常见的限制元素还有<ul>、<ol>、<select>。
如果使用的是字符串模版,是不受限制的,比如.vue单文件用法等。
4.除了template选项外,组件中还可以像Vue实例哪样使用其他的选项,比如data、computed、methods等,但是在使用data时,和实例稍有区别,data必须是函数,然后将数据return 出去,例如:

<div id="app">
    <my-component></my-component>
</div>
<script>
    Vue.component('my-component',{
        template:'<div>{{message}}</div>',
        data:function(){
            return {
                message:'组件内容'
            }
        }
    })
    var app = new Vue({
        el:'#app'
    })
</script>

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

<div id="app">
    <my-component></my-component>
    <my-component></my-component>
    <my-component></my-component>
</div>
<script>
    var data = {
        counter:0
    }
    Vue.component('my-component',{
        template:'<button @click="counter++">{{counter}}</button>',
        data:function(){
            return data;
        }
    })
    var app = new Vue({
        el:'#app'
    })
</script>

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

<div id="app">
    <my-component></my-component>
    <my-component></my-component>
    <my-component></my-component>
</div>
<script>
    Vue.component('my-component',{
        template:'<button @click="counter++">{{counter}}</button>',
        data:function(){
            return {
                counter:0
            };
        }
    })
    var app = new Vue({
        el:'#app'
    })
</script>

这样,点击3个按钮就互补影响了,达到了复用的目的。
5.组件不仅仅是要把模版的内容进行复用,更重要的是组件间要进行通信。通常,父组件的模版中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过props来实现的。
6.在组件中,使用选项props来声明须要从父级接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。
7.我们构造一个数组,接受一个来自父级的数据message,并把它在组件模版中渲染,示例代码:

<div id="app">
    <my-component message="来自父组件的数据"></my-component>
</div>
<script>
    Vue.component('my-component',{
        props:['message'],
        template:'<div>{{message}}</div>'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模版template及计算属性computed和方法methods中使用。上例的数据message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称,如果要传递多个数据,在props数组中添加项即可。
8.由于HTML特性不区分大小写,当使用DOM模版时,驼峰命名(camelCase)的props名称要转为短横分割命名(kebab-case),如:

<div id="app">
    <my-component warning-text="提示信息"></my-component>
</div>
<script>
    Vue.component('my-component',{
        props:['warningText'],
        template:'<div>{{warningText}}</div>'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

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

<div id="app">
    <input type="text" v-model="parentMessage">
    <my-component :message="parentMessage"></my-component>
</div>
<script>
    Vue.component('my-component',{
        props:['message'],
        template:'<div>{{message}}</div>'
    })
    var app = new Vue({
        el:'#app',
        data:{
            parentMessage:''
        }
    })
</script>

这里用v-model绑定了父级的数据parentMessage,当通过输入框任意输入时,子组件接收到的props “message” 也会实时响应,并更新组件模版。
注意,如果要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅仅是字符串,尝试下面的示例来对比:

<div id="app">
    <my-component message="[1,2,3]"></my-component>
    <my-component :message="[1,2,3]"></my-component>
</div>
<script>
    Vue.component('my-component',{
        props:['message'],
        template:'<div>{{message.length}}</div>'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

同一个组件使用了两次,区别是第二次使用的是v-bind。渲染后的结果是,第一个是7,第二个是才是数组的长度3。
10.Vue2.x通过props传递数据是单向的。也就是父组件数据变化时会传递给子组件,但是反过来步行。
11.业务中经常会遇到两种需要改变prop的情况,一种是父组件传递初始值近来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件data内再声明一个数据,引用父组件的prop,示例代码:

<div id="app">
    <my-component :init-count="1"></my-component>
</div>
<script>
    Vue.component('my-component',{
        props:['initCount'],
        template:'<div>{{count}}</div>',
        data:function(){
            return {
                count:this.initCount
            }
        }
    })
    var app = new Vue({
        el:'#app'
    })
</script>

组件中声明了数据count,它在组件初始化时会获取来自父组件的initCount,之后就与之无关了,只用维护count,这样就可以避免直接操作initCount。
另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了,例如:

<div id="app">
    <my-component :width="100"></my-component>
</div>
<script>
    Vue.component('my-component',{
        props:['width'],
        template:'<div :style="style">组件内容</div>',  
        computed:{
            style:function(){
                return {
                    width: this.width + 'px',
                    border: '1px solid black'
                }
            }
        }
    })
    var app = new Vue({
        el:'#app'
    })
</script>

因为CSS传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。
注意,在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变是会影响父组件的。
12.上面介绍的props选项的值都是一个数组,除了数组外,还可以是对象,当prop需要验证时,就需要对象写法。
一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告,以下是几个prop的示例:

 Vue.component('my-component',{
        props:{
            //必须是数字类型
            propA:Number,
            //必须是字符串或数字类型
            propB:[String,Number],
            //布尔值,如果没有定义,默认值就是true
            propC:{
                type:Boolean,
                default:true
            },
            //数字,而且是必传
            propD:{
                type:Number,
                required:true
            },
            //如果是数组或对象,默认值必须是一个函数来返回
            propE:{
                type:Array,
                default:function(){
                    return [];
                }
            },
            //自定义一个验证函数
            propF:{
                validator:function(value){
                    return value > 10;
                }
            }
        }

验证的type类型可以是:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function
    type也可以是一个自定义构造器,使用instanceof检测。
    当prop验证失败时,在开发版本下回在控制台抛出一条警告。
    13.Vue组件的通信场景:父子组件通信、兄弟组件通信、跨级组件通信。
    14.当子组件需要向父组件传递数据时,就要用到自定义事件。v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。
    15.与JavaScript的设计模式——观察者模式中的dispatchEvent和addEventListener这两个方法类似。Vue组件也有一套模式,子组件用$emit()来触发时间,父组件用$on()来监听子组件的事件。
    父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件,示例代码:
<div id="app">
    <p>总数:{{total}}</p>
    <my-component 
        @increase="handleGetTotal"
        @reduce="handleGetTotal"></my-component>
</div>
<script>
    Vue.component('my-component',{
        template:'\
        <div>\
            <button @click="handleIncrease">+1</button>\
            <button @click="handleReduce">-1</button>\
        </div>',  
        data:function(){
            return {
                counter:0
            }
        },
        methods:{
            handleIncrease:function(){
                this.counter ++;
                this.$emit('increase',this.counter);
            },
            handleReduce:function(){
                this.counter --;
                this.$emit('reduce',this.counter);
            }
        }
    })
    var app = new Vue({
        el:'#app',
        data:{
            total:0
        },
        methods:{
            handleGetTotal:function(total){
                this.total = total;
            }
        }
    })
</script>

上面的例子中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data "counter"后,通过$emit()再把它传给父组件,父组件用v-on:increase 和 v-on:reduce (示例使用的是语法糖)。$emit方法的第一个参数是自定义事件的名称,例如示例的increase和reduce后面的参数都是要传递的数据,可以不填或填写多个。
16.除了用 v-on 在组件上监听自定义事件外,也可以监听DOM事件,这时可以用.native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素,示例如下:

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

17.使用 v-model
Vue2.x可以在自定义组件上使用 v-model指令,示例:

<div id="app">
    <p>总数:{{total}}</p>
    <my-component v-model="total"></my-component>
</div>
<script>
    Vue.component('my-component',{
        template:'<button @click="handleClick">+1</button>',  
        data:function(){
            return {
                counter:0
            }
        },
        methods:{
            handleClick:function(){
                this.counter ++;
                this.$emit('input',this.counter);
            }
        }
    })
    var app = new Vue({
        el:'#app',
        data:{
            total:0
        }
    })
</script>

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

<div id="app">
    <p>总数:{{total}}</p>
    <my-component @input="handleGetTotal"></my-component>
</div>
<script>
    Vue.component('my-component',{
        template:'<button @click="handleClick">+1</button>',  
        data:function(){
            return {
                counter:0
            }
        },
        methods:{
            handleClick:function(){
                this.counter ++;
                this.$emit('input',this.counter);
            }
        }
    })
    var app = new Vue({
        el:'#app',
        data:{
            total:0
        },
        methods:{
            handleGetTotal:function(total){
                this.total = total;
            }
        }
    })
</script>

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

<div id="app">
    <p>总数:{{total}}</p>
    <my-component v-model="total"></my-component>
    <button @click="handleReduce">-1</button>
</div>
<script>
    Vue.component('my-component',{
        props:['value'],
        template:'<input :value="value" @input="updateValue">',  
        data:function(){
            return {
                counter:0
            }
        },
        methods:{
            updateValue:function(event){
                this.$emit('input',event.target.value);
            }
        }
    })
    var app = new Vue({
        el:'#app',
        data:{
            total:0
        },
        methods:{
            handleReduce:function(){
                this.total --;
            }
        }
    })
</script>

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

  • 接收一个value属性
  • 在有新的value时触发input事件
    18.在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。示例代码:
<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
<script>
    var bus = new Vue();

    Vue.component('component-a',{
        template:'<button @click="handleEvent">传递事件</button>',  
        methods:{
            handleEvent:function(){
                bus.$emit('on-message','来自组件component-a的内容');
            }
        }
    })
    var app = new Vue({
        el:'#app',
        data:{
            message:''
        },
        mounted:function(){
            var _this = this;
            //在实例初始化时,监听来自bus实例的事件
            bus.$on('on-message',function(msg){
                _this.message = msg;
            })
        }
    })
</script>

首先创建了一个名为bus的空Vue实例。然后全局定义了组件component-a;最后创建Vue实例app,在app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus的事件on-message,而在组件component-a中,点击按钮会通过bus把事件on-message发出去,此时app就会接收到来自bus的事件,进而在回调里完成自己的业务逻辑。
这种方法巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。如果深入使用,可以扩展bus实例,给它添加data、methods、computed等选项,这些都是可以公用的,在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,还有用户的授权token等。只需在初始化时让bus获取一次,任何时间,任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用。项目比较大时,可以选择更好的状态惯例解决方案vuex。
19.除了中央事件总线bus外,还有两种方法可以实现组件间通信:父链 和子组件索引。
20.在子组件中,使用this.$parent 可以直接访问该组件的父实例或组件,父组件也可以通过this.$children 访问它的所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。示例:

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
<script>
    var bus = new Vue();

    Vue.component('component-a',{
        template:'<button @click="handleEvent">通过父链直接修改数据</button>',  
        methods:{
            handleEvent:function(){
                //访问到父链后,可以做任何操作,比如直接修改数据
                this.$parent.message = '来自组件component-a的内容';
            }
        }
    })
    var app = new Vue({
        el:'#app',
        data:{
            message:''
        }
    })
</script>

尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过props和$emit来通信。
21.当子组件较多时,通过this.$children 来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。
Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称,示例:

<div id="app">
    <button @click="handleRef">通过ref获取子组件实例</button>
    <component-a ref="comA"></component-a>
</div>
<script>
    Vue.component('component-a',{
        template:'<div>子组件</div>',  
        data:function(){
            return {
                message:'子组件内容'
            }
        }
    })
    var app = new Vue({
        el:'#app',
        methods:{
            handleRef:function(){
                //通过$refs来访问指定的实例
                var msg = this.$refs.comA.message;
                console.log(msg);
            }
        }
    })
</script>

在父组件模版中,子组件标签上使用ref指定一个名称,并在父组件内通过this.$refs 来访问指定名称的子组件。$refs只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs。
22.当需要让组件组合使用,混合父组件的内容和子组件的模板时,就会用到slot,这个过程叫作内容分发(transclusion)。以<app>为例,它有两个特点:

  • <app>组件不知道它的挂载点会有什么内容。挂载点是由<app>的父组件决定的。
  • <app>组件很可能有它自己的模板。
    props传递数据、events触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。
  1. slot。如下图,一个比较常规的网站布局:


    image.png

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

<app>
    <menu-main></menu-main>
    <menu-sub></menu-sub>
    <div class="container">
        <menu-left></menu-left>
        <container></container>
    </div>
</app>

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

  • <app>组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
  • <app>组件很可能有它自己的模板
    props传递数据、events触发事件 和 slot 内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。
    编译的作用域。比如父组件有如下模板:
<child-component>
  {{ message }}
</child-component>

这里的message就是一个slot,但是它绑定的是父组件的数据,而不是组件<child-component>的数据。
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。例如下面的示例:

<div id="app">
    <child-component v-show="showChild"></child-component>
</div>
<script>
    Vue.component('child-component',{
        template:'<div>子组件</div>'
    });
    var app = new Vue({
        el:'#app',
        data:{
            showChild:true
        }
    })
</script>

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

<div id="app">
    <child-component></child-component>
</div>
<script>
    Vue.component('child-component',{
        template:'<div v-show="showChild">子组件</div>',
        data:function(){
            return {
                showChild:true
            }
        }
    });
    var app = new Vue({
        el:'#app'
    })
</script>

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

<div id="app">
    <child-component>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template:'\
        <div>\
            <slot>\
                <p>如果父组件没有插入内容,我将作为默认出现</p>\
            </solt>\
        </div>'
    });
    var app = new Vue({
        el:'#app'
    })
</script>

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

<div id="app">
    <div>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </div>
</div>

注意,子组件<slot>内的备用内容,它的作用域是子组件本身。
具名slot。给<slot>元素指定一个name后可以分发多个内容,具名slot可以与单个slot共存,例如:

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template:'\
        <div class="container">\
            <div class="header">\
                <slot name="header"></slot>\
            </div>\
            <div class="main">\
                <slot></slot>\
            </div>\
            <div class="footer">\
                <slot name="footer"></slot>\
            </div>\
        </div>'
    });
    var app = new Vue({
        el:'#app'
    })
</script>

子组件内声明了3个<slot>元素,其中在<div class="main">内的<slot>没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。如果没有指定默认的匿名slot,父组件内多余的内容片段都将被抛弃。上例最终渲染结果为:

<div id="app">
  <div class="container">
    <div class="header">
      <h2>标题</h2>
    </div> 
    <div class="main"> 
      <p>正文内容</p> 
      <p>更多的正文内容</p> 
    </div> 
    <div class="footer">
      <div>底部信息</div>
    </div>
  </div>
</div>

在组合使用组件时,内容分发API至关重要
作用域插槽。是一种特殊的slot。使用一个可以复用的模板替换已渲染元素。概念比较难理解,简单示例:

<div id="app">
    <child-component>
        <template scope="props">
            <p>来自父组件的内容</p>
            <p>{{props.msg}}</p>
        </template>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template:'\
        <div class="container">\
                <slot msg="来自子组件的内容"></slot>\
        </div>'
    });
    var app = new Vue({
        el:'#app'
    })
</script>

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

<div id="app">
  <div class="container">
    <p>来自父组件的内容</p> 
    <p>来自子组件的内容</p>
  </div>
</div>

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

<div id="app">
    <my-list :books="books">
        <!--作用域插槽也可以是具名的slot-->
        <template slot="book" scope="props">
            <li>{{ props.bookName }}</li>
        </template>
    </my-list>
</div>
<script>
    Vue.component('my-list',{
        props:{
            books:{
                type:Array,
                default:function(){
                    return [];
                }
            }
        },
        template:'\
            <ul>\
                <slot name="book"\
                v-for="book in books"\
                :book-name="book.name">\
                <!--这里也可以写默认slot内容-->\
            </slot>\
            </ul>\
        '
    });
    var app = new Vue({
        el:'#app',
        data:{
            books:[
                { name:'《Vue.js实战》'},
                { name:'《JavaScript 语言精粹》'},
                { name:'《JavaScript 高级程序设计》'}
            ]
        }
    })
</script>

子组件my-list接收一个来自父级的prop数组books,并且将它在name为book的slot上使用v-for指令循环,同时暴露一个变量bookName。
作用域插槽的使用场景就是既可以复用子组件的slot,又可以使slot内容不一致。如果上例还在其他组件内使用,<li>的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)从子组件内获取。
访问slot。Vue.js 2.x提供了用来访问被slot分发的内容的方法 $slots,示例:

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template:'\
        <div class="container">\
            <div class="header">\
                <slot name="header"></slot>\
            </div>\
            <div class="main">\
                <slot></slot>\
            </div>\
            <div class="footer">\
                <slot name="footer"></slot>\
            </div>\
        </div>',
        mounted:function(){
            var header = this.$slots.header;
            var main = this.$slots.default;
            var footer = this.$slots.footer;
            console.log(footer)
            console.log(footer[0].elm.innerHTML)
        }
    });
    var app = new Vue({
        el:'#app'
    })
</script>

通过slots可以访问某个具名slot,this.slots.default包括了所有没有被包含在具名slot中的节点。$slots在业务中几乎用不到,在用render函数创建组件时会比较有用,但主要还是用于独立组件开发中。

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

<div id="app">
    <child-component :count="1"></child-component>
</div>
<script>
    Vue.component('child-component',{
        name:'child-component',
        props:{
            count:{
                type:Number,
                default:1
            }
        },
        template:'\
        <div class="child">\
            <child-component\
                :count="count + 1"\
                v-if="count < 3"></child-component>\
        </div>'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

设置name后,在组件模板内就可以递归使用了,不过需要注意,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded。
组件递归使用可以用来开发一些具有未知层级关系的独立组件,比如级联选择器和树形控件等。
25.内联模板。组件的模板一般都是在template选项内定义的。Vue提供了一个内联模板的功能,在使用组件时,给组件标签使用inline-template特性,组件就会把它的内容当作模板,而不是把它当内容分发,这让模板更灵活。示例:

<div id="app">
   <child-component inline-template>
        <div>
            <h2>在父组件中定义子组件的模板</h2>
            <p>{{message}}</p>
            <p>{{msg}}</p>
        </div>
   </child-component>
</div>
<script>
    Vue.component('child-component',{
        data:function(){
            return {
                msg:'在子组件声明的数据'
            }
        }
    });
    var app = new Vue({
        el:'#app',
        data:{
            message:'在父组件声明的数据'
        }
    })
</script>

渲染后的结果为:

<div id="app">
  <div>
    <h2>在父组件中定义子组件的模板</h2> 
    <p>在父组件声明的数据</p> 
    <p>在子组件声明的数据</p>
  </div>
</div>

在父组件中声明的数据message和子组件中声明的数据msg,两个都可以渲染(如果同名,优先使用子组件的数据)。这反而是内联模板的缺点,就是作用域比较难理解,如果不是非常特殊的场景,建议不要轻易使用内联模板。
26.动态组件。Vue.js提供了一个特殊元素<component>用来动态地挂在不同的组件,使用is特性来选择要挂载的组件。示例:

<div id="app">
   <component :is="currentView"></component>
   <button @click="handleChangeView('A')">切换到A</button>
   <button @click="handleChangeView('B')">切换到B</button>
   <button @click="handleChangeView('C')">切换到C</button>
</div>
<script>
    
    var app = new Vue({
        el:'#app',
        components:{
            comA:{
                template:'<div>组件A</div>'
            },
            comB:{
                template:'<div>组件B</div>'
            },
            comC:{
                template:'<div>组件C</div>'
            }
        },
        data:{
            currentView:'comA'
        },
        methods:{
            handleChangeView:function(component){
                this.currentView = 'com' + component;
            }
        }
    })
</script>

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

<div id="app">
  <component :is="currentView"></component>
</div>
<script>
  var Home = {
    template:'<p>Welcome home ! </p>'
  }
var app = new Vue({
  el:'#app',
  data:{
    currentView: Home
  }
})
</script>

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

<div id="app">
    <child-component></child-component>
</div>
<script>
    Vue.component('child-component',function(resolve,reject){
        window.setTimeout(function(){
            resolve({
                template:'<div>我是异步渲染的</div>'
            });
        },2000);
    });
    var app = new Vue({
        el:'#app'
    })
</script>

工厂函数接收一个resolve回调,在收到从服务器下载的组件定义时调用。也可以调用reject(reason)指示加载失败。这里setTimeout 指示为了演示异步,具体的下载逻辑可以自己决定,比如把组件配置携程一个对象配置,通过Ajax来请求,然后调用resolve传入配置选项。
28.$nextTick。场景描述:有一个div,默认用v-if将它隐藏,点击一个按钮后,改变v-if的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是false,直接去获取div的内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if的值为true,div才会被创建,此时再去获取,示例代码如下:

<div id="app">
    <div id="div" v-if="showDiv">这是一段文本</div>
    <button @click="getText">获取div内容</button>
</div>
<script>
    var app = new Vue({
        el:'#app',
        data:{
            showDiv:false
        },
        methods:{
            getText:function(){
                this.showDiv = true;
                var text = document.getElementById('div').innerHTML;
                console.log(text)
            }
        }
    })
</script>

这段代码并不难理解,但是运行后在控制台会抛出一个错误:cannot read property 'innerHTML' of null, 意思是获取不到div元素。这就涉及Vue一个重要概念:异步更新队列
Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变是数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。
Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。
知道了Vue异步更新DOM的原理,上面的示例的报错也就不难理解了。
事实上,在执行this.showDiv=true;时,div仍然还是没有被创建出来,知道下一个Vue事件循环时,才开始创建。$nextTick就是用来知道什么时候DOM更新完成的,上面的示例改为:

<div id="app">
    <div id="div" v-if="showDiv">这是一段文本</div>
    <button @click="getText">获取div内容</button>
</div>
<script>
    var app = new Vue({
        el:'#app',
        data:{
            showDiv:false
        },
        methods:{
            getText:function(){
                this.showDiv = true;
                this.$nextTick(function(){
                    var text = document.getElementById('div').innerHTML;
                    console.log(text)
                })
            }
        }
    })
</script>

这时在点击按钮,控制台就打印出div的内容“这是一段文本”了。
理论上,我们应该不用去主动操作DOM,因为Vue的核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如popper.js、swiper等,这些基于原生javascript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick。

  1. X-Template。如果你没有使用webpack、gulp等工具,试想一下你的组件template的内容很冗长、复杂,如果都在JavaScript里拼接字符串,效率很低。Vue提供了另一种定义模板的方式,在<script>标签里使用 text/x-template类型,并且指定一个id,将这个id赋给template。示例:
<div id="app">
    <my-component></my-component>
    <script type="text/x-template" id="my-component">
        <div> 这是组件的内容 </div>
    </script>
</div>
<script>
    Vue.component('my-component',{
        template:'#my-component'
    })
    var app = new Vue({
        el:'#app'
    })
</script>

在<script>标签里,可以愉快地写HTML代码,不用考虑换行等问题。
Vue的初衷并不是滥用它,因为它将模板和组件的其他定义隔离了。
我们后续会使用webpack来编译.vue的单文件,从而优雅地解决HTML书写的问题。
30.手动挂在实例。我们现在所创建的实例都是通过new Vue()的形式创建出来的。在一些非常特殊的情况下,我们需要动态地去创建Vue实例,Vue提供了Vue.extend 和 mount 两个方法来手动挂载一个实例。 Vue.extend 是基础Vue 构造器,创建一个“子类”,参数是一个包含组件选项的对象。 如果Vue实例在实例化时没有收到el选项,它就处于“未挂载”状态,没有关联的DOM元素。可以使用mount()手动地挂载一个未挂载的实例。
这个方法返回实例自身,因而可以链式调用其他实例方法。示例:

<div id="mount-div">
</div>
<script>
    var MyComponent = Vue.extend({
        template:'<div>Hello:{{name}}</div>',
        data:function(){
            return {
                name:'Aresn'
            }
        }
    });
    new MyComponent().$mount('#mount-div')
</script>

运行后,id为mount-div的div元素会被替换为组件MyComponent的template的内容:

<div>Hello:Aresn</div>

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

new MyComponent({
  el:'#mount-div'
});
//或者 ,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount();
document.getElementById('mount-div').appendChild(component.$el);

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345