目录

一、基本概念

1. Options API

2. Composition API

二、setup函数

1. setup函数的参数

props

context

栗子 🌰

        父组件

        子组件 

2. setup函数的返回值

代码

效果

三、定义响应式数据的两种方式

1. Reactive API

01 - 栗子 🌰

        代码

        效果 

02 - Reactive判断的API 

2. Ref API ( 常用 )

01 - 基本使用

        代码

        效果 

02 - Ref自动解包

        注意事项

03 - Ref判断的API

04 - 自定义Ref => customRef

        新建customDebounceRef.js文件

        使用

        效果 

四、readonly

1. 概念

2. 使用

五、toRefs && toRef

1. toRefs

代码

效果 

2. toRef 

六、computed

方式一

方式二

01 - 代码 

02 - 效果 

七、setup中使用ref获取元素或组件

1. 获取元素

2. 获取组件

01 - 子组件 

02 - 父组件

八、生命周期函数

九、provide && inject

1. 父组件代码

2. 子组件代码

3. 效果

十、 侦听数据的变化之watch

1. 侦听单个数据源

01 - 监听具体的某个值

02 - 传入响应式对象

        ref对象

        reactive对象 

2. 侦听多个数据源

01 - 代码

02 - 效果

3. watch的选项

代码

十一、侦听数据的变化之watchEffect

1. watchEffect的基本使用

01 - 代码

02 - 效果 

2. watchEffect的停止监听

01 - 代码

02 - 效果 

3. watchEffect清除副作用

01 - 什么是清除副作用

02 - 如何使用

03 - 代码

04 - 效果 

4. watchEffect的执行时机

代码

5. 和watch的区别

十二、自定义指令

1. 自定义指令的简单使用

01 - 默认实现方式

02 - 封装hook实现

        封装hook

        使用

03 - 使用局部指令

        setup函数

        setup语法糖

04 - 使用全局指令

        main.js中注册

        进行抽取

                定义focus.js文件

                定义index.js文件

                在main.js中导入

05 - 效果

2. 指令的生命周期

01 - 代码

02 - 效果

03 - Vue2 与 Vue3对比

3. 指令的参数和修饰符

01 - 代码 

02 - 效果 

4. 时间显示栗子

01 - setup函数

02 - setup语法糖

03 - 效果

十三、script setup语法糖 🍬

1. 概念

2. 顶层的绑定会被暴露给模板

3. 导入的组件直接使用

4. defineProps()

01 - 父组件

02 - 子组件

5. defineEmits()

01 - 子组件

02 - 父组件

6. defineExpose()

01 - 子组件

02 - 父组件 

十四、内置组件

1. Teleport  :  指定挂载位置

01 - 概念

02 - 基本使用

        代码

        效果

03 - 挂载到#app之外的指定元素上 

        index.html

        组件代码

        效果

04 - 多个Teleport

        代码

        效果 

2. defineAsyncComponent  :  异步组件

01 - 使用异步组件的原因

02 - 使用异步组件defineAsyncComponent

       类型一

       类型二 

3. Suspense  :  实验特性

代码

4. 动态组件  :  component

vue2

        01 - 不使用动态组件

        02 - 使用动态组件

vue3

        01 - 不使用动态组件

        02 - 使用动态组件

03 - 效果

十五、Vue插件

1. 对象类型

2. 函数类型 

3. 改写自定义指令 

十六、h函数 

1. 前言

2. 概念

3. 参数

01 - 标签名

02 - 属性

03 - 内容

4. 基本使用

01 - 在render函数选项中

        代码

        效果

02 - 在setup函数选项中

03 - 在setup语法糖中

十七、jsx

1. 配置

vue-cli环境

        01 - 安装插件

        02 - babel.config.js中配置

vite环境

        01 - 安装插件

        02 - vite.config.js中配置

2. jsx 的基本使用

01 - 在render函数中

02 - 在setup函数中

03 - 在setup语法糖中


一、基本概念

1. Options API

在Vue2中, 编写组件的方式是Options API
  • Options API的一大特点就是在对应的属性中编写对应的功能模块
  • 比如data定义数据methods中定义方法computed中定义计算属性watch中监听属性改变,也包括生命周期钩子

但是这种代码有一个很大的弊端

  • 实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性
  • 组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
  • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人)

2. Composition API

为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方在Vue组件中,这个位置就是 setup 函数

setup函数 : 
  • setup其实就是组件的另外一个选项
  • 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项
  • 比如methods、computed、watch、data、生命周期等等

二、setup函数

1. setup函数的参数

setup函数有两个主要的参数 : props 、context

props

props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取

  • 对于定义props的类型,还是和Vue2的规则是一样的,在props选项中定义
  • 并且在template中依然是可以正常去使用props中的属性
  • 如果在setup函数中想要使用props,那么不可以通过 this 去获取 ( 没有this )
    • 内部调用setup函数的时候是直接调用的,没有绑定this,所以没有
    • props有直接作为参数传递到setup函数中,所以可以直接通过参数来使用即可

context

context,也称之为是一个SetupContext,它里面包含三个属性

  • attrs:所有的非prop的attribute ( 传了属性过来,但是没用props接受的,会在这里 )
  • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用)
  • emit:当我们组件内部需要发出事件时会用到emit

栗子 🌰

        父组件

<template>
  <HelloWorld msg="Welcome to Your Vue.js App" class="app-attr" />
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
export default {
  name: 'App',
  components: {
    HelloWorld
  }
};
</script>
<style></style>

        子组件 

<template>
  <div class="hello">
    <!-- 使用接受过来的参数 -->
    {{ msg }}
  </div>
</template>
<script>
export default {
  name: 'HelloWorld',
  // 组件接受的参数
  props: {
    msg: String
  },
  // 发射的事件这里可以标注一下
  emits:['change'],
  setup(props, context) {
    // 这样可以拿到传递过来的msg的值
    console.log(props.msg);
    // attrs
    console.log(context.attrs);
    // 发射事件
    context.emit('change');
  }
};
</script>
<style scoped></style>

2. setup函数的返回值

  • setup的返回值可以在模板template中被使用
  • 也就是说可以通过setup的返回值来替代data选项

最后导出的一定要是个对象

代码

<template>
  <div class="hello">
    <!-- 使用导出的变量 -->
    <h1>{{ count }}</h1>
    <!-- 使用导出的方法 -->
    <button @click="increment">+ 1</button>
  </div>
</template>
<script>
export default {
  name: 'HelloWorld',
  setup() {
    // 定义普通的变量,可以被正常使用
    // 缺点 : 数据不是响应式的
    let count = 100;
    // 定义方法
    const increment = () => {
      count++;
      console.log(count);
    };
    // 导出
    return {
      count,
      increment
    };
  }
};
</script>
<style scoped></style>

效果

因为只是定义了个变量,然后导出了,并没有使它响应式

三、定义响应式数据的两种方式

1. Reactive API

如果想为在setup中定义的数据提供响应式的特性,那么可以使用reactive的函数

ps : 如果传入一个基本数据类型(String、Number、Boolean)会报一个警告

 

