【Vue】course_2

7.插槽

组件的最大特性就是 重用 ,而用好插槽能大大提高组件的可重用能力。

**插槽的作用:**父组件向子组件传递内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JI8FZpIm-1672136742834)(assets/34.png)]

通俗的来讲,插槽无非就是在 子组件 中挖个坑,坑里面放什么东西由 父组件 决定。

插槽类型有:

  • 单个(匿名)插槽
  • 具名插槽
  • 作用域插槽

7.1 - 插槽内容与插口

在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

这里有一个 <FancyButton> 组件,可以像这样使用:

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

<FancyButton> 的模板是这样的:

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WTdnwc4R-1672136742834)(assets/slots.dbdaf1e8.png)]

最终渲染出的 DOM 是这样:

<button class="fancy-btn">Click me!</button>

完整案例:06_slot/42_slot.html

<!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>插槽</title>
</head>
<body>
  <div id="app">
    <!-- 自定义组件标签调用
    默认情况下 内部代码是无法被解析的
    可以在定义组件的模版时,通过 slot 标签显示内容
    -->
    <fancy-button>点击</fancy-button>
    <fancy-button>注册</fancy-button>
    <fancy-button>登录</fancy-button>
  </div>
</body>
<template id="btn">
  <button>
    按钮 - <slot></slot>
  </button>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const FancyButton = {
    template: '#btn'
  }
  Vue.createApp({
    components: {
      FancyButton
    }
  }).mount('#app')
</script>
</html>

7.2渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。

插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

完整案例:06_slot/43_slot_render_scope.html

<!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>插槽</title>
</head>
<body>
  <div id="app">
    <!-- 自定义组件标签调用
    默认情况下 内部代码是无法被解析的
    可以在定义组件的模版时,通过 slot 标签显示内容
    -->
    <span>{{ msg }}</span>
    <fancy-button>{{ msg }}</fancy-button>
  </div>
</body>
<template id="btn">
  <button>
    按钮 - <slot></slot>
  </button>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const FancyButton = {
    template: '#btn',
    data () {
      return {
        msg: '注册'
      }
    }
  }
  Vue.createApp({
    data () {
      return {
        msg: '登录'
      }
    },
    components: {
      FancyButton
    }
  }).mount('#app')
</script>
</html>

7.3默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:

<button type="submit">
  <slot></slot>
</button>

如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

现在,当我们在父组件中使用 <SubmitButton> 且没有提供任何插槽内容时:

<SubmitButton />

“Submit”将会被作为默认内容渲染:

<button type="submit">Submit</button>

但如果我们提供了插槽内容:

<SubmitButton>Save</SubmitButton>

那么被显式提供的内容会取代默认内容:

<button type="submit">Save</button>

完整案例:06_slot/44_slot_default.html

<!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>插槽</title>
</head>
<body>
  <div id="app">
    <!-- 自定义组件标签调用
    默认情况下 内部代码是无法被解析的
    可以在定义组件的模版时,通过 slot 标签显示内容
    -->
    <fancy-button>点击</fancy-button>
    <fancy-button>注册</fancy-button>
    <fancy-button></fancy-button>
  </div>
</body>
<template id="btn">
  <button>
    <slot>按钮</slot>
  </button>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const FancyButton = {
    template: '#btn'
  }
  Vue.createApp({
    components: {
      FancyButton
    }
  }).mount('#app')
</script>
</html>

7.4具名插槽(v-slot属性,#简写)

有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout> 组件中,有如下模板:

<div class="container">
  <header>
    <!-- 标题内容放这里 -->
  </header>
  <main>
    <!-- 主要内容放这里 -->
  </main>
  <footer>
    <!-- 底部内容放这里 -->
  </footer>
</div>

对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name<slot> 出口会隐式地命名为“default”。

在父组件中使用 <BaseLayout> 时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oKs14cfJ-1672136742835)(assets/named-slots.ebb7b207.png)]

完整案例:06_slot/45_slot_name.html

