Vue复习2.0——组件化开发详解

本文深入探讨Vue的组件化开发,涵盖组件基础、全局与局部组件、数据管理、父子及非父子组件通信、插槽使用、动态组件以及边界情况处理。详细解析了组件的生命周期、props、事件绑定、$refs、状态管理和组件交互策略。
摘要由CSDN通过智能技术生成

Vue复习1.0主要为Vue的基础语法、计算属性、概念、指令。详见Vue复习1.0
Vue复习2.0为组件化开发,此文篇幅较长,请耐心阅读,相信会让你对组件化开发有更深的理解和更好的应用
此文所有代码已上传至GitHub:A-sketch123 -VueComponent

组件化开发

1. 概念

组件化开发即尽可能的将页面拆分成一个个可复用的组件,这样课时我们的代码更加方便组织和管理,并且扩展性也更强。

我们可以在一个 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用,若组件没有挂载在new Vue的实例下则不会被渲染出来


2. 全局组件与局部组件

全局组件:
用过Vue.component(组件名,{方法})来创建的组件是全局的注册的,也就是说它们在注册之后可以在任何新创建的 Vue 根实例 (new Vue) 模板中使用

全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

局部组件:
在new Vue下用components属性,只有在挂载的那个new Vue实例下面才可以使用

<body>
    <div id="app">
      <global-cpn></global-cpn>
      <local-cpn></local-cpn>
    </div>

    <h2>我是分割两个Vue实例的</h2>

    <div id="app2">
      <global-cpn></global-cpn>
      <local-cpn></local-cpn>
    </div>

	//定义全局组件的模板
    <template id="globalCpn">
      <div>
        <p>我是全局组件,在多个Vue的实例下面都可以使用</p>
      </div>
    </template>
    <script>
      // 1.注册一个全局组件,注意全局组件必须在vue实例创建前定义否则无法显示且会报错
      Vue.component('global-cpn', {
        template: '#globalCpn',
      });
      //将局部组件的模板定义在变量里使代码更整洁规范
  	 var localCpn = {
        template: `
   	<div>
        <p>我是局部组件,只有在挂载的那个new Vue实例下面才可以使用</p>
    </div>
  ` };
  
      const app = new Vue({
        el: '#app',
        //在app下注册局部组件
        components: {
          'local-cpn': localCpn,
        },
      });
      const app2 = new Vue({
        el: '#app2',
      });
    </script>
  </body>

效果如下:在app2实例下局部组件无法渲染
在这里插入图片描述

组件的data必须是函数的原因

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝

data属性必须是一个函数,而且这个函数返回一个对象,对象内部保存着数据。这样每个组件实例才有自己的作用域,不同组件之间才能相互独立,不会相互影响
若组件的data是对象,当多个组件共用一个一个数据源时,一处数据改变所有的组件数据都会随着改变,所以要利用函数return 返回对象的拷贝,让每个实例都有自己的作用域。

data(){
    return{
        count:0
    }
},

template模板报错问题

在template模板中需要将所有 html 包装到一个元素(div)中,否则可能会报以下错误
eg:

<template id="cpn">
  <div>我是子组件</div>
  <input type="text">
</template>

在这里插入图片描述
正确应为:

<template id="cpn">
  <div>
  我是子组件
  <input type="text">
  </div>
</template>

3. 模块系统中使用组件

在vue-cli项目中通常会建一个 components 文件存放组件

<script>
//导入组件所在的文件
import Footer from "./components/Footer.vue";

export default {
  components: {
    Footer,
  },
};
</script>

4. 父子组件

组件通信——父传子

父传子用props,子组件用props属性接收父组件传来的数据,父组件用v-bind()来动态传递给子组件数据
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称
方式二:对象,props的值用对象的形式可以设置默认值、类型等属性

props的大小写:
HTML 中的 attribute 名是大小写不敏感的,这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:(此规则也适用于组件命名,驼峰命名的组件在HTML中使用时也得用短横线连接)


使用字符串数组:

<div id="demo">
  <son-cpn :father-title="title"></son-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
  var sonCpn = {
    template: ` <h4>{{fatherTitle}}</h4> `,
    //props使用字符串数组
    props: ['fatherTitle'],
  };
  const demo = new Vue({
    el: '#demo',
    data: {      
      title: '我是父组件传给子组件的值',
    },
    components: {
      'son-cpn': sonCpn,
    },
  });
</script>

使用对象形式

<div id="demo">
  <son-cpn :names="name"></son-cpn>