应用场景 : reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型,最好相互有关联的数据时使用

reactive : 

  • 这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集
  • 数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)
  • 事实上,我们编写的data选项,也是在内部交给了reactive函数将其变成响应式对象的

01 - 栗子 🌰

        代码

<template>
  <div class="hello">
    <!-- 这样使用即可 -->
    <h1>{{ state.count }}</h1>
    <button @click="increment">+ 1</button>
  </div>
</template>
<script>
// 从vue中导入reactive
import { reactive } from 'vue';
export default {
  name: 'HelloWorld',
  setup() {
    // 使用reactive,会返回一个响应式对象
    const state = reactive({
      // 在对象中编写自己所需要的属性
      count: 100
    });
    const increment = () => {
      // 这样使用
      state.count++;
      console.log(state.count);
    };
    return {
      // 导出响应式state对象
      state,
      increment
    };
  }
};
</script>
<style scoped></style>

        效果 

02 - Reactive判断的API 

  • isProxy : 检查对象是否是由 reactive 或 readonly创建的 proxy
  • isReactive : 检查对象是否是由 reactive创建的响应式代理,如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true
  • isReadonly : 检查对象是否是由 readonly 创建的只读代理
  • toRaw : 返回 reactive 或 readonly 代理的原始对象建议保留对原始对象的持久引用。请谨慎使用)
  • shallowReactive : 建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象),只响应第一层
  • shallowReadonly : 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)只检查第一层

2. Ref API ( 常用 )

Ref函数 : 定义简单类型的数据,也可以定义复杂类型的数据

 

应用场景 : 定义一些简单的数据,或者从接口中获得的数据

Ref : 

  • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源
  • 它内部的值是在ref的 value 属性中被维护的
  • 不管传入的是基本类型还是引用类型,都放在.value中

使用的时候是用 .value,但是有两个注意事项

  • 模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以并不需要在模板中通过 ref.value 的方式,直接使用即可
  • setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,依然需要使用 ref.value的方式

01 - 基本使用

        代码

<template>
  <div class="hello">
    <!-- 这样使用即可,不需要使用count.value,会自动解包,取出其中的value -->
    <h1>{{ count }}</h1>
    <button @click="increment">+ 1</button>
  </div>
</template>
<script>
// 从vue中导入ref
import { ref } from 'vue';
export default {
  name: 'HelloWorld',
  setup() {
    // 使用Ref,会返回一个响应式对象
    let count = ref(100);
    const increment = () => {
      // 这样使用,需要使用 .value
      count.value++;
      console.log(count.value);
    };
    return {
      // 直接导出count即可
      count,
      increment
    };
  }
};
</script>
<style scoped></style>

        效果 

02 - Ref自动解包

就是不用在template中使用.value

        注意事项

1. 模板中的解包是浅层的解包,如果放到了对象中,则接包不了

2. 如果把ref返回的对象又放入到reactive中,那么也会自动解包

03 - Ref判断的API

  • isRef : 判断值是否是一个ref对象
  • unref : 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法
    • 如果参数是一个 ref,则返回内部值否则返回参数本身
    • 这是 val = isRef(val) ? val.value : val 的语法糖函数
  • shallowRef : 创建一个浅层的ref对象
  • triggerRef : 手动触发和 shallowRef 相关联的副作用

04 - 自定义Ref => customRef

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制
  • 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数
  • 并且应该返回一个带有 get 和 set 的对象
  • 简单写一个带防抖的ref 

        新建customDebounceRef.js文件

// 导入customRef
import { customRef } from 'vue';

export default function (value) {
  let timer = null;
  // track收集依赖    trigger更新数据
  return customRef((track, trigger) => {
    return {
      get() {
        // 先收集依赖
        track();
        // 把数据返回
        return value;
      },
      set(newValue) {
        // 防抖
        clearTimeout(timer);
        timer = setTimeout(() => {
          // 设置数据
          value = newValue;
          // 更新数据
          trigger();
        }, 1000);
      }
    };
  });
}

        使用

<template>
  <div>
    <input type="text" v-model="message" />
    <h1>{{ message }}</h1>
  </div>
</template>

<script>
import customDebounceRef from '../hook/customDebounceRef';
export default {
  setup() {
    // 使用自定义的customRef
    let message = customDebounceRef('hello');
    return {
      message
    };
  }
};
</script>
<style lang="scss" scoped></style>

        效果 

 

四、readonly

1. 概念

通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,传入给其他地方(组件)的这个 响应式对象希望在另外一个地方(组件)被使用

但是不能被修改 

  • readonly的方法
  • readonly会返回原始对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改)
在开发中常见的readonly方法会传入三个类型的参数:
  • 类型一:普通对象
  • 类型二:reactive返回的对象
  • 类型三:ref的对象

2. 使用

在readonly的使用过程中,有如下规则 : 

  • readonly返回的对象都是不允许修改
  • 但是经过readonly处理的原来的对象是允许被修改的
    • 比如 const info = readonly(obj),info对象是不允许被修改的
    • obj被修改时,readonly返回的info对象也会被修改
    • 但是不能去修改readonly返回的对象info

本质上就是readonly返回的对象的setter方法被劫持了

五、toRefs && toRef

1. toRefs

如果使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive 返回的state对象数据都不再是响应式

如何改成响应式呢,Vue提供了一个toRefs的函数

可以将reactive返回的对象中的属性都转成ref,这样解构出来的就是响应式的了

代码

<template>
  <div class="hello">
    <h1>{{ age }}</h1>
    <button @click="increment">+age</button>
  </div>
</template>
<script>
// 从vue中导入ref
import { reactive, ref, readonly, toRefs } from 'vue';
export default {
  name: 'HelloWorld',
  setup() {
    const info = reactive({ name: 'star', age: 18 });
    // 使用toRefs包裹需要结构的reactive对象,这样解构出来的值也是响应式的
    let { name, age } = toRefs(info);
    const increment = () => {
      info.age++;
      // 👆这样都可以修改age,都是响应式的👇
      // 相当于已经建立了链接,任何一个修改都会引起另外一个变化
      age.value++;
      console.log(age, info.age);
    };
    return {
      name,
      age,
      increment
    };
  }
};
</script>
<style scoped></style>

效果 

2. toRef 

如果只希望转换reactive对象中的其中某个属性为ref, 那么可以使用toRef的方法

 

ps : 这个效率会更高点

六、computed

Vue2

使用的是computed属性

Vue3

需要在setup函数中使用computed方法来编写一个计算属性

方式一

接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象

<template>
  <!-- coderstar -->
  {{ fullName }}
  <!-- 一般 -->
  {{ scoreState }}
</template>

<script>
import { computed, reactive, ref } from 'vue';
export default {
  name: 'App',
  setup() {
    const names = reactive({
      firstName: 'coder',
      lastName: 'star'
    });
    // 直接使用getter函数,正常来说都这么使用
    const fullName = computed(() => names.firstName + names.lastName);

    const score = ref(88);
    const scoreState = computed(() => (score.value > 90 ? '优秀' : '一般'));
    
    return {
      fullName,
      scoreState
    };
  }
};
</script>

