5. vue组件进阶

96 阅读1分钟

一、组件插槽

1. 基本使用

image.png

// App.vue

<template>
  <div class="app">
    <show-message title="哈哈哈" content="我是内容哈哈哈"></show-message>
  </div>
</template>

<script>
import ShowMessage from './showMessage.vue'

export default {
  components: {
    ShowMessage
  }
}

</script>
// showMessage.vue

<template>
    <div class="showMessage">
      <h2>{{ title }}</h2>
      <div class="content">
        <slot></slot>
      </div>
    </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    },
    content: {
      type: String,
      default: ''
    }
  }
}
</script>

<style scoped></style>

2. 具名插槽

image.png
App.vue

<template>
  <div class="app">
    <!-- 具名插槽 -->
    <nav-bar>
      <template v-slot:left>
        <button>返回</button>
      </template>

      <template v-slot:center>
        <span>内容</span>
      </template>

      <!-- v-slot:right 可以简写为 #right -->
      <template #right>
        <a href="">登录</a>
      </template>
    </nav-bar>

    <!-- 只给一个插槽传递数据,动态插槽名 -->
    <nav-bar>
      <template v-slot:[position]>
        <a href="">注册</a>
      </template>
    </nav-bar>
    <button @click="position = 'left'">左边</button>
    <button @click="position = 'center'">中间</button>
    <button @click="position = 'right'">右边</button>
  </div>
</template>

<script>
import NavBar from './NavBar.vue'

export default {
  components: {
    NavBar
  },
  data(){
    return {
      position: "center"
    }
  }
}

</script>

<style scoped>
button{
  width: 40px;
  height: 40px;
}
</style>
NavBar.vue

<template>
  <div>
    <div class="nav-bar">
      <div class="left">
        <slot name="left">left</slot>
      </div>
      <div class="center">
        <slot name="center">center</slot>
      </div>
      <div class="right">
        <slot name="right">right</slot>
      </div>
    </div>
    <div class="other">
      <!-- 插槽默认名字是default -->
      <slot name="default"></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {},
};
</script>

<style scoped>
.nav-bar {
  display: flex;
  height: 44px;
  text-align: center;
  line-height: 44px;
  font-size: 16px;
}

.left {
  width: 80px;
  background-color: orange;
}

.center {
  flex: 1;
  background-color: skyblue;
}

.right {
  width: 80px;
  background-color: bisque;
}
</style>

3. 作用域插槽

实现功能:点击tab,激活有红色下划线样式,显示点击的页面

image.png

//App.vue

<template>
  <div id="app">
    <tab-control :titles="['衣服', '鞋子', '裤子']" @tabItemClick="tabItemClick"></tab-control>

    <!-- 1.v-slot:default简写为 #default -->
    <tab-control :titles="['衣服', '鞋子', '裤子']" @tabItemClick="tabItemClick">
      <template #default="props">
        <button>{{ props.item }}</button>
      </template>
    </tab-control>

    <!-- 2.独占默认插槽的简写:v-slot:default简写为v-slot -->
    <tab-control :titles="['衣服', '鞋子', '裤子']" @tabItemClick="tabItemClick">
      <template v-slot="props">
        <button>{{ props.item }}</button>
      </template>
    </tab-control>

    <!-- 3.如果只有一个默认插槽,template可以省略,可以直接将v-slot写在组件上 -->
    <tab-control :titles="['衣服', '鞋子', '裤子']" @tabItemClick="tabItemClick" v-slot="props">
      <button>{{ props.item }}</button>
    </tab-control>

    <!-- 4.如果有默认插槽和具名插槽就要按照完整的template来编写 -->

    <h1>{{ pageContents[currentIndex] }}</h1>
  </div>
</template>

<script>
import TabControl from './TabControl.vue'

export default {
  components: {
    TabControl
  },
  data() {
    return {
      pageContents: ["衣服页面", "鞋子页面", "裤子页面"],
      currentIndex: 0
    }
  },
  methods: {
    tabItemClick(index) {
      console.log('app', index);
      this.currentIndex = index;
    }
  }
}

</script>
//TabControl.vue
<template>
  <div class="tab-control">
    <template v-for="(item, index) in titles">
      <div class="tab-control-item" :class="{ active: index === currentIndex }" @click="itemClick(index)">
        <slot :item="item">
          <span>{{ item }}</span>
        </slot>
      </div>
    </template>
  </div>
</template>

<script>
export default {
  props: {
    titles: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      currentIndex: 0
    }
  },
  emits: ["tabItemClick"],
  methods: {
    itemClick(index) {
      this.currentIndex = index;
      this.$emit("tabItemClick", index);
    }
  }
};
</script>

