vue学习第四天 组件

Vue组件

课程目标

  1. 了解组件的应用场景

  2. 理解组件的运行机制

  3. 掌握组件的定义和使用

组件是 Vue.js最核心的功能,也是整个框架设计最精彩的地方,当然也是最难掌握的。

1、 组件

1.1 为什么要使用组件

​ 假设有这样一个需求,页面中需要一个input输入框,要求只能输入数字,并且有加减按钮实现加减,好,简单,不就是一个输入框,两个按钮,那还不是有手就行?不过,如果项目中其他地方也需要呢,那就复制粘贴呗,需要几个,复制粘贴几次。虽然这样也解决了问题,但是代码的重复度很高,维护成本也比较高。比如说,项目经理说需求变更了,输入框还要支持最大值最小值限制。那我们需要一个一个去修改,十分麻烦。

组件就是用来提高代码的复用性的,和函数封装是一个道理。

1.2 组件的本质

组件本质上就是一个可复用的Vue实例,且带有一个名字。而我们使用new Vue()创建的vue实例就是最大的组件。组件需要注册后才可以使用。注册有全局注册和局部注册两种方式。全局注册后,任何 Vue 实例中都可以使用。

1.3 全局组件

1.3.1 注册全局组件
//tagName 为组件名,推荐使用小写加减号分割的形式命名。options 为配置对象。
Vue.component(tagName, options)

代码案例:

  // 注册
  Vue.component('v-tab', {
      //我们要封装的HTML结构
    template: '<h1>自定义tab组件!</h1>'
  })
  // 创建根实例
  new Vue({
      //类似于组件中的template
    el: '#app'
  })
1.3.2 使用组件

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

<div id="app">
    <v-tab></v-tab>
</div>
<script>
  // 注册
  Vue.component('v-tab', {
    template: '<h1>自定义tab组件!</h1>'
  })
  // 创建根实例
  new Vue({
    el: '#app'
  })
</script>  

在页面中v-tab标签显示的内容就是组件注册时,template中的内容。也就是说最终渲染的结果为:

<div id="app">
    <h1>自定义tab组件!</h1>
</div>

【注意】:

1、template中的内容必须被一个元素包含,如果直接写成 “自定义tab组件”, 不带h1标签,那么渲染是不成功的。

2、每个组件必须只有一个根元素

以下代码不会正常渲染,Vue 会显示一个错误。

// 注册
  Vue.component('v-tab', {
    template: '
      <h3>{{ title }}</h3>
      <div v-html="content"></div>'
  })

你可以将模板的内容包裹在一个父元素内,来修复这个问题,例如:

Vue.component('v-tab', {
    template: '
    	<div class="blog-post">
    		<h3>{{ title }}</h3>
             <div v-html="content"></div>
         </div>
    '
})
1.3.3 template标签

在创建组件时,如果直接将HTML代码写在template属性中,有这些几个问题:

1.没有代码提示,写起来很吃力

2.HTML代码写在了js中,没有分离,并且不容易维护。

此时我们可以使用template标签来解决这个问题。

template的作用是模板占位符,可帮助我们包裹元素,但是不会被渲染到页面上

我们也可以在template中书写一个css选择器,选中一个template标签。

<template id="blog">
    <div>
    	<h3>{{ title }}</h3>
         <div v-html="content"></div>
    </div>
</template>
Vue.component('v-tab', {
    template: '#blog'
})

1.4 局部组件

全局组件在任何一个Vue实例控制的区域中都可以使用,而局部组件只能在注册的那个组件中使用。

注册局部组件

局部组件需要写在Vue实例中的components属性中

<div id="app">
  <!-- 使用自定义局部组件 -->
  <blog></blog>
</div>
<template id="blog">
    <div>
    	<h3>{{ title }}</h3>
         <div v-html="content"></div>
    </div>
</template>

<script src="js/vue.js"></script>
<script>
  var Child = {
    template: '#blog'
  }

  // 创建Vue实例
  new Vue({
    el: '#app',
    //定义局部组件
    components: {
      // <blog> 将只在父模板可用
      'blog': Child
    }
  })
</script>

2、组件的应用

2.1 组件的data和methods