<!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>具名插槽</title>
</head>
<body>
  <div id="app">
    <base-layout>
      <!-- vue2可以这样写 -->
      <!-- <div slot="header">首页头部</div>
      <div>首页内容</div>
      <div slot="footer">首页底部</div> -->
      <!-- vue3 使用 v-slot指令 -->
      <template v-slot:header>
        <div>首页头部</div>
      </template>
      <div>首页内容</div>
      <template v-slot:footer>
        <div >首页底部</div> 
      </template>
    </base-layout>
    <base-layout>
      <!-- v-slot:header 可简写为 #header -->
      <template #header>
        <div>分类头部</div>
      </template>
      <!-- 默认内容要写 template 就写 v-slot:default 或者  #default -->
      <template #default>
        <div>分类内容</div>
      </template>
      <template #footer>
        <div>分类底部</div>
      </template>
    </base-layout>
  </div>
</body>
<template id="layout">
  <div class="container">
    <header class="header">
       <slot name="header">头部</slot>
    </header>
    <div class="content">
       <slot>内容</slot>
    </div>
    <footer class="footer">
       <slot name="footer">底部</slot>
    </footer>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const BaseLayout = {
    template: '#layout'
  }

  Vue.createApp({
    components: {
      BaseLayout
    }
  }).mount('#app')
</script>
</html>

7.5动态插槽名 - 了解

动态指令参数v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

注意这里的表达式和动态指令参数受相同的语法限制

完整案例:06_slot/46_dynamic_slot_name.html

<!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>46_动态插槽名</title>
</head>
<body>
  <div id="app">
    <button @click="count++">{{ count }}</button>
    <my-com>
      <template v-slot:[type]>
        {{ type }} 父组件默认值
      </template>
    </my-com>
    
  </div>
</body>
<template id="com">
  <div >
    <slot name="dan"> 1 动态插槽名   子组件默认值 1</slot>
    <br />
    <slot name="shaung"> 2 动态插槽名   子组件默认值 2</slot>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp } = Vue

  const Com = {
    template: '#com'
  }

  const app = createApp({
    components: {
      MyCom: Com
    },
    data () {
      return {
        count: 0
      }
    },
    computed: {
      type () {
        return this.count % 2 === 0 ? 'shaung': 'dan'
      }
    }
  })

  app.mount('#app')
</script>
</html>

7.6作用域插槽-了解

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DRQTSeCb-1672136755386)(null)]

完整案例:06_slot/47_scope_slot.html

<!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>插槽</title>
</head>
<body>
  <div id="app">
    <!-- 
      插槽的内容如果既想要访问父组件域内数据 msg,也想要访问子组件域内的数据 str num
      在定义插槽的时候给slot 添加了 str 以及 num的自定义属性
      在调用组件的时候 通过 v-slot 给组件添加 slotProps 的值
      该值将包含子组件域内的数据,以对象的形式存在
    -->
    <fancy-button v-slot="slotProps">
      {{ msg }} -- {{ slotProps.str }} -- {{ slotProps.num }}
    </fancy-button>
  </div>
</body>
<template id="btn">
  <button>
    按钮 - <slot :str="str" :num="10000"></slot>
  </button>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const FancyButton = {
    template: '#btn',
    data () {
      return {
        str: '注册'
      }
    }
  }
  Vue.createApp({
    data () {
      return {
        msg: '登录'
      }
    },
    components: {
      FancyButton
    }
  }).mount('#app')
</script>
</html>

7.7 $slots - 了解

一个表示父组件所传入插槽的对象。

通常用于手写渲染函数,但也可用于检测是否存在插槽。

每一个插槽都在 this.$slots 上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.$slots.default

如果插槽是一个作用域插槽,传递给该插槽函数的参数可以作为插槽的 prop 提供给插槽。

在渲染函数中,可以通过 this.$slots 来访问插槽:

完整案例:06_slot/48_$slot.html

