part5 - 模块2 - 01 - Vue组件

267 阅读12分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

Vue组件

Vue组件

组件用于封装页面的部分功能,将功能的结构、样式、逻辑代码封装为一个整体

一旦发生问题我们只需要去指定组件维护即可,提高复用性和可维护性

使用方法:以自定义HTML的形式插入,使用组件名作为自定义标签名

<body>
  <div id="app">
    <p>我是普通标签</p>
    <!-- 自定义组件直接使用组件名作为标签名即可 -->
    <new-zujian></new-zujian>
  </div>
</body>

组件注册

全局注册组件

全局注册的组件可以应用于任何vue实例中,通过调用component()方法创建

书写方法:vue.component( ' 组件名 ' ,{ 选项对象 } )

注意:全局注册组件必须在根vue实例创建之前使用

<body>
  <div id="app">
    <p>我是普通标签</p>
    <!-- 自定义组件直接使用组件名作为标签名即可 -->
    <new-component></new-component>
  </div>
  <script>
    // 注册全局组件
    Vue.component('new-component', {
      // 组件内部自定义标签内容
      template: '<div>我是全局注册的一个组件</div>'
    })
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {},
    })
  </script>
</body>

局部注册组件

局部注册组件只能在当前实例或者组件中使用,在vue实例的components中使用

<body>
  <div id="app">
    <!-- 引入组件 -->
    <new-component-a></new-component-a>
    <new-component-b></new-component-b>
    <new-component-c></new-component-c>
  </div>
  <script>
    // 我们可以把组件写在外面,直接在components中使用即可
    const NewComponentB = {
      template: `<div>{{value}}</div>`,
      data() {
        return {
          value: '我是组件的data2'
        }
      }
    }
    const vm = new Vue({
      el: '#app',
      data: {},
      // 注册局部组件,内部可以设置多个组件
      components: {
        // 使用-连接需要使用引号包裹起来,首字母大写不需要使用引号包裹
        'new-component-a': {
          template: `<div>{{value}}</div>`,
          data() {
            return {
              value: '我是组件的data'
            }
          }
        },
        // 我们可以直接使用外部配置好的组件直接引入进来
        'new-component-c': NewComponentB, //这种写法兼容性较好
        // 可以使用ES6语法简写为下面(属性名和变量名相同可以简写)
        NewComponentB
      }
    })
  </script>
</body>

组件基础

本质上,组件就是一个可以复用的Vue实例,所以内部也可以设置data、methods、生命周期等钩子函数(el除外)

组件命名规则

  • kebab-case(最常用):使用-连接前后名称,例如:my-son
  • PascalCase:每部分内容首字母大写,例如:MySon

注意:无论组件以何种方式命名,在Dom中引入的的时候必须使用kebab-case命名方法,因为html不区分大小写,全部都认为是小写

template选项

用于设置组件结构,最终会被引入根实例或者其他组件中,内部和在html中书写的结构一样,也可以书写差值表达式、指令等等

<body>
  <div id="app">
    <p>我是普通标签</p>
    <!-- 自定义组件直接使用组件名作为标签名即可 -->
    <new-component></new-component>
  </div>
  <script>
    // 注册全局组件
    Vue.component('new-component', {
      // 使用template设置组件结构,内部可以使用差值表达式
      template: `
        <div>
          <p>{{1 + 2 * 3}}</p>
        </div>
      `
    })
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        value: '我是一条数据'
      },
    })
  </script>
</body>

注意:template选项内部只能有一个大盒子

data选项

用于存储组件中的数据,与根实例不同,data选项是一个函数,数据设置在返回值中,其他方面和根实例使用方法相同

<body>
  <div id="app">
    <!-- 引入组件 -->
    <new-component></new-component>
  </div>
  <script>
    // 注册全局组件
    Vue.component('new-component', {
      // 使用template设置组件结构,内部可以使用差值表达式
      // 如果差值表达式需要使用数据,就需要在组件的data中设置
      template: `
        <div>
          <p>{{value}}</p>
        </div>
      `,
      // data必须写成一个函数,函数返回值才是我们需要的data
      // 这里我们使用es6语法
      data() {
        return {
          value: '我是组件的data'
        }
      }
    })
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {},
    })
  </script>
</body>

注意事项

之所以要以函数的方式存在,是为了确保每一个组件实例都有自己独立一份data数据,不会互相影响

组件间通讯

子组件和父组件以及更加复杂组件关系之间数据的传递

父组件向子组件传值

下面是子组件获取Vue实例中的数据

<body>
  <div id="app">
    <!-- 引入子组件 -->
    <!-- 在子组件内部使用自定义属性的方式引入数据,组件内容可以是静态内容、绑定的静态内容、动态获取data数据 -->
    <new-component title="静态内容"> </new-component>
    <new-component :title="'静态内容'"> </new-component>
    <!-- 这里使用item.title获取到vue实例中的数据 -->
    <new-component :title="item.title"></new-component>
  </div>
  <script>
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        item: {
          title: '我是title'
        }
      },
      // 局部注册组件
      components: {
        'new-component': {
          // 子组件内部使用props接收数据
          props: ['title'],
          // 在结构中使用数据可以使用差值表达式
          template: `<div>{{title}}</div>`
        }
      }
    })
  </script>