</div>

<template id="cpn">
  <div>
    <h4>{{mes}}</h4>
    <ul>
      <li v-for="item in names">{{item}}</li>
    </ul>
  </div>
</template>
<script>
  // props使用对象方式
  var sonCpn = {
    template: '#cpn',
    props: {
      mes: {
        type: String,
        default: '没有传值给我,我是默认值',
      },
      names: {
        type: Array,
         类型是对象或者数组时, 默认值必须是一个函数
        default() {
          return ['张三', '李四', 'F'];
        },
        //必传值
        required: true,
      },
    },
  };
  const demo = new Vue({
    el: '#demo',
    data: {
      name: ['A', 'B', 'C'],
    },
    components: {
      'son-cpn': sonCpn,
    },
  });
</script>

运行结果mes为默认值,names为父组件传过来的值在这里插入图片描述


组件通信——子传父

子组件向父组件传值用自定义事件:
在子组件中,通过$emit()来触发事件
在父组件中,通过v-on来监听子组件事件

this.$emit(‘事件名’)中事件名不可用驼峰命名法(有大小写的),因为 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),如v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到,因为触发的事件名需要完全匹配监听这个事件所用的名称
因此,我们推荐你始终使用 kebab-case 的事件名

<div id="demo">
    <!-- 父组件监听自定义事件 -->
    <son-cpn @btn-click = 'sonclick'></son-cpn>
    <h4>点击次数:{{total}}</h4>
</div>   
<script src="../js/vue.js"></script>
<script>
    var cpn = {
        template:` <button @click="btnClick()">+</button> `,
        data(){
            return {
                count :0
            }
        },
        methods:{
            btnClick(){
                this.count++
                //子组件发送自定义事件
                this.$emit('btn-click',this.count )
            }
        }

    }
    const demo = new Vue({
        el:'#demo',
        data:{
            total:0
        },
        components:{
            'son-cpn':cpn
        },
        methods: {
            sonclick(count){
                this.total = count
            }
        },
    })
</script>

父子组件的访问方式: $refs

若想直接访问某个特定的组件,就可以使用$refs
$refs的使用: $refs和ref指令通常是一起使用的

首先,通过ref给某一个子组件绑定一个特定的ID号
其次,通过this.$refs.ID就可以访问绑定的特定组件了

    <div id="app">
      <cpn ref="child1"></cpn>
      <cpn ref="child2"></cpn>
      <button @click="btnClick">通过res访问子组件</button>
    </div>
    <script src="../js/vue.js"></script>
    <script>
      const app = new Vue({
        el: '#app',
        methods: {
          btnClick() {
            //通过ref访问子组件的数据
            console.log(this.$refs.child1.name);
            //父组件通过调用子组件的方法实现使父级组件可以聚焦子组件里的输入框
            this.$refs.child2.sonfocus();
          },
        },
        components: {
          cpn: {
            template: ` 
        <div>我是子组件
        <input ref="ipt">
        </div>`,
            data() {
              return {
                name: '我是子组件child1的name',
              };
            },
            methods: {
              sonfocus() {
                // 子组件也可以使用ref访问自己内部的指定元素
                this.$refs.ipt.focus();
              },
            },
          },
        },
      });
    </script>

点击前:
点击后:

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

此外,父组件访问子组件也可使用$children ,返回的是是一个数组类型,它包含所有子组件对象。需要注意 $children 并不保证顺序,也不是响应式的。
子组件中直接访问父组件可以通过$parent(官方文档强烈不推荐使用此方法) 这两种方法较少用,在此不展开应用

5. 非父子组件通信

具体演示代码都已上传至GitHub上,有兴趣的欢迎下载体验

兄弟组件传值

  • 大型项目中通过Vuex的状态管理(后面有时间会写Vuex的详细应用博客)
  • 先子传父,再父传子,即子组件1通过this.$emit发送自定义事件给父组件,同时将要传递给子组件2的数据作为参数一并传值给父组件,父组件接收到子组件1的事件后将数据赋值给data并传递给子组件2,子组件2通过props接收父组件传递的数据即是子组件1的数据
  • 使用中央事件总线,即子组件1通过Bus中央事件总线用$emit发送一个事件并携带要传递的数据,在子组件2在mounted()或之前的生命周期中用$on监听组件1中发送的事件并触发自己定义的某个回调函数,触发事件的组件在销毁时建议在destoryed生命周期中通过$off销毁事件总线,注意此方法在Vue3已废除
    在这里插入图片描述
    下载代码后在cpncommunication文件夹下的App.vue注释chlid1、child2即可看子传父父再传子的使用;注释child3、child4即可体验事件总线兄弟组件传值方法