<!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>48_$slots渲染函数</title>
</head>
<body>
  <div id="app">
    <my-com>
      <template #default>1111</template>
      <template #footer>2222</template>
    </my-com>
  </div>
</body>
<template id="com">
  <div><slot>默认值</slot></div>
  <div><slot name="footer">底部默认值</slot></div>
</template>
<script src="lib/vue.global.js"></script>
<script>
  const { createApp, h } = Vue // h 代表创建一个元素 createElement

  const Com = {
    // template: '#com'
    render () {
      console.log(this.$slots)
      return [
        h('div', { class: 'content'}, this.$slots.default()), // <div class="content"><slot></slot></div>
        h('div', { class: 'footer'}, this.$slots.footer()) // <div class="footer"><slot name="footer"></slot></div>
      ]
    }
  }

  const app = createApp({
    components: {
      MyCom: Com
    }
  })

  app.mount('#app')
</script>
</html>

vue2渲染函数参照

<!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>vue2渲染函数</title>
</head>
<body>
  <div id="app">
    <my-com></my-com>
  </div>
</body>
<template id="com">
  <div>com</div>
</template>
<template id="test">
  <div>test</div>
</template>
<script src="../lib/vue.js"></script>
<script>
  const Com = {
    template: '#com'
  }
  const Test = {
    template: '#test'
  }
  // new Vue({
  //   el: '#app',
  //   components: {
  //     MyCom: Com
  //   }
  // })

  // new Vue({
  //   components: {
  //     MyCom: Com
  //   }
  // }).$mount('#app')

  new Vue({
    // render: (h) => h(Com)
    render: (createElement) => createElement('div', {}, [ createElement(Com),  createElement(Test)])
  }).$mount('#app')
</script>
</html>

8.依赖注入

通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJRb14me-1672136742837)(assets/prop-drilling.11201220.png)]

注意,虽然这里的 <Footer> 组件可能根本不关心这些 props,但为了使 <DeepChild> 能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。

provideinject 可以帮助我们解决这一问题。 [1] 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SxMhhE6-1672136742837)(assets/provide-inject.3e0505e4.png)]

8.1 provide

要为组件后代提供数据,需要使用到 provide 选项:

{
  provide: {
    message: 'hello!'
  }
}

对于 provide 对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。

如果我们需要提供依赖当前组件实例的状态 (比如那些由 data() 定义的数据属性),那么可以以函数形式使用 provide

{
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    // 使用函数的形式,可以访问到 `this`
    return {
      message: this.message
    }
  }
}

不会使注入保持响应性(比如祖先组件中有一个count的状态,祖先组件修改完状态,后代组件默认的值没有响应式的改变)

8.2 inject

要注入上层组件提供的数据,需使用 inject 选项来声明:

{
  inject: ['message'],
  created() {
    console.log(this.message) // injected value
  }
}

注入会在组件自身的状态之前被解析,因此你可以在 data() 中访问到注入的属性:

{
  inject: ['message'],
  data() {
    return {
      // 基于注入值的初始数据
      fullMessage: this.message
    }
  }
}

当以数组形式使用 inject,注入的属性会以同名的 key 暴露到组件实例上。在上面的例子中,提供的属性名为 "message",注入后以 this.message 的形式暴露。访问的本地属性名和注入名是相同的。

如果我们想要用一个不同的本地属性名注入该属性,我们需要在 inject 选项的属性上使用对象的形式:

{
  inject: {
    /* 本地属性名 */ localMessage: {
      from: /* 注入来源名 */ 'message'
    }
  }
}

这里,组件本地化了原注入名 "message" 所提供的的属性,并将其暴露为 this.localMessage

默认情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。

如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:

{
  // 当声明注入的默认值时
  // 必须使用对象形式
  inject: {
    message: {
      from: 'message', // 当与原注入名同名时,这个属性是可选的
      default: 'default value'
    },
    user: {
      // 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例
      // 需要独立数据的,请使用工厂函数
      default: () => ({ name: 'John' })
    }
  }
}