</body>

注意props里面的属性不要和data存在同名属性

props命名规则

建议prop命名使用驼峰命名法,父组件绑定时使用 - 连接

<body>
  <div id="app">
    <!-- 传递数据可以使用 - 连接 -->
    <new-component :my-title="item.title"></new-component>
  </div>
  <script>
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        item: {
          title: '我是title'
        }
      },
      // 局部组件
      components: {
        'new-component': {
          // 子组件内部使用props接收数据
          // prop建议使用驼峰命名法
          props: ['myTitle'],
          // 在结构中使用数据可以使用差值表达式
          template: `<div>{{myTitle}}</div>`
        }
      }
    })
  </script>
</body>
练习:通过v-for创建组件
<body>
  <div id="app">
    <!-- 使用template模版进行v-for遍历 -->
    <template v-for="item in items">
      <!-- 内部使用组件,组件内部使用两个数据 -->
      <new-component :my-title="item.title" :my-content="item.content"></new-component>
    </template>
    <!-- 直接在组件上使用v-for,这时需要设置:key,这里我把整个item全部传入组件,之后再使用 -->
    <new-component v-for="item in items" :key="item.title" :item="item" :my-title="item.title"
      :my-content="item.content"></new-component>
  </div>
  <script>
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        // 定义一组数据,后面v-for遍历
        items: [
          {title: '标题1',content: '内容1'},
          {title: '标题2',content: '内容2'},
          {title: '标题3',content: '内容3'},
          {title: '标题4',content: '内容4'},
        ]
      },
      // 局部组件
      components: {
        // 创建组件
        'new-component': {
          // 组件接收数据
          props: ['myTitle', 'myContent', 'item'],
          // 组件使用数据
          template: `<div><h3>{{myTitle}}</h3><p>{{myContent}}</p><p>{{item}}</p></div>`
        }
      }
    })
  </script>
</body>

注意:这里的key是无法传进子组件的,vue知道我们的:key是用于标识数据的,不会将key传进组件中

单向数据流

父子组件间的所有prop都是单向向下绑定的,传向子组件的数据是不能反过来影响父组件数据的,从子组件更改数据不会影响父组件

因为props的数据是不能更改的,如果子组件要处理porp数据,应该存储在子组件的data中进行操作或者在计算属性中操作

<body>
  <div id="app">
    <!-- 使用数据 -->
    <p>{{content}}</p>
    <!-- 引入子组件,子组件传入vue-content数据 -->
    <new-component :vue-content="content"></new-component>
  </div>
  <script>
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        content: '内容'
      },
      // 局部组件
      components: {
        // 创建组件
        'new-component': {
          // 组件接收vueContent
          props: ['vueContent'],
          // 组件中使用data数据
          template: `<div>{{content}}</div>`,
          data () {
            return {
              // 把接受来的数据存在data中,就可以给组件使用了,后期我们就可以编辑这个值了
              content: this.vueContent
            }
          }
        }
      }
    })
  </script>

  <!-- vue中是绝对不允许直接更改props数据的,更改了也无法影响父组件(数组对象除外) -->
</body>

注意:如果prop值是数组和对象,在子组件中编辑将会影响到父组件中数据,因为数组和对象都是引用地址,一旦更改全部引用都会更改

props类型检测

默认props获取到的数据是没有类型限制的,字符串、数组、对象都可以,可以设置props类型检查

将props更改为一个带有验证需求的对象(这个对象就是构造函数,例如String、Array等),并指定对应类型,可以限制类型

<body>
  <div id="app">
    <!-- 引入子组件,子组件传入四个数据 -->
    <new-component
      :one="one"
      :tow="tow"
      :three="three"
      :four="four"
    ></new-component>
  </div>
  <script>
    // vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        // 书写四个数据
        one: {name:'zs',age:19}, //我们把第一条设置为错误类型,可以发现控制台弹出一个错误信息
        tow: [4,5,6],
        three: {name:'zs',age:19},
        four: {name:'ls',age:20}
      },
      // 局部组件
      components: {
        // 创建组件
        'new-component': {
          // 把props设置为对象形式!!!!!!!!!!!!!!!!!!
          props: {
            // 每个数据后面添加一个构造函数类型
            one: String,  //要求数据类型为字符串
            tow: Array,   //要求数据类型为数组
            three: null,  //要求数据类型任意,写为undefined也可以
            four: Object  //要求数据类型为对象
          },
          // 使用数据
          template: `
          <div>
            <p>我是第1行{{one}}</p>
            <p>我是第2行{{tow}}</p>
            <p>我是第3行{{three}}</p>
            <p>我是第4行{{four}}</p>
          </div>`
        }
      }
    })
  </script>
</body>

注意事项

  • 当我们给定的值不是props要求的值,vue只会给出我们一个警告(但还是可以渲染),提示类型错误,因为这不属于语法错误,仍然可以渲染
  • 如果想对一条数据指定多个类型,把指定的prop设置为数组,数组里面添加类型即可,例如one: [String , Array ]