我们之前说,Vue实例相当于一个大组件,既然如此,那么自定义组件中除了template属性外,可以像Vue实例那样使用data,methods,computed等属性。

但是,组件的data和实例data有点区别,实例data是一个对象。组件的data是一个方法,该方法必须返回一个对象。

<div id="app">
    <abc></abc>
</div>
<template id="tmp">
    <div>
        {{msg}}
    </div>
</template>
Vue.component('abc',{
  template:"#tmp",
  data:function(){
      return {
          msg:"组件的内容"
      }
  }
})
let app = new Vue({
    el:"#app"
})
为什么组件的data是一个函数呢?

我们来看一个案例

<div id="app">
    <abc></abc>
    <abc></abc>
    <abc></abc>
</div>
<template id="tmp">
    <button @click="counter++">
       你点了{{counter}}次 
    </button>
</template>
let data = {
    counter:0;
}
Vue.component('abc',{
  template:"#tmp",
  data:function(){
      return data;
  }
})
let app = new Vue({
    el:"#app"
})

我们使用了三次组件,每个组件都是一个点击按钮,在点击时,按钮上的数字就会加1,原本我们期望的是每个组件上的数据都是独立的,但是实际情况却是,点击任意一个组件,3个组件上的数字都会加1,这是因为这三个组件引用的数据其实是同一个对象。这肯定是不行的,我们需要每个组件中的数据都是独立的,所以data必须是一个函数,在函数中返回一个对象。这种写法就是闭包。如果对闭包不了解的同学,建议先去学习一下。

2.2 动态组件

我们之前已经学过,可以通过v-if指令来条件渲染元素,那么如果我们想要条件渲染组件,是不是也可以用v-if,答案是可以的,因为组件本质上就是一个自定义元素。

<div id="example">
  <button @click="change">切换页面</button>
  <home v-if="isShow"></home>
  <list v-else></list>
</div>
<script>
var home = {template:'<div>我是主页</div>'};
var list = {template:'<div>我是列表页</div>'};
Vue.component("home",home)
Vue.component("list",list)
let app = new Vue({
  el: '#example',
  data:{
    inShow:true,
  },
  methods:{
    change(){
      this.isShow = !this.isShow;
    }
  }
})
</script>

不过,vue给我们提供了一个更为简洁的方法:使用component标签。

格式如下:<component v-bind:is="需要显示的组件名称(可以使用data中的数据)"></component>

以上代码修改如下:

<div id="app">
  <button @click="change">切换页面</button>
  <component v-bind:is="name"></component>
</div>
<script>
var home = {template:'<div>我是主页</div>'};
var list = {template:'<div>我是列表页</div>'};
Vue.component("home",home)
Vue.component("list",list)
let app = new Vue({
  el: '#app',
  data:{
    name:"home"
  },
  methods:{
    change(){
      this.name = this.name==="home"?"list":"home";
    }
  }
})
</script>

component还可以配合 keep-alive 来保存被隐藏组件的状态,来看案例

现在我们在每个组件中添加一个多选框。

var home = {template:'<div>我是主页</div><input type="checkbox">'};
var list = {template:'<div>我是列表页</div><input type="checkbox">'};

当我们使用v-if的方式切换组件时,本质上是在动态的创建销毁元素。所以,组件的状态不会被保存,因为新显示的组件都是全新的。

而如果我们想要使用component标签来实现动态组件,并且保存组件状态,可以使用keep-alive来配合。

<div id="app">
  <button @click="change">切换页面</button>
  <keep-alive>
      <component v-bind:is="name"></component>
  </keep-alive>
</div>

2.3 组件命名注意

在组件模板中,组件名,传参名,方法名等不能使用驼峰命名,只能使用短横线分隔命名法。在组件声明的js区域,可是使用驼峰式命名对应。

3、生命周期

什么是生命周期呢,就好像是人有生老病死一样,vue组件也有从创建到消亡的一系列过程。这个过程就叫做vue的生命周期