完整案例:07_provide/49_provide_inject.html

<!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>依赖注入</title>
</head>
<body>
  <div id="app">
    <button @click="count++">{{count}}</button>
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <my-child></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <!-- {{ msg }} -- {{ count }} -->
    {{ mymsg }} -- {{ mycount }} -- {{ mytest }}
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Child = {
    template: '#child',
    // inject: ['msg', 'count']
    inject: {
      mymsg: {
        from: 'msg'
      },
      mycount: {
        from: 'count'
      },
      mytest: {
        default: '传家宝'
      }
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    }
  }

  Vue.createApp({
    data () {
      return {
        message: 'hello',
        count: 100
      }
    },
    provide () {
      // 使用函数的形式,可以访问到 `this`
      return {
        msg: this.message,
        count: this.count
      }
    },
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

发现以上案例在count值发生改变时没有更新后代数据

8.3 配合响应性 computed()

为保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性

完整案例:07_provide/50_provide_inject_computed_vue3.html

<!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>依赖注入</title>
</head>
<body>
  <div id="app">
    <button @click="count++">{{count}}</button>
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <my-child></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <!-- {{ msg }} -- {{ count }} -->
    {{ mymsg }} -- {{ mycount }} -- {{ mytest }}
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Child = {
    template: '#child',
    // inject: ['msg', 'count']
    inject: {
      mymsg: {
        from: 'msg'
      },
      mycount: {
        from: 'count'
      },
      mytest: {
        default: '传家宝'
      }
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    }
  }
  const { computed } = Vue
  Vue.createApp({
    data () {
      return {
        message: 'hello',
        count: 100
      }
    },
    provide () {
      // 使用函数的形式,可以访问到 `this`
      return {
        msg: this.message,
        count: computed(() => this.count) // 确保响应式
      }
    },
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

测试得知vue2中也是如此处理数据

9.动态组件

有些场景会需要在两个组件间来回切换(Tab切换)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43AZ6zrO-1672136742838)(assets/image-20220919113443278.png)]

9.1特殊 Attribute—is

用于绑定动态组件

<!-- currentTab 改变时组件也改变 --> 
<component :is="currentTab"></component>

完整案例:08_dynamic/51_dynamic_component.html

<!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>动态组件</title>
</head>
<body>
  <div id="app">
    <ul>
      <li @click="currentTab='Home'">首页</li>
      <li @click="currentTab='Kind'">分类</li>
      <li @click="currentTab='Cart'">购物车</li>
      <li @click="currentTab='User'">我的</li>
    </ul>

    <component :is="currentTab"></component>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const Home = {
    template: `
      <div>
        首页  
      </div>
    `
  }
  const Kind = {
    template: `
      <div>
        分类  
      </div>
    `
  }
  const Cart = {
    template: `
      <div>
        购物车  
      </div>
    `
  }
  const User = {
    template: `
      <div>
        我的  
      </div>
    `
  }

  Vue.createApp({
    data () {
      return {
        currentTab: 'Home'
      }
    },
    components: {
      Home,
      Kind,
      Cart,
      User
    }
  }).mount('#app')
</script>
</html>

如果此时给每个组件加入一个输入框,输入内容切换组件查看效果,发现切换回来数据不在

9.2 <KeepAlive>组件

缓存包裹在其中的动态切换组件

<KeepAlive> 包裹动态组件时,会缓存不活跃的组件实例,而不是销毁它们。

任何时候都只能有一个活跃组件实例作为 <KeepAlive> 的直接子节点。

完整案例:08_dynamic/52_keep-alive.html

<!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>动态组件</title>
</head>
<body>
  <div id="app">
    <ul>
      <li @click="currentTab='Home'">首页</li>
      <li @click="currentTab='Kind'">分类</li>
      <li @click="currentTab='Cart'">购物车</li>
      <li @click="currentTab='User'">我的</li>
    </ul>
    <!-- 动态组件默认切换时 执行的是组件的 销毁 和 重新创建 -->
    <!-- 可以使用 KeepAlive 保留组件的状态,避免组件的重新渲染 -->
    <keep-alive>
      <component :is="currentTab"></component>
    </keep-alive>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const Home = {
    template: `
      <div>
        首页  <input placeholder="首页"/>
      </div>
    `,
    created () { console.log('Home created') },
    mounted () { console.log('Home mounted') },
    unmounted () { console.log('Home unmounted') }
  }
  const Kind = {
    template: `
      <div>
        分类  <input placeholder="分类"/>
      </div>
    `,
    created () { console.log('Kind created') },
    mounted () { console.log('Kind mounted') },
    unmounted () { console.log('Kind unmounted') }
  }
  const Cart = {
    template: `
      <div>
        购物车  <input placeholder="购物车"/>
      </div>
    `,
    created () { console.log('Cart created') },
    mounted () { console.log('Cart mounted') },
    unmounted () { console.log('Cart unmounted') }
  }
  const User = {
    template: `
      <div>
        我的  <input placeholder="我的"/>
      </div>
    `,
    created () { console.log('User created') },
    mounted () { console.log('User mounted') },
    unmounted () { console.log('User unmounted') }
  }

  Vue.createApp({
    data () {
      return {
        currentTab: 'Home'
      }
    },
    components: {
      Home,
      Kind,
      Cart,
      User
    }
  }).mount('#app')
</script>
</html>

当一个组件在 <KeepAlive> 中被切换时,它的 activateddeactivated 生命周期钩子将被调用,用来替代 mountedunmounted。这适用于 <KeepAlive> 的直接子节点及其所有子孙节点。

9.3activated、deactivated钩子

完整案例:08_dynamic/53_activated_deacvidated.html

<!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>动态组件</title>
</head>
<body>
  <div id="app">
    <ul>
      <li @click="currentTab='Home'">首页</li>
      <li @click="currentTab='Kind'">分类</li>
      <li @click="currentTab='Cart'">购物车</li>
      <li @click="currentTab='User'">我的</li>
    </ul>
    <!-- 动态组件默认切换时 执行的是组件的 销毁 和 重新创建 -->
    <!-- 可以使用 KeepAlive 保留组件的状态,避免组件的重新渲染 -->
    <keep-alive>
      <component :is="currentTab"></component>
    </keep-alive>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const Home = {
    template: `
      <div>
        首页  <input placeholder="首页"/>
      </div>
    `,
    created () { console.log('Home created') },
    mounted () { console.log('Home mounted') },
    unmounted () { console.log('Home unmounted') },
    activated () { console.log('Home 显示')},
    deactivated () { console.log('Home 隐藏')}
  }
  const Kind = {
    template: `
      <div>
        分类  <input placeholder="分类"/>
      </div>
    `,
    created () { console.log('Kind created') },
    mounted () { console.log('Kind mounted') },
    unmounted () { console.log('Kind unmounted') },
    activated () { console.log('Kind 显示')},
    deactivated () { console.log('Kind 隐藏')}
  }
  const Cart = {
    template: `
      <div>
        购物车  <input placeholder="购物车"/>
      </div>
    `,
    created () { console.log('Cart created') },
    mounted () { console.log('Cart mounted') },
    unmounted () { console.log('Cart unmounted') },
    activated () { console.log('Cart 显示')},
    deactivated () { console.log('Cart 隐藏')}
  }
  const User = {
    template: `
      <div>
        我的  <input placeholder="我的"/>
      </div>
    `,
    created () { console.log('User created') },
    mounted () { console.log('User mounted') },
    unmounted () { console.log('User unmounted') },
    activated () { console.log('User 显示')},
    deactivated () { console.log('User 隐藏')}
  }

  Vue.createApp({
    data () {
      return {
        currentTab: 'Home'
      }
    },
    components: {
      Home,
      Kind,
      Cart,
      User
    }
  }).mount('#app')
</script>
</html>

要不不缓存,要缓存都缓存了,这样不好

使用 include / exclude可以设置哪些组件被缓存,使用 max可以设定最多缓存多少个

<!-- 用逗号分隔的字符串,中间不要家空格 -->
<KeepAlive include="a,b">
	<component :is="view"></component>
</KeepAlive>

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

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

组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。

完整案例:08_dynamic/54_keep_alive_include.html

<!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>动态组件</title>
</head>
<body>
    <div id="app">
       <ul>
         <li @click="currentTab='Home'">首页</li>
         <li @click="currentTab='Kind'">分类</li>
         <li @click="currentTab='Cart'">购物车</li>
         <li @click="currentTab='User'">我的</li>
       </ul>
    <!-- 动态组件默认切换时 执行的是组件的 销毁 和 重新创建 -->
       <!-- 可以使用 KeepAlive 保留组件的状态,避免组件的重新渲染 -->
       <!-- 字符串逗号分隔,千万不要加空格 -->
       <!-- <keep-alive include="home,user">
      <component :is="currentTab"></component>
       </keep-alive> -->
       <!-- 正则 -->
    <!-- <keep-alive :include="/home|user/">
         <component :is="currentTab"></component>
       </keep-alive> -->
    <!-- 数组 -->
       <keep-alive :include="['home', 'user']">
         <component :is="currentTab"></component>
       </keep-alive>
     </div>
  </body>
<script src="../lib/vue.global.js"></script>
<script>
  const Home = {
      name: 'home',
      template: `
        <div>
        首页  <input placeholder="首页"/>
        </div>
      `,
      created () { console.log('Home created') },
       mounted () { console.log('Home mounted') },
       unmounted () { console.log('Home unmounted') },
       activated () { console.log('Home 显示')},
       deactivated () { console.log('Home 隐藏')}
     }
     const Kind = {
       name: 'kind',
       template: `
         <div>
           分类  <input placeholder="分类"/>
         </div>
       `,
       created () { console.log('Kind created') },
       mounted () { console.log('Kind mounted') },
       unmounted () { console.log('Kind unmounted') },
       activated () { console.log('Kind 显示')},
       deactivated () { console.log('Kind 隐藏')}
     }
     const Cart = {
      name: 'cart',
      template: `
         <div>
           购物车  <input placeholder="购物车"/>
         </div>
       `,
       created () { console.log('Cart created') },
       mounted () { console.log('Cart mounted') },
       unmounted () { console.log('Cart unmounted') },
       activated () { console.log('Cart 显示')},
       deactivated () { console.log('Cart 隐藏')}
     }
     const User = {
       name: 'user',
       template: `
         <div>
           我的  <input placeholder="我的"/>
         </div>
       `,
       created () { console.log('User created') },
       mounted () { console.log('User mounted') },
      unmounted () { console.log('User unmounted') },
      activated () { console.log('User 显示')},
       deactivated () { console.log('User 隐藏')}
     }
   
     Vue.createApp({
       data () {
         return {
           currentTab: 'Home'
         }
       },
       components: {
         Home,
         Kind,
         Cart,
         User
       }
     }).mount('#app')
   </script>
   </html>

9.4<component>元素

一个用于渲染动态组件或元素的“元组件”

完整案例:08_dynamic/55_component_element.html

<!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>55_component元素</title>
</head>
<body>
  <div id="app">
    <input type="checkbox" v-model="flag" />
    <!-- 条件为真渲染为 a 标签,否则为 span 标签 -->
    <component :is="flag ? 'a' : 'span'">你好</component>

    <component :is="flag ? 'my-com1' : 'my-com2'"></component>
  </div>
</body>
<script src="lib/vue.global.js"></script>
<script>
  const Com1 = {
    template: `<div>com1</div>`
  }
  const Com2 = {
    template: `<div>com2</div>`
  }
  Vue.createApp({
    components: {
      MyCom1: Com1,
      MyCom2: Com2
    },
    data () {
      return {
        flag: false
      }
    }
  }).mount('#app')
</script>
</html>

也可以渲染组件

9.5 DOM 模板解析注意事项(is=“vue:xxx”)-了解

is attribute 用于原生 HTML 元素时,它将被当作 Customized built-in element,其为原生 web 平台的特性。

但是,在这种用例中,你可能需要 Vue 用其组件来替换原生元素,如 DOM 模板解析注意事项所述。你可以在 is attribute 的值中加上 vue: 前缀,这样 Vue 就会把该元素渲染为 Vue 组件(my-row-component为自定义组件):

<table>
  <tr is="vue:my-row-component"></tr>
</table>

完整案例:08_dynamic/56_DOM模板解析注意事项.html

<!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>
</head>
<body>
  <div id="app">
    <table>
      <tr>
        <th>序号</th>
        <th>姓名</th>
      </tr>
      <!-- <my-tr></my-tr> -->
      <tr is="vue:my-tr"></tr>
    </table>
  </div>
</body>
<template id="tr">
  <tr>
    <td>1</td>
    <td>吴大勋</td>
  </tr>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Tr = {
    template: '#tr'
  }

  Vue.createApp({
    components: {
      MyTr: Tr
    }
  }).mount('#app')
</script>
</html>

注意不要使用绑定属性

10.异步组件

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能

10.1 全局API

学习:defineAsyncComponent()

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

完整案例:09_async/57_defineAsyncComponent.html

<!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>异步组件</title>
</head>
<body>
  <div id="app">
    <my-test></my-test>
    <my-com></my-com>
  </div>
</body>
<template id="com">
  <div>异步加载组件</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Com = {
    template: '#com'
  }

  const MyCom = Vue.defineAsyncComponent(() => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(Com)
      }, 3000);
    })
  })

  Vue.createApp({
    components: {
      MyTest: Com,
      MyCom
    }
  }).mount('#app')