props验证

当prop需要设置多种规则的时候,可以把prop写成对象格式,内部为配置项,常用配置项包括:

  • type:同上类型检测
  • required:默认flase,设置数据为必填项,父组件必须传递这个值,否则报错并且在vue devtools中发现有这个值,值为unf
  • default给数据设置默认值,当父组件未传递值的时候生效,一般情况下,required和default不会同时存在在一个数据配置中,当默认值为数组或者对象的时候,需要像data一样使用函数返回,否则会报错,但也能渲染
  • validator:给传入的prop设置校验函数,return值为flase时vue会发出警告,但也能渲染
// 组件
Vue.component('new-component',{
  // 父组件传入数据使用对象格式
  props: {
    one: {
      // type - 用来对传入的数据进行类型检测
      type: [Array, String]
    },
    tow: {
      type: null,
      // required - 设置必传项,不传则报错
      required: true,
    },
    three: {
      type: null,
      // default设置默认值,父元素不传入使用默认值
      default: '我是默认值'
    },
    four: {
      type: Object,
      // 当默认值为数组或者对象需要使用函数返回
      default() { 
        return {name: 'zs',age: 19} 
      }
    },
    five: {
      type: null,
      // 对传入的值进行校验,返回结果为flase会发出警告
      validator(data){
        return data === 'lagou'
      }
    }
  },
  template: '<div>{{one}}{{tow}}{{three}}{{four}}{{five}}</div>'
})

注意:props中的数据无法使用组件中的data、methods等内容,因为props是在组件实例创建之前的,无法调用组件中的数据和方法,这里的this指向window

非props属性

当父组件给子组件传递数据的时候设置属性,此属性在props中不存在,这时这个属性就会绑定在子组件的跟元素上

<!-- 给子组件绑定一个他根本不需要的属性,属性会绑定到生成的结构根元素中 -->
<new-component class="box" name="my" style="height: 100px;"></new-component>

注意事项

  • 如果子组件中绑定了值,而非props属性又绑定一次会发生覆盖(class和style不会覆盖而会发生合并)
  • 如果不希望继承属性,可以在组件选项中设置inheritAttrs:flase,但只适用于普通属性,class和style不受影响
// 组件
Vue.component('new-component',{
  // 设置可以拒绝继承属性,但class和style还是会继承
  inheritAttrs: false,
  props: {},
  template: '<div></div>'
})

子组件向父组件传值

子组件向父组件传值需要使用自定义事件实现

当子组件发生数据变化时,通过$emit()触发自定义事件,事件名称建议使用 - 连接,事件写在html结构中,事件名就是自定义事件名

购物车案例

需求:使用数据在购物车创建商品,商品内部数量变更影响外部数据总数量

<body>
  <!-- 挂载元素 -->
  <div id="app">
    <!-- 插入组件 
      @add-total自定义事件:子组件触发
      v-for循环:遍历商品列表,创建商品
      :name向组件传递数据
    -->
    <shopping-cart 
      @add-total="addTotal" 
      v-for="good in goods" 
      :key="good.id" 
      :name="good.name">
    </shopping-cart>
    <!-- 商品数量合计 -->
    <p>合计商品数量:{{total}}</p>
  </div>

  <script>
    // 创建全局组件
    Vue.component('shopping-cart',{
      // 传递过来的数据
      props: ['name'],
      // 结构
      // 使用传递过来的数据,使用data数据表示数量
      // 添加按钮,添加点击事件更改数据
      template: `
        <div>
          <span>商品名称:{{name}},商品数量{{count}}</span>
          <button @click="addCount()">+1</button>
        </div>`,
      // 组件data数据
      data () {
        return {
          count: 0
        }
      },
      // 函数
      methods: {
        // 用于修改商品数量
        addCount(){
          // 商品数量自增
          this.count++
          // 通知父元素调用事件
          this.$emit('add-total')
        }
      }
    })
    // Vue实例
    new Vue({
      // 挂载对象
      el: '#app',
      // 数据列表
      data: {
        // 商品列表
        goods: [
          {id: 1, name: '苹果'},
          {id: 2, name: '香蕉'},
          {id: 3, name: '橙子'}
        ],
        // 商品总数量
        total: 0
      },
      // 函数
      methods: {
        // 增加商品数量
        addTotal(){
          // 商品总数量自增
          this.total++
        }
      }
    })


    //this.$emit('add-total')会调用<shopping-cart>上面的@add-total事件,@add-total事件调用会调用Vue实例上的addTotal方法,实现总数量的增加
  </script>
</body>

自定义事件传值(改造购物车)

子组件向父组件传值可以直接使用emit()的第二个参数执行

改造上面的案例

// 修改组件模版,再添加一个按钮,按钮上同样创建点击事件,两个按钮出发的时候带不同参数实现不同的结果
template: `
  <div>
    <span>商品名称:{{name}},商品数量{{count}}</span>
    <button @click="addCount(1)">+1</button>
    <button @click="addCount(5)">+5</button>
  </div>`,