vue的生命周期可分为三大阶段,每个阶段都有对应的函数,叫做钩子,又被成为钩子函数:

  • 挂载(初始化相关属性)
    • beforeCreate:vue实例刚被创建出来,此时还没有初始化好data与methods属性。
    • create:data与methods已经初始化,但是还没有编译模板。
    • beforeMount:完成了编译,但是还没有挂载到页面中。
    • mounted:已经将编译好的模板,挂载到页面指定的容器中。
  • 更新(元素或组件的更新操作)
    • beforeUpdate:更新之前执行此函数。data中的状态是最新的,但是页面上显示的数据还是旧值。
    • update:更新完成后执行此函数。data与页面上的数据都是最新的。
  • 销毁(销毁相关属性)
    • beforeDestroy:准备销毁,但是还没有销毁。实例属性和方法仍然可以使用。
    • destroyed:已经销毁,实例不可用。
<div id="app">
    <p>
        {{msg}}
    </p>
    <button @click="update">更新</button>
    <button @click="destroy">销毁</button>
</div>
var myVue=new Vue({
    el:"#app",
    data:{
        msg:"aaa",
    },
    methods:{
        update(){
            this.data = 'bbb'
        },
        destroy(){
            this.$destroy();
        }
    },
    beforeCreate:function(){
        /*
     在调用beforeCreate的时候, 仅仅表示Vue实例刚刚被创建出来
     此时此刻还没有初始化好Vue实例中的数据和方法, 所以此时此刻还不能访问Vue实例中保存的数据和方法
            * */
        console.log(this.msg);
        console.log(this.update);
    },
    created:function(){
        /*
            在调用created的时候, 是我们最早能够访问Vue实例中保存的数据和方法的地方
            * */
        console.log(this.msg);
        console.log(this.update);
    },
    beforeMount:function(){
        /*
            在调用beforeMount的时候, 表示Vue已经编译好了最终模板, 但是还没有将最终的模板渲染到界面上
            * */
        console.log(document.querySelector("p").innerHTML);
        console.log(document.querySelector("p").innerText);
    },
    mounted:function(){
        /*
            在调用mounted的时候, 表示Vue已经完成了模板的渲染, 表示我们已经可以拿到界面上渲染之后的内容了
            * */
        console.log(document.querySelector("p").innerHTML);
    },
   beforeUpdate:function(){
            /*
            在调用beforeUpdate的时候, 表示Vue实例中保存的数据被修改了
            注意点: 在调用beforeUpdate的时候, 数据已经更新了, 但是界面还没有更新
            * */
            console.log("beforeUpdate");
            console.log(this.msg);
            console.log(document.querySelector("p").innerHTML);
        },
        updated:function(){
            /*在调用updated的时候, 表示Vue实例中保存的数据被修改了, 并且界面也同步了修改的数据了* */
            console.log(this.msg);
            console.log(document.querySelector("p").innerHTML);
        },
    beforeDestroy: function(){
            /*
            在调用beforeDestroy的时候, 表示当前组件即将被销毁了
                    beforeDestroy函数是我们最后能够访问到组件数据和方法的钩子函数
            * */
            console.log("beforeDestroy");
            console.log(this.msg);
            console.log(this.say);
        },
        destroyed: function(){
            /*
            在调用destroyed的时候, 表示当前组件已经被销毁了,不要在这个生命周期方法中再去操作组件中数据和方法
            * */
            console.log("destroyed");
        }
})

4、组件通信

组件关系可分为父子关系和非父子关系。

三种通信方式:

  • 父子通信:父组件的数据要传递给子组件
  • 子父通信:子组件中的数据要传递给父组件
  • 非父子组件通信:非父子之间的数据通信

4.1 父子组件

组件之间是可以嵌套的。我们之前说过Vue实例就是一个最大的组件,那么我们在Vue实例中自定义的局部组件就是子组件。

那么除了局部组件之外,该如何定义其他的父子组件呢?

我们可以在父组件中使用components属性来定义子组件。

<div id="app">
    <father></father>
</div>
<template id="father">
	<div>
        <p>我是父组件</p>
        <son></son>
    </div>
</template>
<template id="son">
    <p>
        我是子组件
    </p>
</template>
Vue.component("father",{
    template:"#father",
    components:{
        "son":{
             template:"#son"
        }
    }
})
let app = new Vue({
  el: '#app'
})