</script>
</html>

10.2加载函数

学习:() => import()

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

以后讲解项目时可以用到,需要在脚手架环境中使用(单文件组件中使用

10.3 <Suspense>组件

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

vue3中新增的

完整案例:09_async/58_Suspense.html

<!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>异步组件</title>
</head>
<body>
  <div id="app">
    <my-test></my-test>
    <!-- 组件未加载完毕,显示 正在加载... 加载完毕 显示组件 -->
    <Suspense>
      <my-com></my-com>
      <template #fallback>
        正在加载。。。
      </template>
    </Suspense>
  </div>
</body>
<template id="com">
  <div>异步加载组件</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Com = {
    template: '#com'
  }

  const MyCom = Vue.defineAsyncComponent(() => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(Com)
      }, 3000);
    })
  })

  Vue.createApp({
    components: {
      MyTest: Com,
      MyCom
    }
  }).mount('#app')
</script>
</html>

后期可以和 Transition,KeepAlive,路由等结合使用

11.自定义指令

除了 Vue 内置的一系列指令 (比如 v-modelv-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。

一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。

11.1 自定义指令定义和使用

学习:app.directive()、directives 选项

当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:

完整案例:10_directive/59_自定义指令.html

<!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>自定义指令</title>
</head>
<body>
  <div id="app">
    <input type="text" v-focus>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    directives: { // 局部自定义指令
      focus: {
        mounted (el) {
          el.focus()
        } 
      }
    }
  })

  // 全局自定义指令
  // 指令名称不需要加 v-
  // app.directive('focus', {
  //   mounted (el) { // el 就是当前指令对应的DOM节点
  //     el.focus()
  //   }
  // })

  app.mount('#app')