方式二

接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象

01 - 代码 

<template>
  {{ fullName }}
  <button @click="changeName">change</button>
</template>

<script>
import { computed, reactive } from 'vue';
export default {
  name: 'App',
  setup() {
    const names = reactive({
      firstName: '冲啊',
      lastName: '迪迦奥特曼'
    });
    // 会返回一个ref对象
    const fullName = computed({
      set(newValue) {
        const tempNames = newValue.split(' ');
        names.firstName = tempNames[0];
        names.lastName = tempNames[1];
      },
      get() {
        return names.firstName + names.lastName;
      }
    });
    // 设置值
    const changeName = () => {
      fullName.value = fullName.value === '冲啊迪迦奥特曼' ? '神秘的 宇宙人' : '冲啊 迪迦奥特曼';
    };

    return {
      fullName,
      changeName
    };
  }
};
</script>

02 - 效果 

七、setup中使用ref获取元素或组件

要定义一个ref对象,绑定到元素或者组件的ref属性上即可

1. 获取元素

<template>
  <!-- 1. 指定ref -->
  <h2 ref="titleRef">我是迪迦</h2>
</template>

<script>
import { onMounted, ref } from 'vue';
export default {
  name: 'App',
  setup() {
    // 2. 生成ref对象
    const titleRef = ref();

    // 4. 可以在生命周期中获取到值
    onMounted(() => {
      console.log(titleRef.value); // <h2>我是迪迦</h2>
    });

    return {
      // 3. 返回出去,会自动匹配到对应的ref的
      titleRef
    };
  }
};
</script>

2. 获取组件

01 - 子组件 

<template>
  <div>我是子组件</div>
</template>

<script>
export default {
  name: 'home-layout',
  setup() {
    const showMessage = () => {
      console.log('home-layout function exection');
    };
    return { showMessage };
  }
};
</script>

02 - 父组件

<template>
  <!-- 1. 指定ref -->
  <home ref="homeCompRef" />
</template>

<script>
import { onMounted, ref } from 'vue';
import home from './home.vue';
export default {
  name: 'App',
  components: { home },
  setup() {
    // 2. 生成ref对象
    const homeCompRef = ref();

    // 4. 可以在生命周期中获取到值
    onMounted(() => {
      console.log(homeCompRef.value); // proxy对象
      console.log(homeCompRef.value.$el); // <div>我是子组件</div>
      homeCompRef.value.showMessage(); // 调用子组件方法
    });

    return {
      // 3. 返回出去,会自动匹配到对应的ref的
      homeCompRef
    };
  }
};
</script>

八、生命周期函数

setup中可以直接使用导入的onX函数注册生命周期,并且同一个生命周期可以使用多次

 

ps : 之前在created和beforeCreated生命周期中编写的代码,直接在setup函数中编写即可

九、provide && inject

provide可以传入两个参数 : 

  • name:提供的属性名称
  • value:提供的属性值

inject可以传入两个参数 : 

  • 对应provide传过来的name值
  • 默认值

1. 父组件代码

<template>
  <h1>APP count: {{ count }}</h1>
  <button @click="change">APP button</button>
  <demo />
</template>

<script>
import { ref, provide, readonly } from 'vue'
import demo from './components/demo.vue'

export default {
  name: 'App',
  components: {
    demo
  },
  setup() {
    let count = ref(100)
    // 第一个参数key  第二个参数值,不让子组件随便修改,用readonly包裹一下
    provide('count', readonly(count))
    const change = () => count.value++
    return {
      count,
      change
    }
  }
}
</script>

<style></style>

2. 子组件代码

<template>
  <h2>demo count:{{ count }}</h2>
  <button @click="change">demo change</button>
</template>
<script>
import { ref, inject } from 'vue'
export default {
  setup() {
    // 接收   第二个参数可以给一个默认值
    let count = inject('count', '')
    // 因为设置了readOnly 所以更改不了
    const change = () => count.value++
    return {
      count,
      change
    }
  }
}
</script>

<style lang="scss" scoped></style>

3. 效果

4. 例子

父组件接受route的参数传递给子组件

父组件

const route = useRoute();
// 基金代码
provide(
  'fundCode',
  computed(() => route.query.fundCode)
);
// 基金类型
provide(
  'fundType',
  computed(() => route.query.fundType)
);

子组件

// 基金代码
const fundCode = inject('fundCode').value;
// 基金类型
const fundType = inject('fundType').value;

十、 侦听数据的变化之watch

watch : 

  • watch需要侦听特定的数据源,并在回调函数中执行副作用
  • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调
  • 与watchEffect的比较,watch允许我们懒执行副作用(第一次不会直接执行)
  • 更具体的说明当哪些状态发生变化时,触发侦听器的执行
  • 访问侦听状态变化前后的值

1. 侦听单个数据源

watch侦听函数的数据源有两种类型:

  • 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref)
  • 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref)

01 - 监听具体的某个值

用来监听ref 或者 reactive对象中的具体属性可使用

02 - 传入响应式对象

        ref对象

<template>
  <div>
    <h1>
      {{ star }}
    </h1>
    <button @click="change">change</button>
  </div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
  setup() {
    /**
     * 一、如果ref响应式对象中的是基本数据类型
     */
    const star = ref('123');
    // const star = ref('123');
    // 改变
    const change = () => (star.value = star.value === 'coder' ? '123' : 'coder');
    // 那么直接这样传入即可,拿到的是value值本身
    watch(star, (newValue, oldValue) => {
      console.log('newValue:', newValue, 'oldValue:', oldValue);
    });

    /**
     * 二、如果ref对象中的是对象数据,那么也需要结构 () => ({...star.value}),如果不结构,拿到的是ref对象
     */
    const star = ref({ name: '123', age: 14 });
    // 改变
    const change = () => (star.value.name = star.value.name === 'coder' ? 'pppp' : 'coder');
    // 那么需要结构,如果不结构,拿到的是ref对象
    watch(
      () => ({ ...star.value }),
      (newValue, oldValue) => {
        console.log('newValue:', newValue, 'oldValue:', oldValue);
      }
    );

    return {
      star,
      change
    };
  }
};
</script>

<style lang="scss" scoped></style>

        reactive对象 

<template>
  <div>
    <h1>
      {{ star.name }}
    </h1>
    <button @click="change">change</button>
  </div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
  setup() {
    const star = reactive({ name: '123', age: 15 });
    // 改变
    const change = () => (star.name = star.name === 'coder' ? '123' : 'coder');
    /**
     * 一、如果直接传入reactive对象,那么打印出的也是两个reactive对象
     */
    watch(star, (newValue, oldValue) => {
      console.log('newValue:', newValue, 'oldValue:', oldValue);
    });
    /**
     * 二、如果希望获得普通对象,结构一下
     */
    watch(
      () => ({ ...star }),
      (newValue, oldValue) => {
        console.log('newValue:', newValue, 'oldValue:', oldValue);
      }
    );
    return {
      star,
      change
    };
  }
};
</script>

<style lang="scss" scoped></style>

2. 侦听多个数据源

01 - 代码

