Vue3 学习笔记(九)——Vue组件的使用

  Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。

  在生产项目中:

    ① 当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API。

    ② 当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件。

  本文使用“选项式API”风格进行讲解。

一、组件基础(选项式API风格)

1、定义一个组件并使用

  1)当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC)。

  2)组件名官方推荐使用PascalCase命名规则,如:<PascalCase />;但是,PascalCase 的标签名在 DOM 模板中是不可用的。为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 <MyComponent> 或 <my-component> 引用。

  Test.vue文件:

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

  或者使用内联js创建:

export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
}

  2)使用组件

  ① 在 components 选项上注册;

  ② 使用标签;

<script>
import ButtonCounter from './Test.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

2、声明响应式变量的区域data

  选用选项式 API 时,会用 data 选项来声明组件的响应式状态。如下:

export default {
  data() {
    return {
      count: 1,
      someObject: {}
    }
  },

  // `mounted` 是生命周期钩子,之后我们会讲到
  mounted() {
    // `this` 指向当前组件实例
    console.log(this.count) // => 1

    // 数据属性也可以被更改
    this.count = 2
    
    const newObject = {}  // 不属于响应式变量
    this.someObject = newObject
    console.log(newObject === this.someObject) // false
  }
}

  注:

    ① Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀。它同时也为内部属性保留 _ 前缀。因此,你应该避免在顶层 data 上使用任何以这些字符作前缀的属性。

    ② 在上面的示例中,当你在赋值后再访问 this.someObject,此值已经是原来的 newObject 的一个响应式代理。与 Vue 2 不同的是,这里原始的 newObject 不会变为响应式:请确保始终通过 this 来访问响应式状态。

3、声明方法的区域methods

<button @click="increment1">{{ count }}</button>

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment1() {      // 方法风格1:方法绑定了永远指向组件实例的 this。
      this.count++
    },
    increment2: () => {  // 方法风格2:表达式风格;箭头函数没有自己的 this 上下文。
      // 反例:无法访问此处的 `this`!
    }
  },
  mounted() {
    this.increment1();  // 正常使用,this中可以找到increment1函数;
    this.increment2();  // 报错,this中找不到increment2函数。
  }
}

  注:如上面的示例,Vue 自动为 methods 中的方法绑定了永远指向组件实例的 this。这确保了方法在作为事件监听器或回调函数时始终保持正确的 this。你不应该在定义 methods 时使用箭头函数,因为箭头函数没有自己的 this 上下文。

4、组件间传值props与$emit、events

  1)props:接收父类的值

    ①props可以是基础类型(propA: Number)、数组(props: ['foo'])或对象类型(props:{ title: String, likes: Number}),也可以声明允许多种类型 props: { disabled: [Boolean, Number] };设置默认值使用default、必传约束required(默认不需要必穿)、校验使用validator,示例如下:

export default {
  class Person {
    constructor(firstName, lastName) {
      this.firstName = firstName
      this.lastName = lastName
    }
  },

  props: {
    // 基础类型检查
    //(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必传,且为 String 类型
    propC: {
      type: String,
      required: true
    },
    // Number 类型的默认值
    propD: {
      type: Number,
      default: 100
    },
    // 对象类型的默认值
    propE: {
      type: Object,
      // 对象或者数组应当用工厂函数返回。
      // 工厂函数会收到组件所接收的原始 props
      // 作为参数
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // 自定义类型校验函数
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 自定义类型(无校验函数、只校验是不是该类型的实例化类)
    propH: Person,
    // 函数类型的默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认,这不是一个
      // 工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
}

    ②props示例:

    BlogPost.vue

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

    使用

<script>
import BlogPost from './BlogPost.vue'
  
export default {
  components: {
    BlogPost
  },
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}
</script>

<template>
    <!-- 示例1 -->
    <BlogPost title="My journey with Vue" />
    <!-- 示例2-->
	<BlogPost
  	v-for="post in posts"
	  :key="post.id"
  	:title="post.title"
	></BlogPost>
</template>
  2)$emit用来调用父组件的方法并传递数据

    ① 组件通过$emit触发父类的事件,父组件通过v-on(简写为@)来监听子组件的触发。

  模板表达式中创建emit<button @click="$emit('someEvent')">click me</button>,或者js中methods: { submit() { this.$emit('someEvent') } }。(注:事件的名称支持自动的格式转换,camelCase形式 -> kebab-case 形式 )。

  声明式创建emit

export default {
  emits: ['inFocus', 'submit'],
  // 或者使用对象语法,对触发事件的参数进行验证:
  emits: {
    submit(payload) {
      // 通过返回值为 `true` 还是为 `false` 来判断
      // 验证是否通过
    }
  }
}

    ② 同样,组件的事件监听器也支持 .once 修饰符:<MyComponent @some-event.once="callback" />

    ③$emit示例:

    BlogPost.vue

<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>

<template>
  <div class="blog-post">
	  <h4>{{ title }}</h4>
	  <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

    App.vue

<script>
import BlogPost from './BlogPost.vue'
  
export default {
  components: {
    BlogPost
  },
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ],
      postFontSize: 1
    }
  }
}
</script>

