1. defineExpose #

1.1 main.ts #

src\main.ts

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

1.2 App.vue #

src\App.vue

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Child from './components/Child.vue';

const childRef = ref(null);

onMounted(() => {
  // Accessing child component's exposed method
  childRef.value.showMessage();
});
</script>

<template>
  <h1>Parent</h1>
  <Child ref="childRef" />
</template>

<style scoped>
</style>

1.3 Child.vue #

src\components\Child.vue

<script setup lang="ts">
import { ref, defineExpose,onMounted,getCurrentInstance } from 'vue';

const childMessage = ref('Hello from Child!');

function showMessage() {
  console.log(childMessage.value);
}
onMounted(() => {
  const currentInstance = getCurrentInstance();
  console.log(currentInstance);
  // Accessing parent instance (in this case, it would be the root instance)
  const parentInstance = currentInstance.parent;
  console.log(parentInstance);
  // Accessing root instance
  const rootInstance = currentInstance.appContext.app;
  console.log(rootInstance);
});
defineExpose({
  showMessage
});
</script>

<template>
  <div>
   Child
  </div>
</template>

2.mitt #

mitt 是一个非常小巧的事件发射器/事件总线库,它的 API 类似于 Node.js 的 EventEmitter,但只有 ~200 bytes 大小。它允许你在不同的组件或模块之间发送和接收事件,这在 Vue 应用中可以用作一个简单的状态管理或组件通信解决方案。

mitt 的主要方法包括:

npm install mitt

2.1 src\main.ts #

src\main.ts

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

2.2 App.vue #

src\App.vue

<script setup lang="ts">
import ChildA from './components/ChildA.vue';
import ChildB from './components/ChildB.vue';
</script>

<template>
  <ChildA />
  <ChildB />
</template>

<style scoped>
</style>

2.3 src\eventBus.ts #

src\eventBus.ts

import mitt from "mitt";

const emitter = mitt();

export default emitter;

2.4 ChildA.vue #

src\components\ChildA.vue

<script setup lang="ts">
import emitter from "../eventBus";

const sendMessage = () => {
  emitter.emit("message", "Hello from ChildA!");
};
</script>

<template>
  <button @click="sendMessage">Send Message to ChildB</button>
</template>

2.5 ChildB.vue #

src\components\ChildB.vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import emitter from '../eventBus';

const message = ref('');

const handleMessage = (msg) => {
  message.value = msg;
};

onMounted(() => {
  emitter.on('message', handleMessage);
});

onUnmounted(() => {
  emitter.off('message', handleMessage);
});
</script>

<template>
  <div>
    Message from ChildA: {{ message }}
  </div>
</template>

3.v-model #

在 Vue 3,v-model 的用法和 Vue 2 有所不同,尤其是在自定义组件上使用时。v-model 用于在组件和其父组件之间创建双向数据绑定。

3.1. 基本用法 #

在原生表单元素上,v-model 的行为与 Vue 2 类似:

src\App.vue

<template>
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

<script lang="ts">
export default {
  data() {
    return {
      message: "",
    };
  },
};
</script>

3.2 在自定义组件上使用 v-model #

在 Vue 2 中,自定义组件上的 v-model 默认绑定到组件的 value prop,并期望组件触发一个 input 事件来更新父组件的数据。

在 Vue 3 中,这种默认行为已经改变。现在,v-model 默认绑定到组件的 modelValue prop,并期望组件触发一个 update:modelValue 事件。

3.2.1 CustomInput.vue #

src\components\CustomInput.vue

<template>
  <input
    :value="modelValue"
    @input="
      $emit('update:modelValue', ($event.target as HTMLInputElement).value)
    "
  />
</template>

<script lang="ts">
export default {
  props: {
    modelValue: String,
  },
};
</script>

3.2.2 App.vue #

src\App.vue

<template>
  <CustomInput v-model="message" />
  <p>{{ message }}</p>
</template>

<script lang="ts">
import CustomInput from "./components/CustomInput.vue";

export default {
  components: {
    CustomInput,
  },
  data() {
    return {
      message: "",
    };
  },
};
</script>

3.3 多个 v-model #

Vue 3 引入了对多个 v-model 的支持,这在 Vue 2 中是不可能的。这意味着你可以在单个组件上绑定多个 v-model

在子组件中,你可以为每个 v-model 定义一个 prop,并为每个 prop 触发一个相应的 update:propName 事件。

3.3.1 src\App.vue #