<template>
  <div>
    <h1>
      {{ star.name }}
    </h1>
    <button @click="change">change</button>
  </div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
  setup() {
    // ref对象
    const coder = ref('know');
    // reactive对象
    const star = reactive({ name: '123', age: 15 });
    // 改变
    const change = () => {
      star.name = 'coder';
      coder.value = 'unKnow';
    };
    // 传入数组即可                      // 对应结构
    watch([coder, () => ({ ...star })], ([newCoder, newStar], [oldCoder, oldStar]) => {
      console.log(newCoder, newStar, '----', oldCoder, oldStar);
    });

    return {
      star,
      change
    };
  }
};
</script>

<style lang="scss" scoped></style>

02 - 效果

3. watch的选项

配置第三个参数 : 

  • deep : 是否深度监听
  • immediate : 是否立即执行

代码

<template>
  <div>
    <h1>
      {{ info.name }}
    </h1>
  </div>
</template>
<script>
import { ref, reactive, watch } from 'vue'
export default {
  setup() {
    // reactive对象
    const info = reactive({
      name: '123',
      age: 15,
      friend: { name: '456' }
    })
    watch(
      () => {
        const obj = { ...info }
        // 因为有两层,需要自己结构下,否则返回的是proxy
        obj.friend = { ...obj.friend }
        return obj
      },
      (newValue, oldValue) => {
        console.log(newValue, oldValue)
      },
      {
        // 如果有多层,需要加上deep
        deep: true,
        // 立即执行
        immediate: true
      }
    )

    return {
      info
    }
  }
}
</script>

<style lang="scss" scoped></style>

十一、侦听数据的变化之watchEffect

1. watchEffect的基本使用

watchEffect : 

  • 自动收集响应式数据的依赖
  • watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖
  • 只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行

01 - 代码

<template>
  <div>
    <h1>{{ name }} - {{ age }}</h1>
    <button @click="changeName">changeName</button>
    <button @click="changeAge">changeAge</button>
  </div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    let name = ref('star');
    let age = ref(18);

    const changeName = () => (name.value === 'star' ? (name.value = 'xuanyu') : (name.value = 'star'));
    const changeAge = () => age.value++;

    watchEffect(() => {
      // 因为这里只使用了name,所以只会监听name,如果把age也写进来,那么两个都会监听
      console.log('name:', name.value);
    });

    return { name, age, changeName, changeAge };
  }
};
</script>

<style lang="scss" scoped></style>

02 - 效果 

2. watchEffect的停止监听

假如,某些情况想要停止监听,那么可以获取watchEffect的返回值函数,调用该函数即可

01 - 代码

<template>
  <div>
    <h1>{{ name }} - {{ age }}</h1>
    <button @click="changeName">changeName</button>
    <button @click="changeAge">changeAge</button>
  </div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    let name = ref('star');
    let age = ref(18);

    const changeName = () => (name.value === 'star' ? (name.value = 'xuanyu') : (name.value = 'star'));
    // 获取返回值
    const stopWatchEffect = watchEffect(() => {
      // 自动监听age
      console.log('age:', age.value);
    });
    const changeAge = () => {
      age.value++;
      if (age.value > 22) {
        // 停止监听
        stopWatchEffect();
      }
    };

    return { name, age, changeName, changeAge };
  }
};
</script>

<style lang="scss" scoped></style>

02 - 效果 

3. watchEffect清除副作用

01 - 什么是清除副作用

  • 比如在开发中可能需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,停止了侦听器,或者侦听器侦听函数被再次执行了
  • 那么上一次的网络请求应该被取消掉,这个时候就可以清除上一次的副作用

02 - 如何使用

  • 在给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
  • 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数
  • 可以在传入的回调函数中,执行一些清除工作

03 - 代码

<template>
  <div>
    <h1>{{ name }} - {{ age }}</h1>
    <button @click="changeName">changeName</button>
    <button @click="changeAge">changeAge</button>
  </div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    let name = ref('star');
    let age = ref(18);

    const changeName = () => (name.value === 'star' ? (name.value = 'xuanyu') : (name.value = 'star'));
    // 获取返回值
    const stopWatchEffect = watchEffect((onInvalidate) => {
      // 自动监听age
      console.log('age:', age.value);
      const timer = setTimeout(() => {
        console.log('1s后执行');
      }, 1000);
      // 取消副作用,清除上一次的定时器
      onInvalidate(() => {
        clearTimeout(timer);
      });
    });
    const changeAge = () => {
      age.value++;
      if (age.value > 22) {
        // 停止监听
        stopWatchEffect();
      }
    };

    return { name, age, changeName, changeAge };
  }
};
</script>

<style lang="scss" scoped></style>

04 - 效果 

4. watchEffect的执行时机

  • pre : 默认值,它会在元素 挂载 或者 更新 之前执行
  • post : 元素 挂载 或者 更新 之后执行
  • sync : 强制同步一起执行,效率很低,不推荐

一般来说,如果需要等dom挂载完后操作dom元素的时候,传post即可

代码

<template>
  <div>
    <h1 ref="hRef">star</h1>
  </div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
  setup() {
    // 先声明一个null的ref,等到元素挂载后,会自动赋值过来
    const hRef = ref(null);

    watchEffect(
      () => {
        console.log(hRef.value);
      },
      {
        // 设定元素挂载再执行这个函数
        flush: 'post'
      }
    );
    return {
      hRef
    };
  }
};
</script>

<style lang="scss" scoped></style>

5. 和watch的区别

  • wathc必须指定数据源,watchEffect自动收集依赖
  • watch监听到改变,可以拿到改变前后的值,watchEffect只能拿到最新的值
  • watch第一次默认不执行,watchEffect默认直接执行一次

十二、自定义指令

1. 自定义指令的简单使用

Vue中自带的指令例如v-show、v-for、v-model等等,除了使用这些指令之外,Vue 也允许我们来自定义自己的指令

ps : 一般需要对dom元素进行底层操作时使用

自定义指令

  • 局部指令 : 组件中通过directives选项设定,只能在当前组件中使用
  • 全局指令 : app的directive方法,可以在任意组件中使用

栗子 : 实现input元素挂在完成后自动获得焦点

01 - 默认实现方式

<!-- ? 模块 -->
<template>
  <div class="app-view">
    <input type="text" ref="inputRef" />
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue';

const inputRef = ref(null);

onMounted(() => {
  inputRef.value.focus();
});
</script>

<style scoped></style>

02 - 封装hook实现

        封装hook

import { onMounted, ref } from 'vue';

export default function useInput() {
  const inputRef = ref(null);

  onMounted(() => {
    inputRef.value.focus();
  });

  return { inputRef };
}

        使用

<!-- ? 模块 -->
<template>
  <div class="app-view">
    <input type="text" ref="inputRef" />
  </div>
</template>

<script setup>
import useInput from './hook/useInput.js';

const { inputRef } = useInput();
</script>

<style scoped></style>

03 - 使用局部指令

自定义一个 v-focus 的局部指令

        setup函数

  • 这个自定义指令实现非常简单,只需要在组件选项中使用 directives 即可
  • 它是一个对象,在对象中编写自定义指令的名称(注意:这里不需要加v-)
  • 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,可以在其中完成操作