<template>
  <div :style="{ fontSize: postFontSize + 'em' }">
    <BlogPost
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
      @enlarge-text="postFontSize += 0.1"
    ></BlogPost>
  </div>
</template>

    ④ $emit添加参数

    触发

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

    监听:

 <MyButton @increase-by="(n) => count += n" />

或者:
 <MyButton @increase-by="increaseCount" />
 ...
 methods: {
   increaseCount(n) {
     this.count += n
   }
 }

    ⑤ 添加校验:

  和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit 的内容,返回一个布尔值来表明事件是否合法。

export default {
  emits: {
    // 没有校验
    click: null,

    // 校验 submit 事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm(email, password) {
      this.$emit('submit', { email, password })
    }
  }
}
  3)events用来声明需要抛出的方法或变量

 

5、理解DOM 更新机制-访问更新后的DOM

  当你更改响应式状态后,DOM 会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只更新一次。

  若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:

import { nextTick } from 'vue'

export default {
  methods: {
    increment() {
      this.count++
      nextTick(() => {
        // 访问更新后的 DOM
      })
    }
  }
}

6、理解浅层作用形式-使响应式变量的子项不具备响应式属性

  在 Vue 中,状态都是默认深层响应式的。这意味着响应式变量的“子项”及“子项的子项”具有响应式。Vue也提供了创建“浅层响应式对象”的方法shallowReactive(),可使“变量子项的子项”不具备响应式:

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 更改状态自身的属性是响应式的
state.foo++

// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false

// 不是响应式的
state.nested.bar++

7、阻止短期内响应事件重复触发1-Vue防抖_按钮最后一次响应再执行事件

  ① 在methods中创建一个执行方法,如下面示例中的click()

  ② 在组件创建事件created中设置一个“预置防抖的处理函数”(如:debouncedClick)关联到执行方法(如:click());

  ③ 在组件创建销毁事件unmounted() 中定义“预置防抖的处理函数”的卸载事件(如:this.debouncedClick.cancel())。

export default {
  created() {
    // 每个实例都有了自己的预置防抖的处理函数
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // 最好是在组件卸载时
    // 清除掉防抖计时器
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... 对点击的响应 ...
    }
  }
}

8、阻止短期内响应事件重复触发2-Vue节流_按钮第一次响应后一定时间内不会再执行事件

  略

二、组件样式绑定

1、绑定静态样式

<div class="static" /div>

2、动态绑定样式_组件内(在组件内给组件内的元素绑定样式)

  1)只启用/关闭样式;支持数组

<!-- :class使用 -->
<div :class="{ active: isActive, 'text-danger': hasError }" /div>
<!-- 结果<div class="static active"></div> -->
    
<!-- :class支持数组 -->
<div :class="[activeClass, errorClass]"></div>
<!-- 结果<div class="active text-danger"></div> -->
    
<!-- :class支持数组内计算 -->
<div :class="[{ active: isActive }, errorClass]"></div>

<!-- :class支持三元表达式 -->
<div :class="[isActive ? activeClass : '', errorClass]"></div>

...
data() {
  return {
    isActive: true,
    hasError: false,
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}

  2)在js中创建样式内容