</script>
</html>

11.2自定义指令钩子

一个指令的定义对象可以提供几种钩子函数 (都是可选的):

vue3相比 vue2,钩子函数做了更新

vue2中一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

// vue3钩子函数
const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

给一个元素设置颜色 v-red v-color=“green”,设置手机号正确为绿色,不正确为红色

完整案例10_directive/60_directives_demo.html

<!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>自定义指令</title>
</head>
<body>
  <!-- 
    给一个元素通过 v-red 设置颜色为红色
    给一个元素通过 v-color=‘’ 设置颜色为自定义颜色
    通过 v-tel 设置输入手机号 格式正确为绿色,格式不正确为红色
  -->
  <div id="app">
    <div v-red>自定义指令 无参数 设置为红色</div>
    <!-- 指令后的 green 需要加引号,否则被当作变量 -->
    <div v-color="'green'">自定义指令 有参数 设置为绿色</div>
    <input type="text" v-model="tel" v-tel>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data () {
      return {
        tel: ''
      }
    },
    directives: {
      tel: {
        // 不实用mounted 钩子函数,因为它只执行1次,输入数据过程中,数据一致更新
        // DOM操作  mounted   updated
        updated (el) {
          if (/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(el.value)) {
            el.style.color = 'green'
          } else {
            el.style.color = 'red'
          }
        }
      }
    }
  })

  app.directive('red', {
    mounted (el) {
      el.style.color = 'red'
    }
  })

  app.directive('color', {
    mounted (el, binding) {
      el.style.color = binding.value
    }
  })

  app.mount('#app')