<template>
  <!-- 使用自定义指令 -->
  <input type="text" v-focus />
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
  // 定义自定义指令
  directives: {
    // 指令名称,注 : 不需要加 v-
    focus: {
      // 这里写指令的生命周期函数
      // 使用自定义指令的生命周期,挂载后访问
      mounted(el, bindings, vnode, preVnode) {
        el?.focus()
      }
    }
  },
  setup() {}
}
</script>

<style></style>

        setup语法糖

  • 直接定一个对象,该对象以v开头,后面接上指令的名称,用驼峰来书写
<!-- ? 模块 -->
<template>
  <div class="app-view">
    <input type="text" v-focus-a />
  </div>
</template>

<script setup>
// 定义自定义指令
const vFocusA = {
  // 使用指令生命周期,当被绑定的元素插入到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus();
  }
};
</script>

<style scoped></style>

04 - 使用全局指令

        main.js中注册

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 指令名称
app.directive('focus', {
  // 使用自定义指令的生命周期,挂载后访问
  mounted(el, bindings, vnode, preVnode) {
    el?.focus()
  }
})

app.mount('#app')

        进行抽取

注册directives文件夹

                定义focus.js文件

// 用途:聚焦元素
// Usage: <input v-focus />
export default function directiveFocus(app) {
  app.directive('focus', {
    mounted(el) {
      el?.focus();
    }
  });
}

                定义index.js文件

import directiveFocus from './focus';
// ....

// 用途:注册所有指令
export default function installDirectives(app) {
  // 注册聚焦指令
  directiveFocus(app);

  // 注册其他指令
  // ......
}

                在main.js中导入

import { createApp } from 'vue';

import App from './App.vue';
// 1. 导入指令方法
import installDirectives from './directives';

const app = createApp(App);
// 2。 注册所有指令
installDirectives(app);

app.mount('#app');

05 - 效果

2. 指令的生命周期

一个指令定义的对象,Vue提供了如下的几个钩子函数

  • created : 在绑定元素的 attribute 或事件监听器被应用之前调用
  • beforeMount : 当指令第一次绑定到元素并且在挂载父组件之前调用
  • mounted : 在绑定元素的父组件被挂载后调用 ( 常用 )
  • beforeUpdate : 在更新包含组件的 VNode 之前调用
  • updated : 在包含组件的 VNode 及其子组件的 VNode 更新后调用
  • beforeUnmount : 在卸载绑定元素的父组件之前调用
  • unmounted : 当指令与元素解除绑定且父组件已卸载时,只调用一次

01 - 代码

<!-- ? 模块 -->
<template>
  <div class="app-view">
    <h2 v-if="isShow" v-star>count : {{ count }}</h2>
  </div>
</template>

<script setup>
import { ref } from 'vue';
// 指令
const vStar = {
  // 自动执行
  created() {
    console.log('created : 第一个执行');
  },
  beforeMount() {
    console.log('beforeMount : 第二个执行');
  },
  mounted() {
    console.log('mounted : 第三个执行');
  },

  // 内部或者说子组件发生变化时,自动执行
  beforeUpdate() {
    console.log('beforeUpdate : 更新前执行');
  },
  updated() {
    console.log('updated : 更新后执行');
  },

  // 组件销毁时,自动执行
  beforeUnmount() {
    console.log('beforeUnmount : 销毁前执行');
  },
  unmounted() {
    console.log('unmounted : 销毁前执行');
  }
};

// 数据
const count = ref(0);
// 控制隐藏和销毁
const isShow = ref(true);

// 更改内容后,执行 beforeUpdate和updated
setInterval(() => {
  count.value++;
  if (count.value > 3) {
    // 隐藏
    isShow.value = false;
  }
}, 1000);
</script>

<style scoped></style>

02 - 效果

03 - Vue2 与 Vue3对比

vue2 升级到 vue3 ,自定义指令的生命周期钩子函数发生了改变

3. 指令的参数和修饰符

  • 后面跟着的是  =>  参数
  • 后面跟着的是  =>  修饰符
  • = 后面跟着的是  => 

01 - 代码 

<!-- ? 模块 -->
<template>
  <div class="app-view">
    <h2 v-star:p1.y1.y2="'哈哈哈'">count</h2>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const vStar = {
  // el: 指令所绑定的元素,可以用来直接操作 DOM
  // binding: 一个对象,包含以下属性
  // vnode: Vue 编译生成的虚拟节点
  // prevVnode: 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用
  mounted(el, binding, vnode, prevVnode) {
    console.log('元素', el);
    console.log(binding);
  }
};
</script>

<style scoped></style>

02 - 效果 

4. 时间显示栗子

这里使用了dayjs,可以 npm install dayjs 安装一下

01 - setup函数

<template>
  <h1 v-fomat-time="timeFormatType">{{ timeStamp }}</h1>
</template>
<script>
import { ref } from 'vue'
import dayJs from 'dayjs'
export default {
  directives: {
    'fomat-time': {
      mounted(el, bindings) {
        // 默认显示时间类型
        let formatType = bindings.value
        console.log(formatType)
        // 转换成number类型
        let time = el.textContent.length === 10 ? el.textContent * 1000 : el.textContent * 1;
        // 格式化
        el.textContent = dayJs(time).format(formatType)
        setInterval(() => {
          // 定时器
          time = dayJs(new Date().valueOf()).format(formatType)
          el.textContent = time
        }, 1000)
      }
    }
  },
  setup() {
    // 设置初始时间戳
    const timeStamp = ref(new Date().valueOf())

    const timeFormatType = ref('YYYY-MM-DD HH:mm:ss')

    return {
      timeStamp,
      timeFormatType
    }
  }
}
</script>

<style>
h1 {
  display: inline-block;
  color: transparent;
  -webkit-background-clip: text;
  background-image: linear-gradient(to right, red, blue);
}
</style>

02 - setup语法糖

<template>
  <h1 v-fomat-time="timeFormatType">{{ timeStamp }}</h1>
</template>
<script setup>
import { ref } from 'vue';
import dayJs from 'dayjs';

// 设置初始时间戳
const timeStamp = ref(new Date().valueOf());
// 设置初始时间格式
const timeFormatType = ref('YYYY-MM-DD HH:mm:ss');

// 自定义时间格式化指令
const vFomatTime = {
  mounted(el, bindings) {
    // 获取定义的时间格式
    const { value: timeFormatType } = bindings;
    // 转换成number类型
    let time = el.textContent.length === 10 ? el.textContent * 1000 : el.textContent * 1;
    // 使用dayJs,根据时间格式来格式化时间,并赋值给el
    el.textContent = dayJs(time).format(timeFormatType);
    // 定时器,每隔一秒,重新赋值给el
    setInterval(() => {
      time = dayJs(new Date().valueOf()).format(timeFormatType);
      el.textContent = time;
    }, 1000);
  }
};
</script>

<style>
h1 {
  display: inline-block;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  background-image: linear-gradient(to right, red, blue);
}
</style>