// 修改组件中的事件函数,让函数可以接收一个参数val
addCount(val){
  // 使用参数增加相应个数
  this.count += val
  // 通知父元素调用事件,并且通过$emit输出一个参数,后面想要使用这个参数可以使用 $event
  this.$emit('add-total',val)
}

// 修改父组件中的事件函数,并让函数接受一个参数val
// 增加商品数量
addTotal(val){
  // 商品总数加上参数
  this.total += val
}

// 如果我们@add-total="total += $event"  

注意:当我们想要使用$emit传递过来的参数的时候,有两种方式

  • 如果直接把事件函数体写在html行内,使用参数直接使用event,例如@addtotal="total+=event,例如`@add-total="total += event"`
  • 如果我们事件调用的是vue实例中的函数 ,那么这个参数直接传入函数的参数即可,例如addTotal(*val*)

完整购物车代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/vue.js"></script>
</head>

<body>
  <!-- 挂载元素 -->
  <div id="app">
    <!-- 插入组件 
      @add-total自定义事件:子组件触发
      v-for循环:遍历商品列表,创建商品
      :name向组件传递数据
    -->
    <shopping-cart 
      @add-total="addTotal"  
      v-for="good in goods" 
      :key="good.id" 
      :name="good.name">
    </shopping-cart>
    <!-- 商品数量合计 -->
    <p>合计商品数量:{{total}}</p>
  </div>

  <script>
    // 创建全局组件
    Vue.component('shopping-cart',{
      // 传递过来的数据
      props: ['name'],
      // 结构
      // 使用传递过来的数据,使用data数据表示数量
      // 添加按钮,添加点击事件更改数据,时间带一个参数,通过控制不同的参数实现加不同的值
      template: `
        <div>
          <button @click="addCount(1)">加1件</button>
          <button @click="addCount(5)">加5件</button>
          <span>商品名称:{{name}},商品数量{{count}}</span>
          <button @click="addCount(-1)">减1件</button>
          <button @click="addCount(-5)">减5件</button>
        </div>`,
      // 组件data数据
      data () {
        return {
          count: 0
        }
      },
      // 函数
      methods: {
        // 用于修改商品数量
        addCount(val){
          // 商品数量自增
          this.count += val
          // 通知父元素调用事件
          this.$emit('add-total',val)
        }
      }
    })
    // Vue实例
    new Vue({
      // 挂载对象
      el: '#app',
      // 数据列表
      data: {
        // 商品列表
        goods: [
          {id: 1, name: '苹果'},
          {id: 2, name: '香蕉'},
          {id: 3, name: '橙子'}
        ],
        // 商品总数量
        total: 0
      },
      // 函数
      methods: {
        // 增加商品数量
        addTotal(val){
          // 商品总数量自增
          this.total += val
        }
      }
    })


    //this.$emit('add-total')会调用<shopping-cart>上面的@add-total事件,@add-total事件调用会调用Vue实例上的addTotal方法,实现总数量的增加
  </script>
</body>

</html>

组件和v-model

v-model用于组件时,需要通过props与自定义事件实现

<body>
  <!-- 挂载元素 -->
  <div id="app">
    <!-- 实时展示数据 -->
    <p>{{value}}</p>
    <!-- 插入模版
    使用v-model双向绑定数据,v-model会自动把值传递给组件中,组件中使用props接收即可
    -->
    <new-component v-model="value"></new-component>
  </div>

  <script>
    // 创建全局组件
    Vue.component('new-component',{
      // props接收数据,接收数据存在value中
      props: ['value'],
      // 结构模版
      // 使用:value="value"绑定v-model传递过来的数据
      // @input="onInput"绑定input的input事件
      template: `
        <div>
          <input 
            type="text" 
            @input="onInput" 
            :value="value">
        </div>`,
        methods: {
          // 绑定input标签的input事件,注意这里需要传入一个参数,这参数就是事件元素本身
          onInput(event){
            // 这里的input绑定的是子组件的input事件,而不是input标签的input事件
            // 后面第二个参数是input标签中的value值
            this.$emit('input',event.target.value)
          }
        }
    })
    // Vue实例
    new Vue({
      el: '#app',
      data: {
        value: '123'
      }
    })
  </script>
</body>

注意事项

  • 代码中演示的事件函数写在methods中,如果想卸载template里面需要写成@input="$emit('input',$event.target.value)",可以省略this,直接使用$event表示事件对象

逻辑梳理

  1. 组件通过v-model把数据传递给组件
  2. 组件中使用props接收数据value
  3. 组件模版添加input把value绑定给value
  4. 再给input标签绑定一个input事件,事件中绑定整个组件的input事件
  5. 事件的第二个参数把input标签的value导出出去

非父子组件传值

非父子组件一共有两种形式

  • 兄弟组件:同一个父组件下的两个同级组件
  • 完全无关的两个组件

兄弟组件间传值

兄弟组件之间传值需要通过父组件进行中转,使用子组件改变父组件中的值,然后其他组件再绑定这个值