祖先组件访问嵌套很深的子组件(多个层级的组件)

  • Vuex
  • 使用中央事件总线
  • provide / inject
    这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深并在其上下游关系成立的时间里始终生效。父级组件通过provide导出要传递的数据(provide 选项应该是一个对象或返回一个对象的函数),任何后代组件都可通过inject:['属性名']获取数据

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的

  • $attrs/ $listeners可用于祖先组件与子组件传值

$attrs继承父组件除了用 prop 传递的属性、class 和 style 外的所有的父组件属性,子组件用this.$attrs即可获取,若要父传孙,就在子组件中添加v-bind='$attrs'绑定在孙组件上,适用组件深层嵌套场景下祖先组件向子组件传值的问题
$ listeners属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以用 v-on=“$listeners” 将所有的事件监听器传入此组件的内部组件——在创建更高层次的组件

在这里插入图片描述
在deeplycpn文件夹下的son.vue里可以决定用哪种方法,代码已做了详细注释


6. 组件中插槽的使用

使用插槽slot可以让封装的组件更加具有扩展性,通过插槽使用者可以自定义组件内部要展示的内容
封装组件时可以将共性抽取到组件中,将不同的部分设为为插槽。

插槽的基本使用:

<div id="app">
  <demo></demo>
    <demo>
        <div>我会替换默认内容</div>
    </demo>
</div>

Vue.component('demo', {
  template: `
    <div>
      <slot><h3>我是插槽,没传值时是默认内容</h3></slot>
    </div>
  `
})

效果如下:在这里插入图片描述

具名插槽:(自 2.6.0 起使用语法有所更新)

使用具名插槽可以替换特定部位的内容

Vue2.6.0以前使用具名插槽:给组件模板的slot标签增加一个name属性:<slot name='插槽名字'></slot>,使用时在组件要替换的内容标签上加slot='插槽名字'
Vue2.6.0之后版本使用具名插槽:子组件不变使用形式不变,父组件在 <template> 元素上使用 v-slot:插槽名字 指令,也可以将(v-slot:) 替换为字符 #简写

<div id="app">
  <!-- Vue 2.6.0之前版本 -->
  <!-- <cpn></cpn>
  <cpn><span v-slot="center">替换了中间</span></cpn>
  <cpn>
    <template slot="left">
      <button>点击返回</button>
    </template>
  </cpn> -->

  <!-- Vue 2.6.0之后使用语法 -->
  <cpn></cpn>
  <cpn>
    <template #center>
      <span>替换了中间</span>
    </template>
  </cpn>
  <cpn>
    <template v-slot:left>
      <button>点击返回</button>
    </template>
  </cpn>
</div>

<template id="cpn">
  <div>
    <slot name="left"><span>左边</span></slot>
    <slot name="center"><span>中间</span></slot>
    <slot name="right"><span>右边</span></slot>
  </div>
</template>

在这里插入图片描述


插槽作用域(自 2.6.0 起使用语法有所更新)

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译
有时让插槽内容能够访问子组件中才有的数据是很有用的
自 2.6.0 起有所更新。已废弃的使用 slot-scope