<div :class="classObject" /div>

data() {
  return {
    classObject: {
      active: true,
      'text-danger': false
    }
  }
}

  3)与计算属性一起使用

<div :class="classObject"></div>

data() {
  return {
    isActive: true,
    error: null
  }
},
computed: {
  classObject() {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}

3、动态绑定样式_组件外(在组件外给组件内的元素绑定样式)

  1)静态绑定

<!-- 子组件模板;组件名MyComponent -->
<p class="foo bar">Hi!</p>

<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
    
<!-- 结果 --> 
<p class="foo bar baz boo">Hi!</p>

  2)动态绑定

<!-- 子组件模板;组件名MyComponent -->
<p class="foo bar">Hi!</p>

<!-- 在使用组件时 -->
<MyComponent :class="{ active: isActive }" />

<!-- 结果 -->
<p class="foo bar active">Hi!</p>

  3)指定哪个根元素来接收这个样式 :class="$attrs.class"

<!-- 子组件模板,组件名MyComponent;使用 $attr 指定被应用样式的元素 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

<!-- 在使用组件时 -->
<MyComponent class="baz" />

<!-- 结果 -->
<p class="baz">Hi!</p>
<span>This is a child component</span>

<!-- 子组件模板;组件名MyComponent -->
<p class="foo bar">Hi!</p>


<MyComponent :class="{ active: isActive }" />

4、使用内联样式style

  1) CSS写法_Vue内置的camelCase写法,如下面示例中的colorfontSize

<div :style="{ color: 'red', fontSize: fontSize1 + 'px' }"></div>

data() {
  return {
    fontSize1: 30
  }
}

  2)CSS写法_CSS 中的实际名称写法:

<div :style="{ 'font-size': fontSize1 + 'px' }"></div>

data() {
  return {
    color: 'red',
    fontSize1: '13px'
  }
}

  3)绑定内联样式与内联样式数组

<!-- 绑定单个内联样式 -->
<div :style="styleObject"></div>

<!-- 绑定内联样式数组 -->
<div :style="[baseStyles, overridingStyles]"></div>


data() {
  return {
    styleObject: {
      color: 'red',
      fontSize: '13px'
    }
  }
}

  4):style支持对一个样式属性提供多个值,以解决某个浏览器需要加浏览器特殊前缀才能正常渲染的问题:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

    注:数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex

三、深入组件

1、模板引用ref="input"

  虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute。

  ① 创建引用标签:<input ref="名称" />

  ② 访问模板引用:this.$refs.名称

<template>
  <input ref="input" />
</template>

<script>
export default {
  mounted() {
    this.$refs.input.focus()  // 访问模板引用,如:this.$refs.名称
  }
}
</script>

  1)v-for 中的模板引用

    当在 v-for 中使用模板引用时,相应的引用中包含的值是一个数组(应该注意的是,ref 数组并不保证与源数组相同的顺序)。

<template>
  <ul>
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  mounted() {
    console.log(this.$refs.items)
  }
}
</script>

  2)函数模板引用中使用ref

  除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

  注:注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

  3)组件上的ref-基础使用

  如果一个子组件使用的是选项式 API ,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互

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

export default {
  components: {
    Child
  },
  mounted() {
    // this.$refs.child 是 <Child /> 组件的实例
  }
}
</script>

<template>
  <Child ref="child" />
</template>

  4)组件上的ref-限制父类对子组件的访问

   下面这个例子中,父组件通过模板引用访问到子组件实例后,仅能访问 publicData 和 publicMethod

export default {
  expose: ['publicData', 'publicMethod'],
  data() {
    return {
      publicData: 'foo',
      privateData: 'bar'
    }
  },
  methods: {
    publicMethod() {
      /* ... */
    },
    privateMethod() {
      /* ... */
    }
  }
}

2、全局组件

  注册全局组件见:Vue3 学习笔记(六)——Vue应用的使用

3、

 

四、内置组件

1、KeepAlive 组件缓存容器组件

  <KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例(默认保留组件状态)。语法为<KeepAlive> <component :is="activeComponent" /> </KeepAlive>示例如下:

  CompA.vue

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <p>Current component: A</p>
  <span>count: {{ count }}</span>
  <button @click="count++">+</button>