03 - 效果

十三、script setup语法糖 🍬

1. 概念

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,当同时使用 SFC 与组合式 API 时则推荐该语法

  • 更少的样板内容,更简洁的代码
  • 能够使用纯 Typescript 声明 prop 和抛出事件
  • 更好的运行时性能
  • 更好的 IDE 类型推断性能

需要将 setup attribute 添加到 <script> 代码块上

<script setup>
console.log('哈哈哈');
</script>

里面的代码会被编译成组件 setup() 函数的内容 : 

  • 这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同
  • <script setup> 中的代码会在每次组件实例被创建的时候执行

2. 顶层的绑定会被暴露给模板

当使用 <script setup> 的时候,任何在 <script setup> 声明的

顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 
都能在模板中直接使用

<template>
  <div>{{ mes }}</div>
  <button @click="addClick">按钮</button>
</template>

<!-- 1. 这里加上setup属性 -->
<script setup>
import { ref } from 'vue';

// 定义数据后,template中可以直接使用,无需返回
const mes = ref(0);
// 定义的方法也是,直接可被使用
const addClick = () => {
  console.log('hahah');
};
</script>

3. 导入的组件直接使用

<template>
  <!-- 2. 直接使用,不用通过compoents注册 -->
  <my-home></my-home>
</template>

<script setup>
// 1. 这是导入的组件
import myHome from './myHome.vue';
</script>

4. defineProps()

defineProps  =>  用来接收从父组件传递过来的数据

01 - 父组件

<template>
  <my-home name="hello" :age="18"></my-home>
</template>

<script setup>
import myHome from './myHome.vue';
</script>

02 - 子组件

<template>
  <div>{{ name }} - {{ age }}</div>
</template>

<script setup>
// defineProps是内置组件,可以直接使用,不用导入
// 可以接收一下返回的props对象,也可以不用
const props = defineProps({
  name: {
    type: String,
    default: ''
  },
  age: {
    type: Number,
    default: 0
  }
});
console.log(props); // Proxy {name: 'hello', age: 18}
</script>

5. defineEmits()

defineProps  =>  用来发射事件给父组件

01 - 子组件

<template>
  <button @click="btnClick">发送</button>
</template>

<script setup>
// 1. 注册一下发射的事件
const emits = defineEmits(['btnClick']);
// 2. 监听按钮的点击
const btnClick = () => {
  // 3. 发射
  emits('btnClick', '我发射了');
};
</script>

02 - 父组件

<template>
  <!-- 1. 监听子组件发射来的事件 -->
  <my-home @btnClick="handleClick"></my-home>
</template>

<script setup>
import myHome from './myHome.vue';

// 2. 获取子组件传递过来的值
const handleClick = (message) => {
  console.log(message); // 我发射了
};
</script>

6. defineExpose()

defineExpose  =>  用来暴露数据

ps : 使用 <script setup> 的组件是默认关闭的

01 - 子组件

<script setup>
const foo = () => {
  console.log('foo');
};
// 暴露出去,才可以被访问到
defineExpose({
  foo
});
</script>

02 - 父组件 

<template>
  <!-- 1. 定义ref -->
  <my-home ref="myHomeRef"></my-home>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import myHome from '../../../Vue3/06_阶段六-Vue3全家桶实战/code/04_learn_composition/src/11_script_setup语法/myHome.vue';
// 2. 定义名称一样
const myHomeRef = ref();
onMounted(() => {
  // 3. 在生命周期中访问
  console.log(myHomeRef.value);
});
</script>

十四、内置组件

1. Teleport  :  指定挂载位置

01 - 概念

在某些情况下,希望组件不是挂载在当前组件树上的,可能是移动到Vue app之外的其他位置

  • 比如移动到body元素上,或者我们有其他的div#app之外的元素
  • 可以通过teleport来完成

Teleport : 

  • 它是一个Vue提供的内置组件,类似于react的Portals
  • teleport 翻译过来是心灵传输、远距离运输的意思,有两个属性
    • to : 指定将其中的内容移动到的目标元素,可以使用选择器
    • disabled : 是否禁用 teleport 的功能

02 - 基本使用

        代码

<template>
  <div class="app-view">
    <!-- 把该组件挂载到body元素上 -->
    <teleport to="body">
      <h1>Teleport</h1>
    </teleport>
  </div>
</template>
<script setup></script>

<style>
h1 {
  display: inline-block;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  background-image: linear-gradient(to right, red, green, pink, yellow, blue);
}
</style>

        效果

03 - 挂载到#app之外的指定元素上 

        index.html

        组件代码

<template>
  <div class="app-view">
    <!-- 把该组件挂载到#star元素上 -->
    <teleport to="#star">
      <h1>Teleport</h1>
    </teleport>
    <div class="a">
      <div class="c"></div>
    </div>
  </div>
  <!-- 把该组件挂载到.b元素上 -->
  <teleport to=".b">
    <h1>Teleport123</h1>
  </teleport>
  
  <!-- 文档上说是挂载到#app之外的元素,可是我发现自己内部的也可以指定,emmmm,优先是从内部一层层往外找的 -->
  <!-- 把该组件挂载到.c元素上... -->
  <teleport to=".c">
    <h1>Teleport123</h1>
  </teleport>
</template>
<script setup></script>

<style>
h1 {
  display: inline-block;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  background-image: linear-gradient(to right, red, green, pink, yellow, blue);
}
</style>

        效果

04 - 多个Teleport

会合并,谁先谁在前面

        代码

<template>
  <div class="app-view">
    <!-- 把该组件挂载到#star元素上 -->
    <teleport to="#star">
      <h1>Teleport</h1>
    </teleport>
  </div>
  <!-- 把该组件挂载到#star元素上 -->
  <teleport to="#star">
    <h1>Teleport123</h1>
  </teleport>
</template>
<script setup></script>

<style>
h1 {
  display: inline-block;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  background-image: linear-gradient(to right, red, green, pink, yellow, blue);
}
</style>

        效果 

2. defineAsyncComponent  :  异步组件

01 - 使用异步组件的原因

webpack的打包过程

  • 默认情况下,因为组件和组件之间是通过模块化直接依赖的,所以webpack打包时会将组件模块打包到一起,比如(app.js文件中)
  • 对于一些不需要立即使用的组件,可以对它们进行拆分,拆成小的代码块chunk.js
  • 这些chunk.js会在需要的时候才会去请求服务器加载,然后运行
  • webpack是如何进行分包的呢,是使用import('  ')函数

如果我们的项目过大,对于某些组件希望通过异步的方式来进行加载( 目的是进行分包处理 )

在vue中提供了一个函数  =>  defineAsyncComponent异步组件函数

 

tip : 异步组件在路由中使用的很多 

02 - 使用异步组件defineAsyncComponent

defineAsyncComponent : 接受两种类型的参数

  • 类型一 : 工厂函数,该工厂函数返回一个promise对象 ( 常用 )

  • 类型二 : 接受一个对象类型,对异步函数进行配置( 不常用 )

       类型一

// 导入vue中的方法
import { defineAsyncComponent } from 'vue';
 