即父级模板中的数据来自父组件的data,子组件模板的数据来自子组件自己的data

    <!-- 用的是父组件的 isShow: true(在这里是new Vue的data值的里,子组件会显示-->
     <cpn v-show="isShow"></cpn>
    <script>
      const app = new Vue({
        el: '#app',
        data: {
          isShow: true,
        },
        components: {
          cpn: {
            template: `
            <div>
               <h4>我是子组件</h4>
               <p v-show="isShow">我用的是子组件的isShow: false,不会显示出来</p>
            </div>
        `,
            data() {
              return {
                isShow: false,
				}

结果:页面显示我是子组件,即子组件能成功显示,p标签不能显示


父组件想访问子组件里的数据:

  • 先将子组件的数据作为 <slot> 元素的一个属性绑定上去,绑定在<slot>元素上的 属性被称为插槽 prop
  • 在父级作用域中使用带值的 v-slot 来定义插槽 prop 的名字
   <div id="app">
      <cpn></cpn>
      <cpn>
        <!--Vue 2.6.0之前的方法,用slot-scope-->
        <template slot-scope="sonSlot">
          <div>{{sonSlot.mes}}</div>
          <span>{{sonSlot.data.join(' - ')}}</span>
        </template>
      </cpn>

      <!-- Vue2.6.0起 slot-scope已废弃使用,使用v-slot -->
      <cpn>
        <template v-slot:default="slotProps">
          <div>{{slotProps.mes}}</div>
          <span>{{slotProps.data.join(' - ')}}</span>
        </template>
      </cpn>
    </div>

    <!-- 子组件模板 -->
    <template id="cpn">
      <div>
        <!--  pLanguages(数组)、mes为子组件data的值-->
        <slot :data="pLanguages" :mes="mes">
          <ul>
            <li v-for="item in pLanguages">{{item}}</li>
          </ul>
        </slot>
      </div>
    </template>

7. 动态组件

动态组件常用于标签页等实现在不同组件之间进行动态切换的功能,通过动态的绑定多个组件到component标签的is属性上实现

  <div id="app">
        <!-- 通过点击事件将当前点击的组件名赋值给currentTab -->
      <button
        v-for="item in tabs"
        :key="item.cpn"
        @click="currentTab = item.cpn"
      >
        {{ item.value }}
      </button>
      <!-- is绑定currentName,即显示currentName的组件 -->
        <component :is="currentName"></component>
    </div>
	  const one = {
        template: '<div>标签页1<input type="text"></div>',
      };
      const two = {
        template: '<div>标签页2<input type="text"></div>',
      };
      const three = {
        template: '<div>标签页3<input type="text"></div>',
      };
 new Vue({
        el: '#app',
        data: {
        //页面刚加载时显示标签1
          currentTab: 'one',
          tabs: [ { cpn: 'one', value: '标签1' }, {cpn: 'two',value: '标签2'}{ cpn: 'three',  value: '标签3'}],
        },
        components: {
          one,
          two,
          three,
        },
        computed: {
          currentName() {
            // 返回当前组件名currentTab
            return this.currentTab;
          },
        },
      });

在这里插入图片描述
此时当切换组件时组件原有的input数据会消失,若想保持这些组件的状态以避免反复重新渲染导致的性能问题可以使用<keep-alive><keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive 有两个独有的生命周期 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数(即第二次或以上进入该组件时只执行一个生命周期 activated

<keep-alive>
   <component :is="currentName" class="tab"></component>
  </keep-alive>

如果有多个条件判断的子组件 , 要求只能渲染一个子元素

      <!-- 多个子组件在 keep-alive里若不加条件则默认显示第一个子组件-->
      <keep-alive>
        <one v-if="currentTab == 'one'"></one>  
         <two v-else-if ="currentTab == 'two'" ></two>
        <three v-else></three>
      </keep-alive>

8. 组件边界情况处理

  • 访问根实例new Vue:this.$root

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态

  • 程序化的事件侦听器
    通过 $on(eventName, eventHandler) 侦听一个事件
    通过$once(eventName, eventHandler)一次性侦听一个事件
    通过 $off(eventName, eventHandler) 停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。例如在销毁某个组件前也销毁这个组件所引用的第三方库,通过程序化的侦听器可以使我们程序化地清理建立的所有东西

    <div id="app">
      <input ref="dateInput" />
    </div>
    <script>
      new Vue({
        el: "#app",
        mounted: function() {
          var picker = new Pikaday({
            field: this.$refs.dateInput,
            format: "YYYY-MM-DD"
          });
          //组件销毁前也销毁第三方库的日期选择器picker
          this.$once("hook:beforeDestroy", function() {
            picker.destroy();
          });
        }
      });
  • 递归组件:
    组件可以在自己的模板中通过 name选项调用自己,当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项,若使用局部组件则要自定义name的值,当触发递归组件时记得设置终止条件否则会导致栈溢出
   <template id="globalCpn">
      <div>
         <global-cpn></global-cpn>
        <p>全局组件直接用组件名调用自己</p>
      </div>
    </template>
    <script>
      var localCpn = {
        template: `
    	<div>
        	<loacl></loacl>
       	 	<p>局部组件要设置name调用自己</p>
    	</div>
  		`,
  		name:'loacl'
      	};
      // 1.注册一个全局组件
      Vue.component('global-cpn', {
        template: '#globalCpn',
      });

博客所涉及的完整代码

此文所涉及代码已全部上传至Github:A-sketch123 -VueComponent

参考资料

组件基础——Vue.js
深入了解组件——Vue.js
组件通信全面总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值