<style scoped>
.tab-control {
  display: flex;
  height: 44px;
  line-height: 44px;
  font-size: 16px;
  text-align: center;
}

.tab-control-item {
  flex: 1;
}

.tab-control-item.active span {
  padding: 0 10px;
  color: red;
  font-weight: 700;
  border-bottom: 1px solid red;
}
</style>

二、组件生命周期函数

image.png
//App.vue

<template>
  <div id="app">
   <div>{{message}}</div>
   <!-- beforeUpdate updated -->
   <button @click="message = 'Hello World'">修改message</button>

   <div>
    <button @click="isShowHome = !isShowHome">显示Home</button>
    <!-- beforeUpdate home-beforeDestroy home-destroyed updated-->
    <home v-if="isShowHome"></home>
   </div>
  </div>
</template>

<script>
import Home from './Home.vue'
export default {
  data(){
    return {
      message: "hello App",
      isShowHome: true
    }
  },
  components: {
    Home
  },
  // 1.组件在创建之前
  beforeCreate(){
    console.log("beforeCreate");
  },
  // 2.组件被创建完成
  created(){
    console.log("created");
    console.log("1. 发送网络请求,请求数据");
    console.log("2. 监听eventBus事件");
    console.log("3. 监听watch数据");
  },
  // 3. 组件template准备被挂载
  beforeMount(){
    console.log("beforeMount");
  },
  // 4. 组件template被挂载,虚拟DOM渲染为真实DOM
  mounted(){
    console.log("mounted");
    console.log("1.获取DOM");
    console.log("2.使用DOM");
  },
  // 5.数据发生改变
  // 5.1 准备更新DOM
  beforeUpdate(){
    console.log("beforeUpdate");
  },
  // 5.2 更新DOM
  updated(){
    console.log("updated");
  },
  // 6. 准备卸载VNODE和DOM元素
  // 6.1 卸载之前
  beforeDestroy(){
    console.log("beforeDestroy");
  },
  // 6.2 卸载完成
  destroyed(){
    console.log("destroyed");
  }
}

</script>

//Home.vue
<template>
  <div>Home</div>
</template>

<script>
export default {
  name: 'Home',
  beforeDestroy(){
    console.log("home-beforeDestroy");
  },
  destroyed(){
    console.log("home-destroyed");
  }
}
</script>

<style scoped></style>
生命周期函数「官方」.png 生命周期函数.png

三、ref获取元素组件

//App.vue
<template>
  <div id="app">
    <!-- 原生操作 -->
    <h2 class="title">Hello World</h2>
    <!-- 数据操作 -->
    <h2 ref="title">数据和ref获取:{{ message }}</h2>
    <button ref="btn" @click="changeTitle">修改title</button>

    <banner ref="banner"></banner>
  </div>
</template>

<script>
import Banner from './Banner.vue'
export default {
  data(){
    return {
      message: "Hello World"
    }
  },
  components: {
    Banner
  },
  methods: {
    changeTitle(){
      //1. 不推荐操作原生DOM
       const titleEl = document.querySelector(".title");
       titleEl.textContent = "你好啊,世界";

       this.message = "你好啊,世界";

      // 2.vue内置属性操作DOM
       console.log(this.$refs.title);
       console.log(this.$refs.btn);
       this.$refs.title.innerText = "你好啊,世界";

      // 3.给组件绑定ref,拿到的就是组件实例
       console.log(this.$refs.banner);
      // 3.1父组件可以调用子组件的方法
       this.$refs.banner.bannerClick();

      // 3.2 获取banner组件实例,获取banner中的元素
       console.log(this.$refs.banner.$el);

      // 3.3 如果banner template有多个根节点,拿到的是另一个node节点
      // 注意:开发中不推荐有多个根
      // console.log(this.$refs.banner.$el.nextElementSibling);

      // 4.组件实例还有两个属性(了解)
       console.log(this.$parent);
       console.log(this.$root);

      // 5.this.$children在vue3中移除
    }
  }
}

</script>

//Banner.vue
<template>
  <div class="banner">
    <h2>banner子组件</h2>
  </div>
</template>

<script>
export default {
  name: 'banner',
  methods: {
    bannerClick(){
      console.log("banner");
    }
  }
}
</script>

<style scoped></style>

四、动态组件

image.png