</script>
</html>

自定义指令更多的用来实现DOM相关操作

上述案例中如果在vue2中,使用 inserted 代替 mounted,使用 update代替updated

自行练习vue2的上述案例

12.插件-了解最好掌握

学习:插件开发与原理(app.use())

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:

import { createApp } from 'vue'

const app = createApp({})

app.use(myPlugin, {
  /* 可选的选项 */
})

一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数:

const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}

插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:

  1. 通过 app.component()app.directive() 注册一到多个全局组件或自定义指令。
  2. 通过 app.provide() 使一个资源可被注入进整个应用。
  3. app.config.globalProperties 中添加一些全局实例属性或方法(区别vue2的场景
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

完整案例:11_plugin/61_plugin_vue3.html

<!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>vue3自定义插件</title>
</head>
<body>
  <div id="app">
    {{ $tran('welcome') }} -  {{ $tran('bye') }}
    <button @click="lang='zh'">中文</button>
    <button @click="lang='en'">英文</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data () { // 根组件提供语言变量
      return {
        lang: 'en'
      }
    }
  })

  // 定义插件
  const I18nPlugin = {
    install (app, options) {
      console.log(app)
      console.log(options)
      // 给 Vue的全局添加实例属性和方法 app.config.globalProperties
      // $tran 自定义函数 可以随意替换,这里定义了什么,模版中就使用什么
      app.config.globalProperties.$tran = function (item) { // welcome bye
        console.log(this.$root)
        // 从options中读取 en / zh
        return options[this.$root.lang][item]
      }
    }
  }

  app.use(I18nPlugin, {
    'zh': { welcome: '欢迎', bye: '拜拜' },
    'en': { welcome: 'welcome', bye: 'bye' }
  })

  app.mount('#app')
</script>
</html>

11_plugin/62_plugin_vue2.html

<!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>62_插件的自定义vue2</title>
</head>
<body>
  <div id="app">
    {{ $t('welcome') }} - {{ $t('bye') }}
    <button @click="lang='zh'">中文</button>
    <button @click="lang='en'">英文</button>
  </div>
</body>
<script src="lib/vue.js"></script>
<script>

  // 定义插件
  const i18nPlugin = {
    install (app, options) {
      console.log(options)
      Vue.prototype.$t = function (item)  { // item welcome
        return options[this.$root.lang][item]
      }
    }
  }
  // 使用插件
  Vue.use(i18nPlugin, {
    'en': { welcome: 'welcome', bye: 'goodbye' },
    'zh': { welcome: '欢迎', bye: '拜拜' }
  })

  new Vue({
    data () {
      return {
        lang: 'en'
      }
    }
  }).$mount('#app')
</script>
</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值