</template>

  CompB.vue

<script>
export default {
  data() {
    return {
      msg: ''
    }
  }
}
</script>


<template>
  <p>Current component: B</p>
  <span>Message is: {{ msg }}</span>
  <input v-model="msg">
</template>

  App.vue

<script>
import CompA from './CompA.vue'
import CompB from './CompB.vue'
  
export default {
  components: { CompA, CompB },
  data() {
    return {
      current: 'CompA'
    }
  }
}
</script>

<template>
  <div class="demo">
    <label><input type="radio" v-model="current" value="CompA" /> A</label>
    <label><input type="radio" v-model="current" value="CompB" /> B</label>
    <KeepAlive>
      <component :is="current"></component>
    </KeepAlive>
  </div>
</template>

  1)保留/排除 对某组件的缓存includeexclude

  2)设置最大缓存实例数max

  3)设置缓存实例的生命周期:当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。可以通过 activated 和 deactivated 选项来注册自身的释放。

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

...
export default {
  activated() {
    // 在首次挂载、
    // 以及每次从缓存中被重新插入的时候调用
  },
  deactivated() {
    // 在从 DOM 上移除、进入缓存
    // 以及组件卸载时调用
  }
}

2、Teleport:将一个组件内部的一部分模板“传送”到该组件的外层位置

  适用场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它应该被渲染在Vue组件外部的其他地方。比如:弹出自定义窗体,如下:

  Modal.vue

<script>
export default {
  props: {
    show: Boolean
  }
}
</script>

<template>
  <Transition name="modal">
    <div v-if="show" class="modal-mask">
      <div class="modal-container">
        <div class="modal-header">
          <slot name="header">标题</slot>
        </div>

        <div class="modal-body">
          <slot name="body">内容</slot>
        </div>

        <div class="modal-footer">
          <slot name="footer">页尾
            <button
              class="modal-default-button"
              @click="$emit('close')"
            >OK</button>
          </slot>
        </div>
      </div>
    </div>
  </Transition>
</template>

<style>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  transition: opacity 0.3s ease;
}

.modal-container {
  width: 300px;
  margin: auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  transition: all 0.3s ease;
}

.modal-header h3 {
  margin-top: 0;
  color: #42b983;
}

.modal-body {
  margin: 20px 0;
}

.modal-default-button {
  float: right;
}

/*
 * 对于 transition="modal" 的元素来说
 * 当通过 Vue.js 切换它们的可见性时
 * 以下样式会被自动应用。
 *
 * 你可以简单地通过编辑这些样式
 * 来体验该模态框的过渡效果。
 */
.modal-enter-from {
  opacity: 0;
}

.modal-leave-to {
  opacity: 0;
}

.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>

  App.vue

<!--可定制插槽和 CSS 过渡效果的模态框组件。-->
<script>
import Modal from './Modal.vue'
export default {
  components: {
    Modal
  },
  data() {
    return {
      showModal: false
    }
  }
}
</script>

<template>
  <button id="show-modal" @click="showModal = true">弹出窗体</button>
  <Teleport to="body">
    <!-- 使用这个 modal 组件,传入 prop -->
    <modal :show="showModal" @close="showModal = false">
      <template #header>
        <h3>custom header</h3>
      </template>
    </modal>
  </Teleport>
</template>

  1)禁用 Teleport方案disabled;示例如下:

<Teleport :disabled="isMobile">
  ...
</Teleport>

3、Transition:过渡动画组件

  它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:

  • 由 v-if 所触发的切换
  • 由 v-show 所触发的切换
  • 由特殊元素 <component> 切换的动态组件
  • 改变特殊的 key 属性

  1)基础示例:

  v-if

<script>
export default {
  data() {
    return {
      show: true
    }
  }
}
</script>

<template>
  <button @click="show = !show">Toggle</button>
  <Transition>
    <p v-if="show">hello</p>
  </Transition>
</template>

<style>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>

  v-if、v-else-if、v-else-if

<script setup>
import { ref } from 'vue'