4.2 父向子传递数据

组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。但是在子组件中是不能直接访问父组件的数组的,必须通过父组件传递。就像《满城尽带黄金甲》中周润发的那句台词:我给你的,才是你的;我不给,你不能抢。

那么怎么才能让周润发给呢?

1.在父组件中通过v-bind传递数据

格式:<父组件名 v-bind:自定义接收名称=“要传递的数据”></父组件名>

周润发已经给了,那周杰伦需要接才可以,不接也是拿不到数据的。

2.在子组件中通过props属性来接收数据

格式:props:["自定义接收名称"]

来个例子看看

<div id="app">
    <father></father>
</div>
<template id="father">
	<div>
        <p>{{name}}</p>
        <!-- 这里v-bind: 可以使用语法糖,缩写为: -->
        <son :fatherName="name"></son>
    </div>
</template>
<template id="son">
    <p>{{fatherName}}</p>
</template>
Vue.component("father",{
    template:"#father",
    data:function(){
        return {
            name:"周润发"
        }
    }
    components:{
        "son":{
             template:"#son",
    		props:["fatherName"]
        }
    }
})
let app = new Vue({
  el: '#app'
})

【注意】:如果父组件不进行v-bind来绑定,其实子组件也可以接收,但是接收的数据不是动态的,也就是说如果,父组件的数据发送变化时,子组件中从父组件接收过来的数据并不会接着更新。

案例
<div id="app">
    <child :data="movies" type="电影"></child>
    <child :data="books" type="图书"></child>
    <child :data="stus" type="学生"></child>
</div>
<script src="./js/vue.js"></script>
<script>
    const Child = {
        template: `
<div>
{{type}}列表
<ul>
<li v-for="name in data" :key="name">{{name}}</li>
    </ul>
    </div>
`,
        props: ['data', 'type']
    }

    Vue.component('Child', Child)

    const app = new Vue({
        el: "#app",

        data: {
            movies: ["电影1", "电影2", "电影3", "电影4"],
            books: ['书名1', '书名2', '书名3'],
            stus: ['学生1', '学生2', '学生3']
        }
    })

</script>

4.3 父向子传递方法

和数据一样,在子组件也是不能直接使用父组件的方法的。如果子组件要使用,同样需要父组件传递。

1.在父组件中通过v-on传递方法

格式:<父组件名 v-on:自定义接收名称=“要传递的方法”></父组件名>

2.在子组件中自定义一个方法,在方法中通过this.$emit('自定义接收名称')来触发传递过来的方法。

<div id="app">
    <father></father>
</div>
<template id="father">
	<div>
       <button @clikc="say">周润发</button>
        <!-- 这里v-ON: 可以使用语法糖,缩写为@ -->
        <son @fatherFn="say"></son>
    </div>
</template>
<template id="son">
   <button @clikc="sonSay">周杰伦</button>
</template>
Vue.component("father",{
    template:"#father",
  	methods:{
        say(){
            alert("周润发")
        }
    }
    components:{
        "son":{
            template:"#son",
            methods:{
                sonSay(){
                    this.$emit("fatherFn");
                }
            }
        }
    }
})
let app = new Vue({
    el: '#app'
})

如果传递的方法有参数,则可以在this. e m i t 函 数 中 书 写 。 emit函数中书写。 emitemit函数第一个参数为调用的传入的函数名称,从第二个参数开始为传入函数的参数。

Vue.component("father",{
    template:"#father",
  	methods:{
        say(msg){
            alert(msg)
        }
    }
    components:{
        "son":{
            template:"#son",
            methods:{
                sonSay(){
                    this.$emit("fatherFn","周润发");
                }
            }
        }
    }
})
let app = new Vue({
    el: '#app'
})

4.4 子向父传值

props以单向数据流的形式可以很好的完成父子组件的通信

所谓单向数据流:就是数据只能通过 props 由父组件流向子组件,而子组件并不能通过修改 props 传过来的数据修改父组件的相应状态。

如何将子组件中的数组,传递给父组件呢?

既然我们可以将父组件的方法传递给子组件,那么我们就可以在子组件中调用父组件中的方法,
在调用父组件方法的时候传递的参数,也就会被父组件接收。