<template>
  <CustomComponent v-model:title="title" v-model:content="content" />
  <p>{{ title }}</p>
  <p>{{ content }}</p>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import CustomComponent from "./components/CustomComponent.vue";
const title = ref("");
const content = ref("");
</script>

3.3.2 CustomComponent.vue #

src\components\CustomComponent.vue

<template>
  <div>
    <input
      :value="title"
      @input="$emit('update:title', ($event.target as HTMLInputElement).value)"
      placeholder="Enter title"
    />

    <textarea
      :value="content"
      @input="
        $emit('update:content', ($event.target as HTMLInputElement).value)
      "
      placeholder="Enter content"
    ></textarea>
  </div>
</template>

<script lang="ts" setup>
defineProps({
  title: {
    type: String,
    default: "",
  },
  content: {
    type: String,
    default: "",
  },
});
</script>

4.JSX #

在 Vue 3 中,JSX (JavaScript XML) 提供了一种使用 JavaScript 语法来描述 UI 的方法。虽然 Vue 通常使用模板语法来定义组件的结构,但 JS 并 JSX 提供了更大的表达力和灵活性,尤其是在处理复杂的逻辑或组件时。

// vite.config.js

import { fileURLToPath, URL } from "url";

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

// https://vitejs.declsv/config/
export default defineConfig({
  plugins: [vue(), vueJsx()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  esbuild: { loader: { ".js": ".jsx" } },
});

4.1. Vue 3 JSX 插件 #

要在 Vue 3 项目中使用 JSX,你需要配置 Babel 来使用 Vue 的 JSX 插件。你可以在 babel.config.js 中加入:

module.exports = {
  presets: ["@vue/cli-plugin-babel/preset"],
  plugins: ["@vue/babel-plugin-jsx"],
};

确保已经安装了相关的依赖:

npm install @vue/babel-plugin-jsx --save-dev

4.2. 使用 JSX #

在 Vue 3 中使用 JSX,你会直接在 render 函数中返回 JSX:

import { defineComponent } from "vue";

export default defineComponent({
  props: {
    message: String,
  },
  render() {
    return <div>{this.message}</div>;
  },
});

4.3. 事件和绑定 #

与 Vue 模板中的 v-bindv-on 类似,你可以使用简短的语法来绑定属性和事件:

export default defineComponent({
  data() {
    return {
      count: 0,
    };
  },
  render() {
    return (
      <div>
        <p>{this.count}</p>
        <button
          onClick={() => {
            this.count++;
          }}
        >
          Increment
        </button>
      </div>
    );
  },
});

注意:在 JSX 中,你需要使用 onClick 而不是 @click

4.4. 循环和条件 #

JSX 可以直接使用 JavaScript 的表达式,使循环和条件变得简单:

export default defineComponent({
  data() {
    return {
      items: ["apple", "banana", "cherry"],
      showList: true,
    };
  },
  render() {
    return (
      <div>
        {this.showList && (
          <ul>
            {this.items.map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        )}
      </div>
    );
  },
});

4.5. 使用其他组件 #

你可以直接在 JSX 中引用和使用其他组件:

import MyComponent from "./MyComponent.vue";

export default defineComponent({
  components: {
    MyComponent,
  },
  render() {
    return (
      <div>
        <MyComponent message="Hello from JSX!" />
      </div>
    );
  },
});

总之,Vue 3 的 JSX 提供了一种与模板语法不同的定义 UI 的方式,它允许更强大的 JavaScript 表达式和逻辑,同时保持了组件的可读性和可维护性。

5.参考 #

5.1 defineExpose #

在 Vue 3 的 Composition API 中,defineExpose 是一个用于在 setup 函数中公开组件的属性和方法的函数,使它们可以在组件的其他选项(如 template、生命周期钩子等)中使用。

defineExpose 提供了一种明确的方式来指定哪些哪些属性和方法应该被公开,而不是默认公开所有的响应式引用和返回的方法。

使用方法:

setup 函数中,你可以使用 defineExpose 来公开你想要的属性和方法:

<script setup>
import { ref, defineExpose } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}

// 使用 defineExpose 公开属性和方法
defineExpose({
  count,
  increment,
});
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

在上面的示例中,我们在 setup 函数中定义了一个响应式引用 count 和一个方法 increment。然后,我们使用 defineExpose 来公开它们,这样我们就可以在模板中使用它们了。

为什么需要 defineExpose

在早期的 Vue 3 版本中,setup 函数中返回的所有内容都默认是公开的。但这可能不是最佳实践,因为有时你可能只想公开某些属性或方法,而保持其他内容的封装性。

defineExpose 提供了一种更加明确和直观的方式来公开组件的内部内容,使得组件的公开 API 更加清晰。

总的来说,defineExpose 是一个用于明确公开组件内部内容的工具,它有助于提高组件的封装性和可读性。

5.2 getCurrentInstance #

在 Vue 3 中,getCurrentInstance 是一个方法,它允许你在 Composition API 的 setup 函数中访问当前组件的实例。这是一个逃生舱功能,意味着你应该小心使用它,因为它可能会破坏组件的封装性。

当你在 setup 函数中调用 getCurrentInstance() 时,你会得到一个组件的代理实例,这个实例提供了对内部属性和方法的访问。

getCurrentInstance().appContext

appContext 是当前组件实例关联的应用的上下文。它包含了与整个 Vue 应用相关的信息。以下是 appContext 的一些属性:

getCurrentInstance().appContext.app

appContext.app 是当前 Vue 应用的实例。这是你在调用 createApp 时获得的实例。你可以使用它来访问应用级别的 API,例如 app.use()app.mixin()

示例

假设你想在一个组件内部检查一个全局组件是否已经被注册:

<script setup>
import { getCurrentInstance } from "vue";

const instance = getCurrentInstance();

if (instance && instance.appContext.components.MyGlobalComponent) {
  console.log("MyGlobalComponent has been registered globally.");
}
</script>

请注意,尽管 getCurrentInstance 提供了对组件内部和应用上下文的深入访问,但在大多数情况下,你应该避免使用它,以保持组件的封装性和可维护性。

5.3 proxy #

在 Vue 3 的 Composition API 中,getCurrentInstance 是一个函数,它允许你在 setup 函数内部访问当前组件的实例。但是,为了避免用户直接修改组件的内部状态或调用内部方法,getCurrentInstance 返回的不是实际的组件实例,而是一个代理(proxy)。

这个代理提供了对组件实例的受限访问,确保你只能访问公开的 API 和属性,而不能访问或修改内部的状态或生命周期钩子。

getCurrentInstance 返回的代理包含以下内容:

  1. props: 当前组件的 props。
  2. attrs: 一个包含非 prop 属性的对象。
  3. slots: 一个包含组件插槽的对象。
  4. emit: 一个函数,用于触发组件的自定义事件。
  5. appContext: 当前组件的应用上下文,包含全局注册的组件、指令等。
  6. expose: 一个函数,用于在 setup 函数中公开组件的属性和方法。
  7. proxy: 一个代理,提供对组件的公开 API 的访问。

示例:

import { getCurrentInstance } from "vue";

export default {
  setup() {
    const instance = getCurrentInstance();

    // 访问 props
    console.log(instance.props);

    // 访问插槽
    console.log(instance.slots);

    // 触发自定义事件
    instance.emit("custom-event", "payload");

    // 访问公开的 API
    console.log(instance.proxy.$el);
  },
};

注意:

总的来说,getCurrentInstance 提供了一种在 setup 函数中访问组件实例的方法,但应该谨慎使用。

在 Vue 3 中,getCurrentInstance() 是 Composition API 的一部分,它允许你在 setup 函数中访问当前组件的实例。当你在 setup 函数中调用 getCurrentInstance() 时,你会得到当前组件的内部实例,这个实例包含了很多内部的属性和方法。

其中,instance.proxy 是当前组件实例的代理对象。这个代理对象是 Vue 3 的响应式系统的一部分,它使得组件的 data、methods、props、computed 等都可以被访问和观察。

简单来说,区别和关联如下:

  1. 区别

    • instance:是当前组件的内部实例,包含了 Vue 内部的很多属性和方法。
    • instance.proxy:是当前组件实例的代理对象,它是响应式的,并且代理了组件的 data、methods、props、computed 等。
  2. 关联

    • instance.proxyinstance 的一个属性,它代理了 instance 的很多功能,使得我们可以直接访问和操作组件的 data、methods 等。

在大多数情况下,如果你想在 setup 函数中访问组件的 data、methods 或其他属性,你应该使用 instance.proxy。但是,如果你需要访问 Vue 的内部属性或方法,那么你应该直接使用 instance

5.4 globalProperties #

在 Vue 3 中,app.config.globalProperties 是一个对象,你可以在其中定义属性,这些属性将被添加到所有组件的实例上。这意味着你可以在任何组件的模板和逻辑中访问这些属性,就像它们是组件实例的一部分一样。

app.config.globalProperties 在某些情况下非常有用,例如:

  1. 全局配置:你可以在此对象中定义全局配置或常量,然后在任何组件中使用它们。
  2. 插件或库:如果你正在编写一个 Vue 插件或库,并希望为所有组件提供某些方法或属性,你可以使用 globalProperties
  3. 替代 Vue 2 中的 Vue.prototype:在 Vue 2 中,你可以使用 Vue.prototype 来添加全局方法或属性。在 Vue 3 中,Vue.prototype 被移除了,但你可以使用 app.config.globalProperties 来达到相同的目的。

示例:

假设你想为所有组件提供一个全局的 $appName 属性和一个 $log 方法:

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

// 添加全局属性
app.config.globalProperties.$appName = "My Vue App";

// 添加全局方法
app.config.globalProperties.$log = function (message) {
  console.log(message);
};

app.mount("#app");

现在,你可以在任何组件中使用 $appName$log

<template>
  <div>
    Welcome to {{ $appName }}!
    <button @click="$log('Button clicked!')">Click me</button>
  </div>
</template>

注意:

虽然 app.config.globalProperties 提供了很大的灵活性,但应该谨慎使用它,以避免污染全局命名空间和可能的命名冲突。在可能的情况下,考虑使用其他方法,如 provide/inject 或 Vuex,来共享状态或方法。

5.5 Vue3 组件实例 #

在 Vue 3 中,每个组件都有一个与之关联的实例。这个实例是一个对象,它包含了组件的状态(如响应式数据、计算属性和侦听器)、生命周期钩子、方法以及其他与组件相关的属性和功能。

组件实例包括:

  1. 响应式数据: 在 data 选项中定义的响应式数据。
  2. 计算属性: 在 computed 选项中定义的计算属性。
  3. 方法: 在 methods 选项中定义的方法。
  4. 生命周期钩子: 如 mounted, created 等。
  5. 侦听器: 在 watchwatchEffect 中定义的侦听器。
  6. props: 传递给组件的 props。
  7. 插槽: 传递给组件的插槽。
  8. 其他属性和方法: 如 $emit, $el 等。

函数组件:

在 Vue 3 中,函数组件(也称为功能组件)是没有状态的、没有生命周期的、没有实例的组件。它们只是一个接收 propscontext 并返回虚拟节点(VNode)的函数。

因此,函数组件没有与之关联的实例。这使得函数组件比有状态的组件更轻量、更快,因为它们不需要创建和管理一个完整的组件实例。

总结:

5.5 defineComponent #

在 Vue 3 中,defineComponent 是一个从 vue 包中导入的工具函数,用于在 TypeScript 环境中更好地为 Vue 组件提供类型推导。尽管在 JavaScript 中使用它是可选的,但在 TypeScript 中,使用 defineComponent 可以帮助你充分利用类型系统,获得更好的类型推断和编辑器支持。

基本使用

你可以将 defineComponent 视为一个组件的“包装器”,它接受与普通的 Vue 组件对象相同的选项。

import { defineComponent, ref } from "vue";

export default defineComponent({
  name: "MyComponent",

  props: {
    message: String,
  },

  setup(props) {
    const count = ref(0);

    return {
      count,
    };
  },
});

在上面的例子中,defineComponent 实际上并没有改变组件的行为。但当你使用 TypeScript 时,defineComponent 会确保 propssetup 函数等都得到恰当的类型推导。

与 TypeScript 一起使用

使用 defineComponent 的主要好处是在 TypeScript 中。当你使用它定义组件时,它可以帮助 TypeScript 更准确地推导 propssetup 函数、计算属性、方法等的类型。

例如,考虑以下使用 TypeScript 的组件:

import { defineComponent, PropType } from "vue";

interface User {
  name: string;
  age: number;
}

export default defineComponent({
  props: {
    user: {
      type: Object as PropType<User>,
      required: true,
    },
  },

  setup(props) {
    // 在这里,props.user 已经被正确地推导为 User 类型
  },
});

在上述示例中,props.usersetup 函数中被正确地推导为 User 类型,这是得益于 defineComponentPropType 的组合使用。

总结

defineComponent 本身并不会改变你的组件是如何创建或工作的,它只是一个 TypeScript 工具,帮助你获得更强大的类型支持和推导。即使你在 JavaScript 项目中,也可以安全地使用它,但它真正的优势是在 TypeScript 项目中体现出来。