const docState = ref('saved')
</script>

<template>
	<span style="margin-right: 20px">Click to cycle through states:</span>
  <div class="btn-container">
		<Transition name="slide-up">
      <button v-if="docState === 'saved'"
              @click="docState = 'edited'">Edit</button>
      <button v-else-if="docState === 'edited'"
              @click="docState = 'editing'">Save</button>
      <button v-else-if="docState === 'editing'"
              @click="docState = 'saved'">Cancel</button>
    </Transition>
  </div>
</template>

<style>
.btn-container {
  display: inline-block;
  position: relative;
  height: 1em;
}

button {
  position: absolute;
}

.slide-up-enter-active,
.slide-up-leave-active {
  transition: all 0.25s ease-out;
}

.slide-up-enter-from {
  opacity: 0;
  transform: translateY(30px);
}

.slide-up-leave-to {
  opacity: 0;
  transform: translateY(-30px);
}
</style>

  2)组件间过渡

  子组件CompA.vue与CompB.vue

<!-- CompA.vue -->
<template>
  <div>
    Component A
  </div>
</template>

<!-- CompB.vue -->
<template>
  <div>
    Component B
  </div>
</template>

  父组件

<script>
import CompA from './CompA.vue'
import CompB from './CompB.vue'

export default {
  components: { CompA, CompB },
  data() {
    return {
      activeComponent: 'CompA'
    }
  }
}
</script>

<template>
	<label>
    <input type="radio" v-model="activeComponent" value="CompA"> A
  </label>
  <label>
    <input type="radio" v-model="activeComponent" value="CompB"> B
  </label>
  <Transition name="fade" mode="out-in">
    <component :is="activeComponent"></component>
  </Transition>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

  3)设置组件执行动画的属性、持续时间和速度曲线

<script>
export default {
  data() {
    return {
      show: true
    }
  }
}
</script>

<template>
	<button @click="show = !show">Toggle Slide + Fade</button>
  <Transition name="slide-fade">
    <p v-if="show">hello</p>
  </Transition>
</template>

<style>
.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

  4)自定义过渡 class

你也可以向 <Transition> 传递以下的 props 来指定自定义的过渡 class:

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

你传入的这些 class 会覆盖相应阶段的默认 class 名。这个功能在你想要在 Vue 的动画机制下集成其他的第三方 CSS 动画库时非常有用,比如 Animate.css

<script>
export default {
  data() {
    return {
      show: true
    }
  }
}
</script>

<template>
	<button @click="show = !show">Toggle</button>
  <Transition
    name="custom-classes"
    enter-active-class="animate__animated animate__tada"
    leave-active-class="animate__animated animate__bounceOutRight"
  >
    <p v-if="show">hello</p>
  </Transition>
</template>

<style>
@import "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css";
</style>

4、TransitionGroup:v-for 列表中元素变化时的过渡动画组件

  <TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:

  • 默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。

  • 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。

  • 列表中的每个元素都必须有一个独一无二的 key attribute。

  • CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。

  1)平滑的进入 / 离开动画

<!--
通过内建的 <TransitionGroup> 实现“FLIP”列表过渡效果。
https://aerotwist.com/blog/flip-your-animations/
-->

<script>
import { shuffle } from 'lodash-es'

const getInitialItems = () => [1, 2, 3, 4, 5]
let id = getInitialItems().length + 1

export default {
  data() {
    return {
      items: getInitialItems()
    }
  },
  methods: {
    insert() {
      const i = Math.round(Math.random() * this.items.length)
      this.items.splice(i, 0, id++)
    },
    reset() {
      this.items = getInitialItems()
    },
    shuffle() {
      this.items = shuffle(this.items)
    },
    remove(item) {
      const i = this.items.indexOf(item)
      if (i > -1) {
        this.items.splice(i, 1)
      }
    }
  }
}
</script>

<template>
  <button @click="insert">添加</button>
  <button @click="reset">移除</button>
  <button @click="shuffle">重新排列</button>

  <TransitionGroup tag="ul" name="fade" class="container">
    <div v-for="item in items" class="item" :key="item">
      {{ item }}
      <button @click="remove(item)">x</button>
    </div>
  </TransitionGroup>