//App.vue
<template>
  <div id="app">
    <div class="tabs">
      <template v-for="(item, index) in tabs">
        <button @click="itemClick(item)" :class="{ active: currentTab === item }">
          <!-- :class="{ active: currentIndex === index }" -->
          {{ item }}
        </button>
      </template>
    </div>
    <div class="view">
      <!-- 1. 第一种做法:v-if进行判断逻辑,决定显示那个组件 -->
      <!-- <template v-if="currentIndex === 0">
        <home></home>
      </template>
      <template v-if="currentIndex === 1">
        <about></about>
      </template>
      <template v-if="currentIndex === 2">
        <category></category>
      </template> -->

      <!-- 2. 动态组件 -->
      <!-- is中的组件需要来自两个地方:1.全局注册的组件 2.局部注册的组件 -->
      <!-- <component :is="tabs[currentIndex]"></component> -->
      <component name="wx" :age="18" @homeClick="homeClick" :is="currentTab"></component>
    </div>
  </div>
</template>

<script>
import Home from "./views/Home.vue";
import About from "./views/About.vue";
import Category from "./views/Category.vue";

export default {
  data() {
    return {
      tabs: ["home", "about", "category"],
      currentTab: "home",
      // currentIndex: 0
    };
  },
  components: {
    Home,
    About,
    Category,
  },
  methods: {
    itemClick(tab) {
      // this.currentIndex = index;
      this.currentTab = tab;
    },
    homeClick(arg) {
      console.log("homeClick", arg);
    },
  },
};
</script>

<style scoped>
.active {
  color: red;
}
</style>
//Home.vue
<template>
  <div class="home">
    <h2>home组件:{{ name }} - {{ age }}</h2>
    <button @click="homeBtnClick">homeBtn</button>
  </div>
</template>

<script>
export default {
  name: 'home',
  props: {
    name: {
      type: String,
      default: ""
    },
    age: {
      type: Number,
      default: 0
    }
  },
  methods: {
    homeBtnClick(){
      this.$emit("homeClick", "home");
    }
  }
}
</script>

<style scoped></style>
//About.vue
<template>
  <div class="about">
    <h2>about</h2>
  </div>
</template>

<script>
export default {
  name: 'about'
}
</script>

<style scoped></style>
//Category.vue
<template>
  <div class="category">
    <h2>category</h2>
  </div>
</template>

<script>
export default {
  name: 'category'
}
</script>

<style scoped></style>

五、keep-Alive的使用

缓存了的组件不会销毁,keep-alive生命周期,进入组件的时候activated,离开组件的时候deactivated

//App.vue
<template>
  <div id="app">
    <div class="tabs">
      <template v-for="(item, index) in tabs">
        <button
          @click="itemClick(item)"
          :class="{ active: currentTab === item }"
        >
          {{ item }}
        </button>
      </template>
    </div>
    <div class="view">
      <!-- 1.全部缓存 -->
      <!-- <keep-alive >
        <component :is="currentTab"></component>
      </keep-alive> -->

      <!-- 
        include - string | RegExp | Array: 只有名称匹配的组件会被缓存
        exclude - string | RegExp | Array:任何名称匹配的组件都不会被缓存
        max - number | string:最多可以缓存多少个组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁
       -->

      <!--
        include和exclude 属性允许组件有条件的缓存:
        2.1 include="home,about"表示只缓存home,about组件
        2.2 匹配首先检查组件自身的name选项,不是注册的名字
        2.3 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示 
          include="home,about"
          :include="/home|about/"
          :include="['home', 'about']"
      -->
      <keep-alive include="home,about">
        <component :is="currentTab"></component>
      </keep-alive>
    </div>
  </div>
</template>

<script>
import Home from "./views/Home.vue";
import About from "./views/About.vue";
import Category from "./views/Category.vue";

export default {
  data() {
    return {
      tabs: ["home", "about", "category"],
      currentTab: "home",
      // currentIndex: 0
    };
  },
  components: {
    Home,
    About,
    Category,
  },
  methods: {
    itemClick(tab) {
      // this.currentIndex = index;
      this.currentTab = tab;
    }
  },
};
</script>

<style scoped>
.active {
  color: red;
}
</style>

//Home.vue
<template>
  <div class="home">
    <h2>home组件</h2>
    <h2>当前计数:{{ counter }}</h2>
    <button @click="counter++">+1</button>
  </div>
</template>

<script>
export default {
  name: 'home',
  data(){
    return {
      counter: 0
    }
  },
  created(){
    console.log("home created");
  },
  destroyed(){
    console.log("home destroyed");
  },

  // 对于缓存组件来说,再次进入时,是不会执行created或者mounted等生命周期函数的
  // keep-alive组件进入活跃状态
  activated(){
    console.log("activated");
  },
  deactivated(){
    console.log("deactivated");
  }
}
</script>

<style scoped></style>
//About.vue
<template>
  <div class="about">
    <h2>about</h2>
  </div>