<body>
  <!-- 挂载元素 -->
  <div id="app">
    <p>value的值:{{value}}</p>
    <son1 @chage-value="value = $event"></son1>
    <son2 :value="value"></son2>
  </div>
  <script>
    const son1= {
      template: `
        <div>
          <button @click="$emit('chage-value','我是新的值,我要被传出去')">给我变</button>
        </div>
      `
    }
    const son2= {
      props: ['value'],
      template: `<div>组件2接收的值:{{value}}</div> `
    }
    // Vue实例
    new Vue({
      el: '#app',
      data: {
        value: '我是旧值,我会被替换掉'
      },
      components: {
        son1,
        son2
      }
    })
  </script>
</body>

代码逻辑解析:

  1. 在vue实例中添加数据value
  2. 组件1中添加按钮通过自定义事件改编父组件的value值
  3. 组件2绑定父组件的value值

EventBus - 事件总线

目前的问题

  • 当组件存在嵌套关系时,如果嵌套层数过多按照关系传值会很复杂
  • 组件为了数据中转,data中会存在许多与当前组件功能无关的数据

EventBus (事件总线)是一个独立的事件中心,用于管理不同组件间的传值操作,只是一项功能,不可以存储数据

EventBus通过一个新的vue实例来管理组件传值操作,组件通过给实例注册事件,调用事件来实现数据传递

功能实现:发送数据的组件触发bus事件,接收组件给bus注册对应事件,一般情况下,我们会吧bus的实例创建放在一个单独的js文件中使用js引入

实例:son1向son2传递数据
<body>
  <div id="app">
    <!-- 插入两个组件 -->
    <son1></son1>
    <son2></son2>
  </div>
  <script>
    // 创建两个组件
    // 组件1
    const son1 = {
      // 结构中添加一个按钮,按钮点击触发事件
      template: `<div><button @click="clickBtn">传递数据</button></div>`,
      methods: {
        // 按钮点击触发的事件
        clickBtn(event){
          // 调用把bus中的chage-value事件,并把数据传递过去
          bus.$emit('chage-value','新数据')
        }
      }
    }
    // 组件2
    const son2= {
      // 组件2直接使用自己的data数据
      template: `<div>{{value}}</div>`,
      data () {
        return {
          value: '原始'
        }
      },
      // /当组件2创建之后在bus上注册一个事件,当事件触发把数据拿过来(value)使用
      created () {
        // 使用bus的$on方法,添加'chage-value自定义事件,事件接受一个参数value
        bus.$on('chage-value',(value)=>{
          // 使用接收的数据替换现有的数据
          // 注意:这里的this因为使用了箭头函数,所以this就指向son2本身,如果使用function需要手动设置this
          this.value = value
        })
      }
    }
    const bus = new Vue()
    const vm = new Vue({
      el: '#app',
      // 添加局部组件
      components: {
        son1,son2
      }
    })
  </script>
</body>

代码逻辑解析

  • 接收数据的组件在初始化完成后在bus上注册一个事件bus.$on(事件名,事件函数(接收的数据)),一旦事件触发,会把数据传递过来
  • 发送数据的组件在自己的函数中调用bus的事件bus.$emit(事件名,要发送的数据)传递数据
  • 当发送数据的组件发送数据之后,接收方也能拿到数据,就可以使用了

原理上来说这种方式适合任意两个组件的传值,这里只是使用兄弟组件做演示

其他通信方式

日常工作中,进行数据通信使用的大部分都是之前学写到的通信方式,以下方式作为了解,因为仅仅是用于简单的vue项目

$root

用来访问当前组件树(组件嵌套结构中的顶层标签)的根实例,如果没有父组件,访问自己,简单的vue应用可以通过这种方式传值

书写方式:this.$root.数据名......

<body>
  <div id="app">
    <p>one的值是{{one}}</p>
    <!-- 插入组件 -->
    <son1></son1>
  </div>
  <script>
    // 组件
    const son1 = {
      // 结构中添加一个按钮,按钮点击触发事件
      template: `<div><button @click="clickBtn">+1</button></div>`,
      methods: {
        clickBtn(){
          // 使用this.$root直接访问组件树的根实例data数据
          this.$root.one += 1
        }
      }
    }
    const vm = new Vue({
      el: '#app',
      data :{one: 1},
      // 添加局部组件
      components: {son1}
    })
  </script>
</body>

如果我们工作中这样写,一旦更改超过我们的预期,我们可能找不到具体是哪个$root更改的数据,所以不建议使用

补充

除了root之外,vue中还提供了root之外,vue中还提供了parent(父组件)和$children(子组件)用于快速访问父子组件,具体使用方法可查询官方文档

$refs

获取设置了ref属性的Html标签或者子组件

// 简单用法
<body>
  <div id="app">
    <!-- 给input设置ref属性 -->
    <input type="text" ref="inp">
    <!-- 按钮设置点击事件 -->
    <button @click="fn">获取焦点</button>
  </div>
  <script>
    
    const vm = new Vue({
      el: '#app',
      data :{one: 1},
      methods: {
        fn(){
          //  使用this.$refs获取指定ref属性的元素
          this.$refs.inp.focus()
        }
      }
    })
  </script>
</body>

给子组件设置ref属性,渲染后可以通过$refs获取子组件实例