4.5 非父子组件通信

使用一个空的 Vue 实例作为中央事件总线( bus ),也就是一个中介。

为了更形象地了解它,我们举一个生活中的例子。

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

这两个例子中 你和出租者担任的就是两个跨级的组件,而房产中介就是这个中央事件总线。

<div id="app">
    <tenant></tenant>
    <landlord></landlord>
</div>
<template id="tenant">
    <div>房客:<button @click='renting'>租房</button></div>
</template>
<template id="landlord">
    <div>房东:<button @click='rentOut'>出租</button></div>
</template>
<script src="vue.min.js"></script>
<script>
    let bus = new Vue();

    let tenant = {
        template:"#tenant",
        data(){
            return {
                msg:"爱干净,无不良癖好"
            }
        },
        methods:{
            renting(){
                //触发其他组件的事件
                bus.$emit('landlord-event',this.msg);
            }
        },
        mounted:function(){
            //监听事件
            bus.$on('tenant-event',(msg)=>{
                console.log("房客收到了信息:"+msg);
            })
        }
    }
    let landlord = {
        template:"#landlord",
        data(){
            return {
                msg:"押一付三,领包入住"
            }
        },
        methods:{
            rentOut(){
                //触发房客组件的事件
                bus.$emit('tenant-event',this.msg);
            }
        },
        mounted:function(){
            //监听事件
            bus.$on('landlord-event',(msg)=>{
                console.log("房东收到了信息:"+msg);
            })
        }
    }
    Vue.component('tenant',tenant);
    Vue.component('landlord',landlord);

    let app = new Vue({
        el:"#app"
    });
</script>

5、组件化案例

5.1 评论功能案例

<div id="app">
    <comment-app></comment-app>
  </div>
  <script>
    const bus = new Vue()

    const CommentApp = {
      template: `
        <div class="comment">
          <CommentInput @add:comment="getComment"></CommentInput>
          <CommentList :comments="comments"></CommentList>
        </div>
      `,
      data () {
        return {
          comments: [
            {
              username: '张三',
              content: '评论内容'
            },

            {
              username: '李四',
              content: '评论内容'
            }
          ]
        }
      },

      methods: {
        getComment (comment) {
          this.comments.push(comment)
        }
      },

      created () {
        bus.$on('remove:comment', (index) => {
          this.comments.splice(index, 1)
        })
      }
    } 

    const CommentInput = {
      template: `
        <div class="comment-input">
          <div class="input-group">
            <input v-model="comment.username" class="username" type="text" placeholder="请输入用户名">
          </div>
          <div class="input-group">
            <textarea v-model="comment.content" class="content" name="" placeholder="请输入评论内容"></textarea>
          </div>
          
          <div class="input-group">
            <button class="btn" @click="sendComment">提交</button>
          </div>
        </div>
      `,

      data () {
        return {
          comment: {
            username: "",
            content: ""
          }
        }
      },

      methods: {
        sendComment () {
          if (this.comment.username && this.comment.content) {
            this.$emit('add:comment', {...this.comment})
            this.comment.content = ""
          }
        }
      },

      created () {
        console.log(this)
      }
    }

    const CommentList = {
      template: `
        <div class="comments-list">
          <CommentItem 
            v-for="(comment, index) in comments" :key="index"
            :comment="comment" 
            :index="index">
          </CommentItem>
        </div>
      `,
      props: {
        comments: Array
      }
    }

    const CommentItem = {
      template: `
        <div class="comments-item">
          <div>{{comment.username}}</div>
          <div>{{comment.content}}</div>
          <button class="btn" @click="remove">删除</button>
        </div>
      `,
      props: {
        comment: Object,
        index: Number
      },

      methods: {
        remove () {
          // 触发自定义事件,传递要被删除的元素的index
          bus.$emit('remove:comment', this.index)
        }
      } 
    }

    Vue.component('CommentApp', CommentApp)
    Vue.component('CommentInput', CommentInput)
    Vue.component('CommentList', CommentList)
    Vue.component('CommentItem', CommentItem)


    const app = new Vue({
      el: "#app"
    })

  </script>
  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值