// 普通组件
import profile from './pages/profile.vue';

// 异步组件
const profile = defineAsyncComponent(() => import('./pages/profile.vue'));

       类型二 

const profile = defineAsyncComponent({
  // 工厂函数
  loader: () => import('./pages/profile.vue'),
  // 加载过程中显示的组件Loading,用来占位的,用普通组件的方式导入即可
  loadingComponent: Loading,
  // 加载失败时显示的组件Error
  errorComponent: Error,
  // 在显示loadingComponent组件之前的延迟,等待多长时间 | 默认值200ms
  delay: 2000,
  // 如果提供了timerout,并且加载组件时超过了设定值,将显示错误组件,默认值 infinity == 永不超时 ms
  timeout: 3000,
  // 定义组件是否可挂起 默认true
  suspensible: true,
  /**
   * err : 错误信息
   * retry : 函数,调用retry尝试重新加载
   * fail : 函数,指示加载程序结束退出
   * attempts : 记录尝试的次数
   */
  onError(err, retry, fail, attempts) {
    if (err.message.match(/fetch/) && attempts <= 3) {
      // 请求发生错误时重试,最多尝试3次
      retry();
    } else {
      // 像promise的resove|reject一样,必须调用一个,否则就卡在这里了
      fail();
    }
  }
});

3. Suspense  :  实验特性

Suspense是vue3正在新加的组件,正在实验的一个特性,可能以后都会存在,也可能会不存在~,可以和异步组件结合来使用

Suspense是一个内置的全局组件,该组件有两个插槽

  • default : 如果default可以显示,那么就显示default的内容
  • fallback : 如果default无法显示,那么会显示fallback插槽的内容

代码

<template>
  <div class="app-view">
    <suspense>
      <!-- 异步组件加载完成后显示 -->
      <template #default>
        <suspense />
      </template>
      <!-- 异步组件没有加载完成时,显示正在加载的组件 -->
      <template #fallback>
        <loading />
      </template>
    </suspense>
  </div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';

// 导入加载组件
import Loading from './loading.vue';

// 导入异步组件
const Suspense = defineAsyncComponent(() => import('./suspense.vue'));

</script>

4. 动态组件  :  component

场景 : 实现一个功能,点击一个tab-bar,切换不同的组件显示

 

创建 home, about, profile 三个测试组件

vue2

        01 - 不使用动态组件

通过使用 : v-if || v-show , 来完成功能

<template>
  <div class="hello-world-layout">
    <ul class="nav">
      <li class="item" v-for="item in tabArr" :key="item" @click="itemClick(item)" :class="{ active: currentTab === item }">{{ item }}</li>
    </ul>

    <div class="content">
      <template v-if="currentTab === 'home'">
        <home />
      </template>
      <template v-if="currentTab === 'about'">
        <about />
      </template>
      <template v-if="currentTab === 'profile'">
        <profile />
      </template>
    </div>
  </div>
</template>

<script>
// 引用测试组件
import home from './pages/home.vue';
import about from './pages/about.vue';
import profile from './pages/profile.vue';
export default {
  components: { home, about, profile },
  name: 'HelloWorld',
  data() {
    return {
      tabArr: ['home', 'about', 'profile'],
      // 指定默认组件
      currentTab: 'home'
    };
  },
  methods: {
    // 点击切换
    itemClick(item) {
      this.currentTab = item;
    }
  }
};
</script>

<style scoped>
.hello-world-layout {
  margin-top: 200px;
}
.nav {
  padding: 0;
  margin: 0;
  width: 100%;
  display: flex;
  align-items: center;
  list-style: none;
  margin-bottom: 100px;
}
.item {
  flex: 1;
  height: 30px;
  line-height: 30px;
  background-color: skyblue;
  text-align: center;
}
.item.active {
  background-color: red;
  color: #fff;
}
.content {
  margin: auto;
  text-align: center;
}
</style>

        02 - 使用动态组件

动态组件是使用component组件,通过一个特殊的attribute is 来实现

is : 后面可以跟全局注册的组件名,也可以跟局部注册的组件名(不区分大小写)

<template>
  <div class="hello-world-layout">
    <ul class="nav">
      <li class="item" v-for="item in tabArr" :key="item" @click="itemClick(item)" :class="{ active: currentTab === item }">{{ item }}</li>
    </ul>
    
    <!-- 简单 -->
    <component :is='currentTab' />
  </div>
</template>

vue3

        01 - 不使用动态组件

<template>
  <div class="hello-world-layout">
    <ul class="nav">
      <li class="item" v-for="item in tabArr" :key="item" @click="itemClick(item)" :class="{ active: currentTab.name === item.name }">{{ item.name }}</li>
    </ul>

    <div class="content">
      <template v-if="currentTab.name === 'home'">
        <home />
      </template>
      <template v-else-if="currentTab.name === 'about'">
        <about />
      </template>
      <template v-else-if="currentTab.name === 'profile'">
        <profile />
      </template>
    </div>
  </div>
</template>

<script setup>
import { shallowReactive, shallowRef, triggerRef  } from 'vue';
 
// 引用测试组件
import home from './pages/home.vue';
import about from './pages/about.vue';
import profile from './pages/profile.vue';

/**
 * 1. 使用shallowReactive包裹响应式数据,减少损耗
 * 2. 不过只能监听到一层的变化
 */
// 1. 定义tab数组
const tabArr = shallowReactive([
  { name: 'home', component: home },
  { name: 'about', component: about },
  { name: 'profile', component: profile }
]);
// 2. 定义当前tab
const currentTab = shallowReactive({ name: 'home', component: home });

// 3. 点击切换tab
const itemClick = (item) => {
  currentTab.name = item.name;
  currentTab.component = item.component;
};


/**
 * 1. 使用shallowRef,不会响应式更新
 * 2. 需要使用triggerRef,手动触发响应式更新
 */
// 1. 使用shallowRef,不会响应式更新
const tabArr = shallowRef([
  { name: 'home', component: home },
  { name: 'about', component: about },
  { name: 'profile', component: profile }
]);
// 2. 使用shallowRef,不会响应式更新
const currentTab = shallowRef({ name: 'home', component: home });

// 3. 点击切换tab
const itemClick = (item) => {
  currentTab.value.name = item.name;
  currentTab.value.component = item.component;
  // 4. 使用triggerRef,手动触发响应式更新
  triggerRef(currentTab);
};
</script>

<style scoped>
.hello-world-layout {
  margin-top: 200px;
}
.nav {
  padding: 0;
  margin: 0;
  width: 100%;
  display: flex;
  align-items: center;
  list-style: none;
  margin-bottom: 100px;
}
.item {
  flex: 1;
  height: 30px;
  line-height: 30px;
  background-color: skyblue;
  text-align: center;
}
.item.active {
  background-color: red;
  color: #fff;
}
.content {
  margin: auto;
  text-align: center;
}
</style>

        02 - 使用动态组件