<body>
  <div id="app">
    <!-- 插入组件,设置组件的ref属性 -->
    <son1 ref="son1"></son1>
    <!-- 按钮设置点击事件 -->
    <button @click="fn">修改为新值</button>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      methods: {
        // 按钮是件函数
        fn(){
          // 直接通过$refs.son1访问组件son1并修改data中的value值
          this.$refs.son1.value = '新的值'
        }
      },
      // /组件
      components: {
        son1: {
          // 模版中使用data中的value
          template: `<div>{{value}}</div>`,
          data () {
            return {
              value: '旧的值'
            }
          }
        }
      }
    })
  </script>
</body>

注意

  • 要想通过ref访问组件,要确保组件已经加载完毕
  • 所有设置过ref的元素都会存在vue实例的$refs属性中,以键值对的方式存储起来

组件插槽

组件插槽可以快捷设置组件内容

单个插槽

如果我们希望组件标签可以像HTML标签一样设置内容,那么组件的灵活度会很高,但是默认情况下,直接在组件html中书写的内容会被抛弃

可以在组件中使用<slot>进行插槽设置

<body>
  <div id="app">
    <!-- 插入组件 -->
    <son1>
      <!-- 注意:在这里只能获取到父组件的内容,无法读取son1里面的data内容 -->
      <!-- 在插槽内可以使用普通文本,也可通过{{}}动态获取内容 -->
      我是一个普通文本内容
      <p>{{slot[0]}}</p>
      <p>{{slot[1]}}</p>
      <p>{{slot[2]}}</p>
    </son1>
    <!-- 再插入一个组件,组件内插槽不设置任何内容 -->
    <!-- 如果这里不设置任何内容,插槽设置的默认内容会出现在这里 -->
    <son1></son1>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        // vue实例数据
        slot: ['插槽内容','插槽内容2','插槽内容3']
      },
      // 组件
      components: {
        son1: {
          // html模版中添加一个插槽,插槽设置默认内容
          // 默认内容会在没有设置插槽内容是自动显示,反之插槽设置了内容会被替换
          template: `
            <div>
              <p>组件的固定内容</p>
              <slot>我是插槽默认内容,如果你没有在外边设置插槽内容,就会显示我</slot>
            </div>
          `
        }
      }
    })
  </script>
</body>

注意事项:

使用插槽直接书写的{{内容}}只能引入父组件的数据,无法访问子组件的数据

具名插槽

如果组件多个位置需要设置插槽,按需要给slot设置name,称为具名插槽

<body>
  <div id="app">
    <!-- 插入组件 -->
    <son1>
      <!-- 使用具名插槽,具名插槽使用的时候需要在template标签中使用v-slot:指定要使用的插槽名
        template表示占位符,没有实际意义
        v-slot:插槽名 要写在template中 -->
      <template v-slot:header>
        <p>我是header插槽内容</p>
        <p>我是header插槽内容2</p>
      </template>

      <!-- 如果我们插槽没有命名,那么直接写结构就可以了,如果想要写结构,可以写成下面注释的内容 -->
      <!-- <template v-slot:default> -->
        <p>我是main插槽内容</p>
        <p>我是main插槽内容2</p>
      <!-- </template> -->

      <!-- 具名插槽使用的时候可以吧v-slot:简写为# -->
      <template #footer>
        <p>我是footer插槽内容</p>
        <p>我是footer插槽内容2</p>
      </template>
    </son1>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {},
      // 组件
      components: {
        son1: {
          // html模版中添加一个插槽,给不同的插槽设置不同的名字,就可以插入多个插槽
          template: `
          <div>
            <header>
              <slot name="header"></slot>
            </header>
            <main>
              <slot></slot>
            </main>
            <footer>
              <slot name="footer"></slot>
            </footer>
          </div>
          `
        }
      }
    })
  </script>
</body>

具名插槽使用的时候vue允许把v-slot:简写为#

作用域插槽

如果我们希望插槽内部也可以使用子组件的data数据需要使用作用域插槽

组件将需要被使用的数据通过v-bind绑定给slot标签,这种用于插槽传递数据的属性称作插槽prop

<body>
  <div id="app">
    <!-- 插入组件 -->
    <son1>
      <!-- 使用作用域插槽
        #default用于绑定插槽名,default是默认插槽
        #default="obj"表示将默认插槽所有使用v-bind绑定的值组成一个对象
        后面我们使用.调用属性名的方式调用数据-->
      <template #default="obj">
        <p>{{obj.value}}</p>
        <p>{{obj.one}}</p>
        <p>{{obj}}</p>
      </template>
    </son1>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {},
      // 组件
      components: {
        son1: {
          // 模版哪使用v-bind方式调用data数据
          template: `
          <div>
              <slot :value="value" :one="one"></slot>
          </div>
          `,
          data () {
            return {
              value: '子组件value',
              one: '子组件的one'
            }
          }
        }
      }
    })
  </script>
</body>

其他写法

  • 如果只存在默认插槽,还想传递数据,那么可以直接把v-slot写在组件上,例如: <组件名 v-slot:default="对象名"></组件名>,甚至可以直接省略:default,注意这里不可以直接写成#="对象名"

  • 可以直接使用ES6的解构操作进行数据接收,例如v-slot:default="{ value ,one..... }",使用的时候就可以直接使用value和one了

    • <template #default="{value,one}">
        <p>{{value}}</p>
        <p>{{one}}</p>
      </template>
      