</template>

<style>
.container {
  position: relative;
  padding: 0;
}

.item {
  width: 100%;
  height: 30px;
  background-color: #f3f3f3;
  border: 1px solid #666;
  box-sizing: border-box;
}

/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}

/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: scaleY(0.01) translate(30px, 0);
}

/* 3. 确保离开的项目被移除出了布局流
      以便正确地计算移动时的动画效果。 */
.fade-leave-active {
  position: absolute;
}
</style>

  2)渐进延迟列表动画

<script>
import gsap from 'gsap'

const list = [
  { msg: 'Bruce Lee' },
  { msg: 'Jackie Chan' },
  { msg: 'Chuck Norris' },
  { msg: 'Jet Li' },
  { msg: 'Kung Fury' }
]

export default {
  data() {
    return {
      query: ''
    }
  },
  computed: {
    computedList() {
      return list.filter((item) => item.msg.toLowerCase().includes(this.query))  // 查询包含的值
    }
  },
  methods: {
    onBeforeEnter(el) {
      el.style.opacity = 0
      el.style.height = 0
    },
    onEnter(el, done) {
      gsap.to(el, {
        opacity: 1,
        height: '1.6em',
        delay: el.dataset.index * 0.15,
        onComplete: done
      })
    },
    onLeave(el, done) {
      gsap.to(el, {
        opacity: 0,
        height: 0,
        delay: el.dataset.index * 0.15,
        onComplete: done
      })
    }
  }
}
</script>

<template>
  <input v-model="query" />
  <TransitionGroup
    tag="ul"
    :css="false"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
  >
    <li
      v-for="(item, index) in computedList"
      :key="item.msg"
      :data-index="index"
    >
      {{ item.msg }}
    </li>
  </TransitionGroup>
</template>

5、Suspense:在组件树中协调对异步依赖的处理

  它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

<Suspense>
└─ <Dashboard>
   ├─ <Profile>
   │  └─ <FriendStatus>(组件有异步的 setup())
   └─ <Content>
      ├─ <ActivityFeed> (异步组件)
      └─ <Stats>(异步组件)

  在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 <Suspense>,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态

  异步组件默认就是“suspensible”的。这意味着如果组件关系链上有一个 <Suspense>,那么这个异步组件就会被当作这个 <Suspense> 的一个异步依赖。在这种情况下,加载状态是由 <Suspense> 控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略

  1)基础使用模板

<Suspense>
  <!-- 主要内容;如:具有深层异步依赖的组件 -->
  <component :is="Component"></component>

  <!-- 在 #fallback 插槽中显示 “正在加载中” -->
  <template #fallback>
    Loading...
  </template>
</Suspense>

  在初始渲染时,<Suspense> 将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容(#fallback)。当所有遇到的异步依赖都完成后,<Suspense> 会进入完成状态,并将展示出默认插槽的内容(#default;如上图<component :is="Component"></component>)。如果在初次渲染时没有遇到异步依赖,<Suspense> 会直接进入完成状态。

  2)异步组件设置timeout 

  在等待(异步组件)渲染新内容耗时超过 timeout 之后,<Suspense> 将会切换为展示后备内容。若 timeout 值为 0 将导致在替换默认内容时立即显示后备内容。异步组件设置timeout示例如下:

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

  3)具有的事件

  <Suspense> 组件会触发三个事件:pendingresolve 和 fallbackpending 事件是在进入挂起状态时触发。resolve 事件是在 default 插槽完成获取新内容时触发。fallback 事件则是在 fallback 插槽的内容显示时触发。

  4)错误处理

  <Suspense> 组件自身目前还不提供错误处理,不过你可以使用 errorCaptured 选项或者 onErrorCaptured() 钩子,在使用到 <Suspense> 的父组件中捕获和处理异步错误。

五、语法

  见:Vue3 学习笔记(八)——Vue语法

六、生命周期

  见:Vue3 学习笔记(十)——生命周期

下一章:Vue3 学习笔记(十)——生命周期

posted @ 2023-04-18 16:47  ꧁执笔小白꧂  阅读(464)  评论(0编辑  收藏  举报