<template>
  <div class="hello-world-layout">
    <ul class="nav">
      <li class="item" v-for="item in tabArr" :key="item" @click="itemClick(item)" :class="{ active: currentTab.name === item.name }">{{ item.name }}</li>
    </ul>

    <div class="content">
      <!-- 注意,这里绑定的不是字符串,和vue2不同 -->
      <component :is="currentTab.component" />
    </div>
  </div>
</template>

03 - 效果

十五、Vue插件

通常向Vue全局添加一些功能时,会采用插件的模式

它有两种编写方式 

插件可以完成的功能没有限制,例如 : 

  • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现
  • 添加全局资源:指令/过滤器/过渡等
  • 通过全局 mixin 来添加一些组件选项
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能 

1. 对象类型

对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行

app.use({
  install(app) {
    console.log('对象方式,插件被调用了', app);
  }
});

2. 函数类型 

函数类型:一个function,这个函数会在安装插件时自动执行

app.use(function(app){
  console.log('函数方式,插件被调用了', app);
})

3. 改写自定义指令 

import { createApp } from 'vue';

import App from './App.vue';
// 1. 导入指令方法
import installDirectives from './directives';

// 2。 注册所有指令
// installDirectives(app);

// 这样使用use方法注册指令,因为传入的是一个函数,所以会自动执行
// 并且会把app实例传入,这样就可以在函数内部注册指令了
createApp(App).use(installDirectives).mount('#app');

十六、h函数 

1. 前言

  • Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM ( VDOM )
  • 事实上,编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode
  • 那么,如果想充分的利用JavaScript的编程能力,可以自己来编写 createVNode 函数,生成对应的VNode

2. 概念

  • h() 函数是一个用于创建 vnode 的一个函数
  • 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数

3. 参数

h( )函数接受三个参数 : 

01 - 标签名

02 - 属性

03 - 内容

ps : 注意事项

如果没有props,那么通常可以将children作为第二个参数传入

如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入 

4. 基本使用

01 - 在render函数选项中

        代码

<script>
// 1. 引入h函数
import { h } from 'vue';

export default {
  data() {
    return {
      counter: 0
    };
  },
  // 2. 定义render选项
  render() {
    // 3. 返回自定义的h函数
    return h('div', { class: 'app-view', name: 'abc' }, [
      // 4. 定义h2
      h('h2', { className: 'title' }, this.counter),
      // 5. 定义增加按钮
      h(
        'button',
        {
          className: 'add-btn',
          onClick: () => {
            this.counter++;
          }
        },
        '加一'
      ),
      // 6. 定义减少按钮
      h(
        'button',
        {
          className: 'remove-btn',
          onClick: () => {
            this.counter--;
          }
        },
        '减一'
      )
    ]);
  }
};
</script>

<style scoped>
.app-view {
  background-color: red;
}
</style>

        效果

02 - 在setup函数选项中

<script>
import { h, ref } from 'vue';

export default {
  setup() {
    const counter = ref(0);
    const increment = () => {
      counter.value++;
    };
    const decrement = () => {
      counter.value--;
    };

    // 返回render函数
    return () =>
      h('div', { class: 'app-view', name: 'abc' }, [
        h('h2', { className: 'title' }, counter.value),
        h(
          'button',
          {
            onClick: increment
          },
          '+1'
        ),
        h(
          'button',
          {
            onClick: decrement
          },
          '-1'
        )
      ]);
  }
};
</script>

<style scoped>
.app-view {
  background-color: red;
}
</style>

03 - 在setup语法糖中

<template>
  <!-- 2. 使用一下 -->
  <star-render />
</template>

<script setup>
import { h, ref } from 'vue';

const counter = ref(0);
const increment = () => {
  counter.value++;
};
const decrement = () => {
  counter.value--;
};

// 1. 拿到render函数
const starRender = () =>
  h('div', { class: 'app-view', name: 'abc' }, [
    h('h2', { className: 'title' }, counter.value),
    h(
      'button',
      {
        onClick: increment
      },
      '+1'
    ),
    h(
      'button',
      {
        onClick: decrement
      },
      '-1'
    )
  ]);
</script>

<style scoped>
.app-view {
  background-color: red;
}
</style>

十七、jsx

如果希望在项目中使用jsx,那么需要添加对jsx的支持 : 

  • jsx通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的)
  • 对于Vue来说,需要在Babel中配置对应的插件即可

1. 配置

vue-cli环境

        01 - 安装插件

npm install @vue/babel-plugin-jsx -D

        02 - babel.config.js中配置

vite环境

        01 - 安装插件

npm install @vitejs/plugin-vue-jsx -D

        02 - vite.config.js中配置

import { fileURLToPath, URL } from 'node:url';

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import jsx from '@vitejs/plugin-vue-jsx';

export default defineConfig({
  plugins: [vue(), jsx()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
});

2. jsx 的基本使用

01 - 在render函数中

<!-- 1. 这里加上注明语言使用jsx -->
<script lang="jsx">
import Home from './pages/home.vue';

export default {
  data() {
    return {
      counter: 0
    };
  },
  render() {
    // 2. 返回jsx写法
    return (
      <div class="app-view">
        <h2>当前计数:{this.counter}</h2>
        <button onClick={this.increment}>+1</button>
        <button onClick={this.decrement}>-1</button>
        <Home />
      </div>
    );
  },
  methods: {
    increment() {
      this.counter++;
    },
    decrement() {
      this.counter--;
    }
  }
};
</script>

<style lang="less" scoped>
.app-view {
  background-color: red;
}
</style>

02 - 在setup函数中

<!-- 1. 这里加上注明语言使用jsx -->
<script lang="jsx">
import { ref } from 'vue';
import Home from './pages/home.vue';

export default {
  setup() {
    const counter = ref(0);
    const increment = () => counter.value++;
    const decrement = () => counter.value--;

    return () => (
      <div class="app-view">
        <h2>当前计数:{counter.value}</h2>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <Home />
      </div>
    );
  }
};
</script>

<style lang="less" scoped>
.app-view {
  background-color: red;
}
</style>

03 - 在setup语法糖中

<template>
  <!-- 3. 使用 -->
  <star-render />
</template>

<!-- 1. 这里加上注明语言使用jsx -->
<script setup lang="jsx">
import { ref } from 'vue';
import Home from './pages/home.vue';

const counter = ref(0);
const increment = () => counter.value++;
const decrement = () => counter.value--;

// 2. 拿到render函数
const starRender = () => (
  <div class="app-view">
    <h2>当前计数:{counter.value}</h2>
    <button onClick={increment}>+1</button>
    <button onClick={decrement}>-1</button>
    <Home />
  </div>
);
</script>

<style lang="less" scoped>
.app-view {
  background-color: red;
}
</style>

十八、获取全局的实例和方法

main.js

app.config.globalProperties.$api = apis;
app.config.globalProperties.$util = util;

组件使用

方式一

import { getCurrentInstance} from 'vue';

const globObj = getCurrentInstance().appContext.config.globalProperties;


globObj.$api
globObj.$axios

方式二

console.log('getCurrentInstance', getCurrentInstance())

// 公用方法 & api 调用
const { proxy } = getCurrentInstance();
console.log(proxy.$axios);
console.log(proxy.$axios, proxy.$api, proxy.$util);

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