内置组件

vue内部提供的组件,例如slot

动态组件

适用于多个组件频换切换的处理

使用<component>将一个元组件渲染为动态组件,使用is属性绑定需要渲染的组件名

<body>
  <div id="app">
    <!-- 创建三个按钮用于切换data中的how数据
      当我们点击三个按钮的时候动态组件会分别显示A、B、C组件-->
    <button v-for="name in title" :key="name" @click="how = name">{{name}}</button>
    <!-- 动态组件,绑定is属性到how -->
    <component :is="how"></component>
  </div>
  <script>
    // 三个组件
    const comA = {
        template: `<div>comA组件的内容</div>`
      },
      comB = {
        template: `<div>comB组件的内容</div>`
      },
      comC = {
        template: `<div>comC组件的内容</div>`
      }

    const vm = new Vue({
      el: '#app',
      data: {
        // 组件名数组
        title: ['comA', 'comB', 'comC'],
        // 动态组件当前绑定的组件名
        how: 'comA'
      },
      // 组件
      components: {
        comA,
        comB,
        comC
      }
    })
  </script>
</body>

is属性在每次切换的时候,vue都会先销毁当前组件再创建一个新的组件实例,前一个组件的状态是无法保留的

keep-alive组件

主要用于保持组件状态或者避免重新渲染,程序会在组件切换过程中将组件进行缓存

书写方法:使用kepp-alive标签包裹动态组件即可

<div id="app">
  <!-- 创建三个按钮用于切换data中的how数据
    当我们点击三个按钮的时候动态组件会分别显示A、B、C组件-->
  <button v-for="name in title" :key="name" @click="how = name">{{name}}</button>
 <!-- 设置keep-alive组件,让每次切换组件都能保留住之前的状态 -->
  <keep-alive>
    <component :is="how"></component>
  </keep-alive>
</div>

keep-alive常用属性

  • include:用于指定那些组件会被缓存

    • <!-- 三种include设置方式
      	方式1:直接设置组件名,使用逗号隔开(注意逗号后面不能加空格)
        方式2:使用动态绑定,支持数组
      	方式3:使用正则表达式
      	下面三条都设置了comA,comB,comC缓存
      -->
      <keep-alive include="comA,comB,comC"> </keep-alive>
      <keep-alive :include="数组"> </keep-alive>
      <keep-alive :include="/com[ABC]/"> </keep-alive>
      
  • exclude:用于指定哪些组件不会被缓存(使用方法同上)

  • max:用于设置最大缓存个数(缓存距离当前操作最近的n次切换操作)

    • <!-- 设置最大缓存只能缓存两个组件,缓存离当前操作组件最近的2次操作组件,超过2个最早的组件删除缓存 -->
      <keep-alive max="2">
      

过渡组件

用于vue插入、更新、移除Dom时,提供应用过度、动画效果

transition组件

给元素或者组件添加进入、离开过渡效果:

  • 条件渲染v-if
  • 条件展示v-show
  • 动态组件
  • 组件根节点

组件提供了6个class类名,可以在css中设置具体过渡效果:

  • 入场动画
    • v-enter:入场前的样式(动画过渡还没开始)
    • v-enter-to:入场后的样式(此时动画过渡已经完成)(一般不设置,使用当前状态
    • v-enter-active:元素入场过程参数控制(入场方式、过渡时间等等)
  • 离场动画
    • v-leave:离场前的样式(动画过渡还没开始)(一般不设置,使用当前状态
    • v-leave-to:离场后的样式(此时动画过渡已经完成)
    • v-leave-active:元素离场过程参数控制(离场方式、过渡时间等等)
!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/vue.js"></script>
  <style>
    /* 直接使用类名即可,这是vue提供的类名 */
    /* 设置进场前和离场后样式 */
    .v-enter,
    .v-leave-to {
      opacity: 0;
    }

    /* 配置进场离场过渡方式 */
    .v-enter-active,
    .v-leave-active {
      /* 全部属性 事件0.5秒 */
      transition: all .5s;
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- 按钮点击切换now的值 -->
    <button @click="now = !now">切换</button>
    <!-- 过渡组件 -->
    <transition>
      <!-- 组件内p标签使用v-show -->
      <p v-show="now">内容</p>
    </transition>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        now: true
      }
    })
  </script>
</body>

</html>

一般情况下,入场最终状态和出场处置状态不需要设置,就直接使用原始状态就可以了

相关属性

name属性