</template>

<script>
export default {
  name: 'about',
  destroyed(){
    console.log("about destroyed");
  }
}
</script>

<style scoped></style>
//Category.vue
<template>
  <div class="category">
    <h2>category</h2>
  </div>
</template>

<script>
export default {
  name: 'category',
  destroyed(){
    console.log("category destroyed");
  }
}
</script>

<style scoped></style>

六、异步组件的使用

image.png

//App.vue
<template>
  <div id="app">
    <div class="tabs">
      <template v-for="(item, index) in tabs">
        <button @click="itemClick(item)" :class="{ active: currentTab === item }">
          {{ item }}
        </button>
      </template>
    </div>
    <div class="view">
      <keep-alive include="home,about">
        <component :is="currentTab"></component>
      </keep-alive>
    </div>
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue'

import Home from "./views/Home.vue";
import About from "./views/About.vue";
// import Category from "./views/Category.vue";

/**
 * webpack分包:用import(...)函数引入的文件在打包的时候会独立打包
 *  + 返回一个promise对象
 * 
 * 要想对组件分包
 *  + components中需要放一个组件
 *  + 可以用vue自带的defineAsyncComponent定义一个组件
 *  + 会对这个组件进行分包处理,打包后另加一个JS
 */
const Category = defineAsyncComponent(() => import("./views/Category.vue"))

export default {
  data() {
    return {
      tabs: ["home", "about", "category"],
      currentTab: "home"
    };
  },
  components: {
    Home,
    About,
    Category,
  },
  methods: {
    itemClick(tab) {
      this.currentTab = tab;
    }
  },
};
</script>

<style scoped>
.active {
  color: red;
}
</style>

其余页面是keep-alive中的

七、组件的v-model

v-model数据的双向绑定: image.png

组件的v-model 自定义多个属性名称:

image.png
//App.vue
<template>
  <div class="app">
    <!-- 1.input v-model数据的双向绑定 -->
    <!-- <input v-model="message" />
    <input :value="message" @input="message = $event.target.value" /> -->

    <!-- 2.vue3组件的v-model-->
    <!-- <counter v-model="appCounter"></counter> -->

    <!-- vue2组件 v-model 实现-->
    <counter :value="appCounter" @input="appCounter = $event"></counter>

    <!-- vue3组件 v-model 实现-->
    <!-- <counter :modelValue="appCounter" @update:modelValue="appCounter = $event"></counter> -->

    <!-- 3.组件的v-model 自定义多个属性名称 counter name-->
    <counter2 
      v-model:counter="appCounter" 
      @update:counter="appCounter = $event"
      v-model:name="appName"
    ></counter2>
  </div>
</template>

<script>
import Counter from './Counter.vue'
import Counter2 from './Counter2.vue'

export default {
  components: {
    Counter,
    Counter2
  },
  data() {
    return {
      appCounter: 100,
      message: '',
      appName: 'wx'
    }
  }
}

</script>

//Counter.vue
<template>
  <div class="counter">
    <!-- 
      + 在vue2中一个组件上的v-model默认会利用名为value的props和名为input的事件
      + vue3在组件上的用法发生了变化:
        + value-> modelValue
        + input -> update:modelValue
     -->
    <h2>Counter: {{ value }}</h2>
    <button @click="changeCounter">修改counter</button>
  </div>
</template>

<script>

export default{
  props: {
    modelValue: {
      type: Number,
      default: 0
    },
    value: {
      type: Number,
      default: 0
    }
  },
  methods: {
    changeCounter(){
      // this.$emit("update:modelValue", 99)

      this.$emit("input", 99);
    }
  }
}

</script>

//Counter2.vue
<template>
  <div class="counter">
    <!-- 
      + 在vue2中一个组件上的v-model默认会利用名为value的props和名为input的事件
      + vue3在组件上的用法发生了变化:
        + value-> modelValue
        + input -> update:modelValue
     -->
    <h2>Counter: {{ counter }}</h2>
    <button @click="changeCounter">修改counter</button>

    <!-- name绑定 绑定多个值 -->
    <h2>name: {{ name }}</h2>
    <button @click="changeName">修改name</button>
  </div>
</template>

<script>

export default{
  props: {
    counter: {
      type: Number,
      default: 0
    },
    name: {
      type: String,
      default: ''
    }
  },
  emits: ["update:counter"],
  methods: {
    changeCounter(){
      this.$emit("update:counter", 99);
    },
    changeName(){
      this.$emit("update:name", "Hezi");
    }
  }
}

</script>

八、组件的混入mixins

image.png

全局混入

全局混入.png

mixin的合并规则

minxin的合并规则.png