用于给多个元素、组件设置不同的过渡效果,这是需要讲v-更改为对应的name-形式,没有设置name的还是使用v-

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/vue.js"></script>
  <style>
    /* 设置第一个p标签的入场前和出场动画后样式 */
    /* 这里使用name名字one代替v */
    .one-enter,
    .one-leave-to {
      opacity: 0;
    }

    /* 设置第二个p标签的入场前和出场动画后样式 */
    /* 这里使用name名字tow代替v */
    .tow-enter,
    .tow-leave-to {
      font-size: 1000px;
    }

    /* 配置两个p标签的进场离场过渡方式 */
    .one-enter-active,
    .one-leave-active,
    .tow-enter-active,
    .tow-leave-active {
      /* 全部属性 事件0.5秒 */
      transition: all .5s;
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- 按钮点击切换now的值 -->
    <button @click="now = !now">切换</button>

    <!-- 两个过渡组件,设置不同的name名字 -->
    <transition name="one">
      <!-- 组件内p标签使用v-show -->
      <p v-show="now">内容</p>
    </transition>
    <transition name="tow">
      <!-- 组件内p标签使用v-show -->
      <p v-show="now">内容</p>
    </transition>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        now: true
      }
    })
  </script>
</body>
appear属性

让组件在初始渲染的时候也能实现实现过渡效果,默认布尔值

<!-- 直接给transition组件加上appear属性即可 -->
<transition appear>

自定义过渡类名

自定义类名比普通类名优先级更高,在使用第三方css动画库时非常有用

用于设置自定义类名的属性如下

  • enter-class:入场开始
  • enter-active-class:入场配置
  • enter-to-class:入场结束
  • leave-class:离场开始
  • leave-active-class:离场配置
  • leave-to-class:离场结束

用于设置初始过渡类名的的属性如下

  • appear-class:初始入场初始状态
  • appear-to-class:初始入场结束状态
  • appear-active-class:入场配置
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/vue.js"></script>
  <style>
    /* 直接使用类名即可,这是vue提供的类名 */
    /* 设置进场前和离场后样式 */
    .v-enter,
    .v-leave-to {
      opacity: 0;
    }

    /* 配置进场离场过渡方式 */
    .v-enter-active,
    .v-leave-active {
      /* 全部属性 事件0.5秒 */
      transition: all .5s;
    }

    /* 另外再设置一个过渡类名 */
    .test {
      transition: all 5s;
    }
  </style>
</head>

<body>
  <div id="app">
    <button @click="now = !now">切换</button>
    
    <!-- 过渡组件,过渡组件使用自定义过渡类名,就会使用自定义的过渡效果 -->
    <transition enter-active-class="test" leave-active-class="test">
      <p v-show="now">内容</p>
    </transition>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        now: true
      }
    })
  </script>
</body>

第三方css动画库 - Animate.css

通过设置类名给元素添加各种动画效果

Animate.css | A cross-browser library of CSS animations.

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入第三方动画库 -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
  <script src="./js/vue.js"></script>
</head>

<body>
  <div id="app">
    <button @click="now = !now">切换</button>
    <!-- 在transition中使用自定义过渡类名
    animate__backInDown和animate__backOutDown可以在官网查找到 -->
    <transition enter-active-class="animate__backInDown" leave-active-class="animate__backOutDown">
      <!-- 这里使用新版本需要添加一个类名animate__animated -->
      <p class="animate__animated" v-show="now">内容</p>
    </transition>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        now: true
      }
    })
  </script>
</body>

使用注意事项:

  • animate__前缀和compat版本的问题:_:
    • Animate.css目前最新版本(4.0+)要求需要过度的元素添加类名animate__animated,而之前版本类名不需要animate__`前缀,如果你想使用新版本还不想添加这个前缀可以使用兼容compat版本

tarnsition-group组件

tarnsition-group用来给列表统一设置过渡效果

和tarnsition的区别

  • tarnsition-group会生成一个元素节点容器,可以使用tag属性更改节点类型,默认是span节点
  • 过渡效果会应用于内部的元素,而不会应用到容器上
  • 子节点使用v-for必须设置独立的key,动画才能正常工作

tarnsition-group使用方法和tarnsition几乎相同

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./js/vue.js"></script>
  <style>
    /* css部分和tarnsition一摸一样 */
    .v-enter,
    .v-leave-to {
      margin-left: -200px;
    }

    .v-enter-active,
    .v-leave-active {
      transition: all .5s;
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- 两个按钮,点击可以修改num值 -->
    <button @click="num++">+1行</button>
    <button @click="num--">-1行</button>
    <!-- transition-group组件
      使用tag把默认的span修改为ul -->
    <transition-group tag="ul">
      <!-- 使用v-for创建li标签,注意要使用key属性 -->
      <li v-for="(i,index) in num" :key="index">我是第{{i}}行</li>
    </transition-group>
  </div>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        num: 4
      }
    })
  </script>
</body>

</html>

当列表元素因为变更导致后面元素位移比较生硬的时候,可以通过.v-move类名设置移动时的效果

<style>
    ul {
      position: relative;
    }

    .v-enter, .v-leave-to {
      opacity: 0;
      transform: translateX(100px);
    }

    .v-enter-active, .v-leave-active {
      transition: all .5s;
    }

    /* 在元素离场初始状态的时候让元素在离场的过程中脱离标准流,因为在离场过程中如果不脱离标准流元素还是会占有位置,导致.v-move不能生效 */
    .v-leave-active {
      position: absolute;
    }
    /* 通过.v-move设置元素移动过渡效果 */
    .v-move {
      transition: all .5s;
    }

  </style>