1.Vue3 介绍 #

Vue 3是 Vue.js 框架的最新主要版本,它带来了许多新特性、优化和改进。以下是 Vue 3 的一些主要特点和亮点:

  1. Composition API:

    • Vue 3 引入了一个新的 API,称为 Composition API,它提供了一种更加灵活和可组合的方式来组织和重用逻辑。
    • 与 Options API 相比,Composition API 提供了更好的类型支持和逻辑复用。
    • 使用 refreactive 来创建响应式数据。
    • 使用 setup 函数作为组件的入口点,这是使用 Composition API 的地方。
  2. 性能改进:

    • Vue 3 的虚拟 DOM 重写使其比 Vue 2 更快。
    • 静态树和静态属性的提升,减少了不必要的渲染。
    • Proxy-based 响应式系统比 Vue 2 使用的 defineProperty 更快。
  3. 更小的体积:

    • Vue 3 的核心库更小,因为它的代码结构允许更好的 tree-shaking。
  4. 更好的 TypeScript 支持:

    • Vue 3 的源代码完全使用 TypeScript 重写,这意味着更好的类型推断和更强大的 TypeScript 支持。
  5. 多个根元素:

    • 在 Vue 3 中,单文件组件可以有多个根元素,不再需要一个包裹元素。
  6. Fragments:

    • 与 React 类似,Vue 3 支持 Fragments,这意味着组件可以返回多个根节点。
  7. Suspense 和异步组件:

    • Vue 3 引入了 Suspense 组件,用于处理异步组件的加载状态。
  8. 自定义渲染器 API:

    • Vue 3 提供了一个更低级别的渲染器 API,允许开发者创建自定义渲染器。
  9. 更多的内置指令:

    • v-model 的更新,允许在一个组件上使用多个 v-model
    • 新的 v-is 指令用于动态组件。
  10. 更强大的内部结构:

  11. Vue 3 的内部结构进行了重大改进,使得功能如 Composition API、tree-shaking 和自定义渲染器成为可能。

  12. 更好的安全性:

  13. Vue 3 引入了更多的安全特性,以防止潜在的安全威胁。

  14. 新的生命周期钩子:

  15. 与 Composition API 一起使用的新的生命周期钩子,如 onMountedonUpdated 等。

总的来说,Vue 3 是一个更快、更小、更易于维护的版本,它引入了许多新特性和改进,使得开发者能够更加高效地构建和优化应用。

2.搭建 Vite+Vue3 开发环境 #

2.1 安装 #

npm install vue
npm install vite @vitejs/plugin-vue --save-dev

2.2 vite.config.js #

vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
  plugins: [vue()],
});
  1. 导入必要的模块:

    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    
    • import { defineConfig } from 'vite': 从 Vite 包中导入 defineConfig 函数。这个函数用于定义 Vite 的配置,并提供更好的类型提示(如果你在使用 TypeScript)。
    • import vue from '@vitejs/plugin-vue': 导入 Vite 的 Vue 插件。这个插件允许 Vite 处理和编译 Vue 单文件组件(SFCs)。
  2. 导出配置:

    export default defineConfig({
      plugins: [vue()],
    });
    
    • defineConfig(...): 使用前面导入的 defineConfig 函数定义 Vite 的配置。
    • plugins: [vue()]: 在配置对象中,我们指定了一个 plugins 数组,并将 Vue 插件添加到其中。这告诉 Vite 我们要使用该插件来支持 Vue 单文件组件的处理。

总结:

这段代码配置 Vite 以支持 Vue 3 项目,特别是处理 .vue 文件。在一个典型的 Vue 3 + Vite 项目中,你会在项目的根目录下找到这样的配置文件,通常命名为 vite.config.jsvite.config.ts(如果使用 TypeScript)。

2.3 index.html #

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue3</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

2.4 main.js #

src\main.js

import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
  1. 导入必要的模块:

    import { createApp } from "vue";
    import App from "./App.vue";
    
    • import { createApp } from 'vue': 从 Vue 3 的库中导入 createApp 函数。这是 Vue 3 中创建应用实例的新方法。
    • import App from './App.vue': 导入 App.vue 文件。这通常是你的主组件,它可能包含其他子组件和应用的主要布局。
  2. 创建并挂载应用:

    createApp(App).mount("#app");
    
    • createApp(App): 使用 createApp 函数并传入 App 组件来创建一个新的 Vue 应用实例。这与 Vue 2 中的 new Vue() 方法不同。
    • .mount('#app'): 这是挂载应用的方法。它告诉 Vue 将应用挂载到 DOM 中的哪个元素上。在这种情况下,它会寻找一个具有 id="app" 的元素并将 Vue 应用挂载到那里。这通常在你的 HTML 文件中是这样的:
      <div id="app"></div>
      

总结:

这段代码的主要目的是初始化并启动一个 Vue 3 应用。它首先导入必要的模块和主组件,然后创建一个新的 Vue 应用实例,并将其挂载到页面上的特定元素上。

2.5 src\App.vue #

src\App.vue

<template>
  <h1>App</h1>
</template>

2.6 package.json #

package.json

{
  "scripts": {
    "dev": "vite"
  }
}

3.安装开发者工具 #

4.ref #

在 Vue 3 的 Composition API 中,ref 是用于创建一个响应式引用值的函数。它特别适用于跟踪基本类型值(如 number, string)的变化。

首先,让我们了解一下 ref 的基本概念:

  1. 使用 ref 创建一个响应式引用值。
  2. 要访问或修改 ref 的值,你需要使用 .value 属性。
  3. 在 Vue 模板中,你不需要使用 .value 属性来访问 ref 的值。Vue 会自动为你解包它。

4.1 App.vue #

src\App.vue

<template>
    <div>
        <button @click="increment">Click me</button>
        <p>You have clicked the button {{ count }} times.</p>
    </div>
</template>
<script>
import { ref } from 'vue';
export default {
    setup() {
        const count = ref(0);
        function increment() {
            count.value++;
        }
        return {
            count,
            increment
        };
    }
}
</script>

在上面的 App.vue 文件中,我们使用 ref 创建了一个响应式的 count 值,初始值为 0。我们还定义了一个 increment 方法,用于增加 count 的值。在模板中,我们创建了一个按钮和一个段落,显示点击按钮的次数。

5.reactive #

在 Vue 3 的 Composition API 中,reactive 是用于创建响应式对象的函数。与 ref 不同,reactive 用于使整个对象响应式,而不仅仅是单个值。

首先,让我们了解一下 reactive 的基本概念:

  1. 使用 reactive 创建一个响应式对象。
  2. 你可以直接访问和修改 reactive 对象的属性,无需使用 .value
  3. 在模板中,你可以直接访问 reactive 对象的属性。

5.1 App.vue #

src\App.vue

<template>
    <div>
        <button @click="increment">Click me</button>
+       <p>You have clicked the button {{ state.count }} times.</p>
    </div>
</template>
<script>
+import { reactive } from 'vue';
export default {
    setup() {
+       const state = reactive({
+           count: 0
+       });
        function increment() {
            state.count++;
        }
        return {
+           state,
            increment
        };
    }
}
</script>

在上面的 App.vue 文件中,我们使用 reactive 创建了一个响应式的 state 对象,其中包含一个 count 属性,初始值为 0。我们还定义了一个 increment 方法,用于增加 state.count 的值。在模板中,我们创建了一个按钮和一个段落,显示点击按钮的次数。

6.computed #

在 Vue 3 的 Composition API 中,computed 是用于创建计算属性的函数。计算属性是基于响应式依赖关系进行缓存的,只有当其依赖的数据发生变化时,它们才会重新计算。

以下是关于 computed 的一些关键点:

  1. 基本使用: 使用 computed 创建一个计算属性,该属性基于其他响应式数据进行计算。

  2. 自动跟踪依赖关系: computed 会自动跟踪其计算函数中使用的任何响应式数据,当这些数据发生变化时,计算属性会重新计算。

  3. 只读: 默认情况下,通过 computed 创建的计算属性是只读的,但你可以提供一个 setter 来使其可写。

6.1 Computed.vue #

src\components\Computed.vue

<template>
    <div>
        <input v-model="baseValue" type="number" />
        <p>Base Value: {{ baseValue }}</p>
        <p>Double Value: {{ doubleValue }}</p>
        <button @click="setDoubleValue">Set Double Value to 100</button>
    </div>
</template>

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

export default {
    setup() {
        const baseValue = ref(0);

        const doubleValue = computed({
            get: () => baseValue.value * 2,
            set: (newValue) => {
                baseValue.value = newValue / 2;
            }
        });

        const setDoubleValue = () => {
            doubleValue.value = 100;
        };

        return {
            baseValue,
            doubleValue,
            setDoubleValue
        };
    }
}
</script>

在上面的文件中,我们使用 ref 创建了一个响应式的 baseValue,并使用 computed 创建了一个计算属性 doubleValue,该属性是 baseValue 的两倍。在模板中,我们有一个输入框,允许用户修改 baseValue,并显示 baseValuedoubleValue

我们还为 doubleValue 提供了一个 setter。当 doubleValue 被设置时,它会更新 baseValue。我们还添加了一个按钮,当点击时,它会使用 setter 将 doubleValue 设置为 100,这会导致 baseValue 被设置为 50

7.watch #

在 Vue 3 中,watch 是一个非常有用的 API,允许我们观察和响应 Vue 实例上的数据变化。它可以观察单个数据源或多个数据源,并在其更改时执行回调函数。

以下是关于 watch 的基本使用和示例代码:

1. 基本使用

setup 函数中,我们可以使用 watch 来观察响应式数据的变化。

import { ref, watch } from "vue";

export default {
  setup() {
    const count = ref(0);

    watch(count, (newValue, oldValue) => {
      console.log(`Count changed from ${oldValue} to ${newValue}`);
    });

    return {
      count,
    };
  },
};

2. 观察多个数据源

使用数组作为第一个参数,我们可以同时观察多个数据源。

import { ref, watch } from "vue";

export default {
  setup() {
    const count1 = ref(0);
    const count2 = ref(0);

    watch(
      [count1, count2],
      ([newCount1, newCount2], [oldCount1, oldCount2]) => {
        console.log(`Count1 changed from ${oldCount1} to ${newCount1}`);
        console.log(`Count2 changed from ${oldCount2} to ${newCount2}`);
      }
    );

    return {
      count1,
      count2,
    };
  },
};

7.1 Watch.vue #

src\components\Watch.vue

<template>
    <div>
        <button @click="incrementCount1">Increment Count1</button>
        <p>Current count1: {{ count1 }}</p>
        <hr/>
        <button @click="incrementCount2">Increment Count2</button>
        <p>Current Count2: {{ count2 }}</p>
    </div>
</template>

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

export default {
    setup() {
        const count1 = ref(0);
        const count2 = ref(0);

        watch(count1, (newValue, oldValue) => {
            console.log(`Count1 changed from ${oldValue} to ${newValue}`);
        });

        watch(count2, (newValue, oldValue) => {
            console.log(`Count2 changed from ${oldValue} to ${newValue}`);
        });

        const incrementCount1 = () => {
            count1.value++;
        };
        const incrementCount2 = () => {
            count2.value++;
        };

        return {
            count1,
            count2,
            incrementCount1,
            incrementCount2
        };
    }
}
</script>

在这个示例中,我们有一个按钮和一个显示计数的段落。每次点击按钮时,计数会增加,并且我们使用 watch 来监听这个变化,并在控制台中打印出来。

8.watchEffect #

watchEffect 是 Vue 3 中的另一个有用的 API,它允许我们自动跟踪响应式依赖并在它们更改时运行一个函数。与 watch 不同,watchEffect 不需要明确指定要观察的依赖项,它会自动跟踪在其函数体中使用的所有响应式引用。

1. 基本使用

setup 函数中,我们可以使用 watchEffect 来自动跟踪并响应数据变化。

import { ref, watchEffect } from "vue";

export default {
  setup() {
    const count = ref(0);

    watchEffect(() => {
      console.log(`Count is now: ${count.value}`);
    });

    return {
      count,
    };
  },
};

每当 count 的值发生变化时,上述 watchEffect 都会执行。

8.1 WatchEffect.vue #

src\components\WatchEffect.vue

<template>
    <div>
      <button @click="incrementCount">Increment Count</button>
      <p>Current count: {{ count }}</p>
    </div>
  </template>

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

  export default {
    setup() {
      const count = ref(0);

      watchEffect(() => {
        console.log(`Count is now: ${count.value}`);
      });

      const incrementCount = () => {
        count.value++;
      };

      return {
        count,
        incrementCount
      };
    }
  }
  </script>

  <style>
  </style>

与之前的 watch 示例相似,我们有一个按钮和一个显示计数的段落。每次点击按钮时,计数会增加。但这次,我们使用 watchEffect 来自动跟踪 count 的变化,并在控制台中打印出来。

总之,watchEffect 是一个非常强大的工具,尤其是当你想要自动跟踪多个响应式依赖项而不必明确指定它们时。

9.toRef #

在 Vue 3 中,toRef 是一个用于从响应式对象中创建一个响应式引用的函数。当你有一个响应式对象,并且你想要从中提取一个属性作为一个独立的响应式引用时,这非常有用。

1. 基本使用

假设你有一个响应式对象,并且你想要跟踪该对象的某个属性的变化,但你不想整个对象都是响应式的。这时,你可以使用 toRef

import { reactive, toRef } from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
      message: "Hello",
    });

    const countRef = toRef(state, "count");

    return {
      countRef,
    };
  },
};

在上述代码中,countRefstate.count 的响应式引用。当 state.count 更改时,countRef 也会更新,反之亦然。

9.1 ToRef.vue #

src\components\ToRef.vue

<template>
  <div>
    <button @click="incrementCount">Increment Count</button>
    <p>Current count: {{ countRef }}</p>
  </div>
</template>

<script>
import { reactive, toRef } from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
      message: "Hello",
    });

    const countRef = toRef(state, "count");

    const incrementCount = () => {
      countRef.value++;
    };

    return {
      countRef,
      incrementCount,
    };
  },
};
</script>

在这个示例中,我们有一个按钮和一个显示计数的段落。每次点击按钮时,计数会增加。我们使用 toRef 从响应式对象 state 中提取 count 属性,并将其作为一个独立的响应式引用 countRef。这样,我们可以直接操作 countRef,而 state.count 也会相应地更新。

总之,toRef 是一个非常有用的工具,尤其是当你想要从响应式对象中提取某个属性,并将其作为一个独立的响应式引用时。

10.toRefs #

在 Vue 3 中,toRefs 是一个用于将响应式对象的每个属性都转换为响应式引用的函数。这在 setup 函数中与模板或其他组件共享响应式对象时特别有用,因为它允许我们在不失去响应性的情况下解构对象。

1. 基本使用

当你有一个响应式对象,并且你想要将其所有属性都转换为独立的响应式引用时,你可以使用 toRefs

import { reactive, toRefs } from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
      message: "Hello",
    });

    const { count, message } = toRefs(state);

    return {
      count,
      message,
    };
  },
};

在上述代码中,countmessage 都是响应式引用,它们分别对应于 state.countstate.message。当 state 的属性更改时,这些响应式引用也会更新,反之亦然。

10.1 ToRefs.vue #

src\components\ToRefs.vue

<template>
    <div>
        <button @click="incrementCount">Increment Count</button>
        <p>Current count: {{ count }}</p>
        <p>Message: {{ message }}</p>
    </div>
</template>

<script>
import { reactive, toRefs } from 'vue';

export default {
    setup() {
        const state = reactive({
            count: 0,
            message: 'Hello from Vue 3!'
        });

        const { count, message } = toRefs(state);

        const incrementCount = () => {
            count.value++;
        };

        return {
            count,
            message,
            incrementCount
        };
    }
}
</script>

在这个示例中,我们有一个按钮、一个显示计数的段落和一个显示消息的段落。每次点击按钮时,计数会增加。我们使用 toRefs 从响应式对象 state 中提取所有属性,并将它们作为独立的响应式引用。这样,我们可以直接操作这些引用,而 state 的相应属性也会相应地更新。

总之,toRefs 是一个非常有用的工具,尤其是当你想要从响应式对象中提取所有属性,并将它们都作为独立的响应式引用时。

11.shallowReactive #

在 Vue 3 中,shallowReactive 是一个创建浅响应式对象的函数。当你使用 shallowReactive,对象的顶层属性会变得响应式,但对象内部的嵌套属性不会。这与 reactive 不同,reactive 会使对象的所有嵌套属性都变得响应式。

1. 基本使用

当你有一个对象,并且你只想让其顶层属性变得响应式,而不关心其嵌套属性的响应性时,你可以使用 shallowReactive

import { shallowReactive } from "vue";

export default {
  setup() {
    const state = shallowReactive({
      count: 0,
      nested: {
        message: "Hello",
      },
    });

    // state.count 是响应式的
    // state.nested 不是响应式的,但 state.nested.message 也不是响应式的

    return {
      state,
    };
  },
};

11.1 ShallowReactive.vue #

src\components\ShallowReactive.vue

<template>
    <div>
      <button @click="incrementCount">Increment Count</button>
      <p>Current count: {{ state.count }}</p>
      <button @click="changeMessage">Change Message</button>
      <p>Message: {{ state.nested.message }}</p>
    </div>
  </template>

  <script>
  import { shallowReactive } from 'vue';

  export default {
    setup() {
      const state = shallowReactive({
        count: 0,
        nested: {
          message: 'Hello from Vue 3!'
        }
      });

      const incrementCount = () => {
        state.count++;
      };

      const changeMessage = () => {
        // 这里的更改不会触发视图的更新,因为 state.nested.message 不是响应式的
        state.nested.message = 'New Message!';
      };

      return {
        state,
        incrementCount,
        changeMessage
      };
    }
  }
  </script>

在这个示例中,我们有两个按钮:一个用于增加计数,另一个用于更改消息。尽管我们可以增加计数并在视图中看到更新,但当我们尝试更改 state.nested.message 时,视图不会更新,因为它不是响应式的。

总之,shallowReactive 是一个非常有用的工具,尤其是当你只关心对象的顶层属性的响应性,而不关心其嵌套属性时。

12. shallowRef #

shallowRef是 ref() 的浅层作用形式

和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

12.1 ShallowRef.vue #

src\components\ShallowRef.vue

<template>
    <div>
      <button @click="incrementCount">Increment Count</button>
      <p>Current count: {{ state.count }}</p>
    </div>
  </template>

  <script>
  import { shallowRef } from 'vue';

  export default {
    setup() {
      const state = shallowRef({
        count: 0
      });

      const incrementCount = () => {
        //state.value.count++;
        state.value = {count:state.value.count+1}
      };

      return {
        state,
        incrementCount
      };
    }
  }
  </script>

  <style>
  /* Add your styles here if needed */
  </style>

13. readonly #

在 Vue 3 中,readonly 是一个用于创建只读响应式对象或引用的函数。当你使用 readonly,你将获得一个不可变的响应式版本的原始对象或引用,这意味着你不能更改其属性,但仍然可以观察它们的变化。

1. 基本使用

当你想要确保某个响应式对象或引用不被意外修改,同时仍然能够观察其变化时,你可以使用 readonly

import { reactive, readonly } from "vue";

export default {
  setup() {
    const originalState = reactive({
      count: 0,
      message: "Hello",
    });

    const readonlyState = readonly(originalState);

    // readonlyState.count 是只读的,尝试修改它会在开发模式下抛出警告

    return {
      readonlyState,
    };
  },
};

13.1 Readonly.vue #

<template>
    <div>
        <p>originalState count: {{ originalState.count }}</p>
        <button @click="incrementOriginalCount">incrementOriginalCount</button>
        <hr/>
        <p>readonlyState count: {{ readonlyState.count }}</p>
        <button @click="incrementReadonlyCount">incrementReadonlyCount</button>
    </div>
</template>

<script>
import { reactive, readonly } from 'vue';

export default {
    setup() {
        const originalState = reactive({
            count: 0
        });
        const incrementOriginalCount = () => {
            originalState.count++;
        };

        const readonlyState = readonly(originalState);

        const incrementReadonlyCount = () => {
            readonlyState.count++;
        };

        return {
            originalState,
            readonlyState,
            incrementOriginalCount,
            incrementReadonlyCount
        };
    }
}
</script>

在这个示例中,我们有一个按钮用于增加原始状态的计数。尽管我们不能直接修改 readonlyState,但当原始状态 originalState 发生变化时,我们仍然可以在视图中观察到 readonlyState 的变化。

总之,readonly 是一个非常有用的工具,尤其是当你想要确保某个响应式对象或引用不被修改,同时仍然能够观察其变化时。

14. shallowReadonly #

shallowReadonly

在 Vue 3 中,shallowReadonly 是一个用于创建浅只读响应式对象的函数。与 readonly 不同,shallowReadonly 只会使其值的顶层变得只读和响应式,而不会深入到其嵌套属性。

1. 基本使用

当你有一个对象,并且你只想让其顶层属性变得只读和响应式,而不关心其嵌套属性的响应性或只读性时,你可以使用 shallowReadonly

import { shallowReadonly } from "vue";

export default {
  setup() {
    const state = shallowReadonly({
      count: 0,
      nested: {
        message: "Hello",
      },
    });

    // state.count 是只读的
    // state.nested 不是只读的,但 state.nested.message 也不是只读的

    return {
      state,
    };
  },
};

14.1 ShallowReadonly.vue #

src\components\ShallowReadonly.vue

<template>
    <div>
        <p>Current count: {{ readonlyState.count }}</p>
        <button @click="incrementCount">incrementCount</button>
        <p>Message: {{ readonlyState.nested.message }}</p>
        <button @click="changeMessage">changeMessage</button>
    </div>
</template>

<script>
import { reactive, shallowReadonly } from 'vue';

export default {
    setup() {
        const originalState = reactive({
            count: 0,
            nested: {
                message: 'hello'
            }
        });
        const readonlyState = shallowReadonly(originalState);
        const incrementCount = () => {
            readonlyState.count++;
        };
        const changeMessage = () => {
            readonlyState.nested.message = "world"
        };
        return {
            readonlyState,
            incrementCount,
            changeMessage
        };
    }
}
</script>

需要注意的是,尽管 state 的顶层属性是只读的,其嵌套属性(如 state.nested.message)仍然可以被修改。

总之,shallowReadonly 是一个非常有用的工具,尤其是当你只关心对象的顶层属性的只读性和响应性,而不关心其嵌套属性时。

15. ToRaw.vue #

在 Vue 3 中,toRaw 是一个用于获取响应式对象的原始版本的函数。当你有一个响应式对象并需要访问其未经代理的原始数据时,这个函数非常有用。

1. 基本使用

当你想要从响应式对象中获取其原始版本,例如,当你需要对原始对象执行操作而不触发响应式系统时,你可以使用 toRaw

import { reactive, toRaw } from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
      message: "Hello",
    });

    const rawState = toRaw(state);

    // rawState 是 state 的原始版本,不是响应式的

    return {
      state,
      rawState,
    };
  },
};

15.1 ToRaw.vue #

src\components\ToRaw.vue

<template>
    <div>
        <button @click="incrementCount">Increment Count</button>
        <p>Current count: {{ state.count }}</p>
        <button @click="changeRawMessage">Change Raw Message</button>
        <p>Message: {{ state.message }}</p>
    </div>
</template>

<script>
import { reactive, toRaw } from 'vue';

export default {
    setup() {
        const state = reactive({
            count: 0,
            message: 'Hello from Vue 3!'
        });

        const incrementCount = () => {
            state.count++;
        };

        const changeRawMessage = () => {
            const rawState = toRaw(state);
            rawState.message = 'Changed via raw!';
            // 注意:尽管我们更改了 rawState,但 state.message 也会更新,因为它们引用的是同一个对象
        };

        return {
            state,
            incrementCount,
            changeRawMessage
        };
    }
}
</script>

在这个示例中,我们有两个按钮:一个用于增加计数,另一个用于更改消息。尽管我们通过 toRaw 获取的原始对象来更改 message,但由于 staterawState 引用的是同一个对象,所以 state.message 也会更新。

总之,toRaw 是一个非常有用的工具,尤其是当你需要访问响应式对象的原始数据或在其上执行非响应式操作时。

16. MarkRaw #

在 Vue 3 中,markRaw 是一个用于标记一个对象,使其永远不会变成响应式的函数。这对于确保某些对象不被 Vue 的响应式系统跟踪或代理非常有用。

1. 基本使用

当你有一个对象,并且你不希望它变成响应式的,即使你尝试将其传递给 reactiveref,你可以使用 markRaw

import { reactive, markRaw } from "vue";

export default {
  setup() {
    const nonReactiveState = {
      count: 0,
      message: "Hello",
    };

    markRaw(nonReactiveState);

    const state = reactive(nonReactiveState);

    // 尽管我们尝试将 nonReactiveState 传递给 reactive,但由于我们使用了 markRaw,state 仍然不是响应式的

    return {
      state,
    };
  },
};

16.1 MarkRaw.vue #

src\components\MarkRaw.vue

<template>
    <div>
        <button @click="incrementCount">Increment Count</button>
        <p>Current count: {{ state.count }}</p>
        <button @click="changeMessage">Change Message</button>
        <p>Message: {{ state.message }}</p>
    </div>
</template>

<script>
import { reactive, markRaw } from 'vue';

export default {
    setup() {
        const nonReactiveState = {
            count: 0,
            message: 'Hello from Vue 3!'
        };

        markRaw(nonReactiveState);

        const state = reactive(nonReactiveState);

        const incrementCount = () => {
            state.count++;
        };

        const changeMessage = () => {
            state.message = 'Changed message!';
            // 注意:尽管我们更改了 state,但视图不会更新,因为 state 不是响应式的
        };

        return {
            state,
            incrementCount,
            changeMessage
        };
    }
}
</script>

在这个示例中,我们有两个按钮:一个用于增加计数,另一个用于更改消息。尽管我们尝试更改 state 的属性,但视图不会更新,因为由于 markRaw 的使用,state 不是响应式的。

总之,markRaw 是一个非常有用的工具,尤其是当你需要确保某个对象不被 Vue 的响应式系统跟踪或代理时。

17. customRef #

在 Vue 3 中,customRef 是一个用于创建自定义响应式引用的函数。它允许你为引用的 getset 操作提供自定义逻辑,这在某些高级用例中非常有用,例如当你需要在值改变之前或之后执行某些操作时。

1. 基本使用

customRef 接受一个函数,该函数提供两个参数:tracktrigger。这两个函数分别用于手动跟踪和触发响应式依赖。

import { customRef } from "vue";

export default {
  setup() {
    const count = customRef((track, trigger) => {
      let value = 0;
      return {
        get() {
          track();
          return value;
        },
        set(newValue) {
          value = newValue;
          trigger();
        },
      };
    });

    return {
      count,
    };
  },
};

17.1 CustomRef.vue #

src\components\CustomRef.vue

<template>
  <div>
    <button @click="incrementCount">Increment Count</button>
    <p>Current count: {{ count }}</p>
  </div>
</template>

<script>
import { customRef } from "vue";

export default {
  setup() {
    const count = customRef((track, trigger) => {
      let value = 0;
      return {
        get() {
          track();
          console.log("Getting count:", value);
          return value;
        },
        set(newValue) {
          console.log("Setting count from", value, "to", newValue);
          value = newValue;
          trigger();
        },
      };
    });

    const incrementCount = () => {
      count.value++;
    };

    return {
      count,
      incrementCount,
    };
  },
};
</script>

在这个示例中,我们有一个按钮用于增加计数。每次我们获取或设置 count 的值时,都会在控制台中打印消息,这是由于我们在 customRefgetset 函数中添加的自定义逻辑。

总之,customRef 是一个非常有用的工具,尤其是当你需要为响应式引用的 getset 操作提供自定义逻辑时。

17.2 NumericInput #

src\components\NumericInput.vue

<template>
    <div>
        <input v-model="numericInput" placeholder="Only numbers allowed" />
    </div>
</template>

<script>
import { customRef } from 'vue';

export default {
    setup() {
        const numericInput = customRef((track, trigger) => {
            let value = '';
            return {
                get() {
                    track();
                    return value;
                },
                set(newValue) {
                    if (/^\d*$/.test(newValue)) {
                        value = newValue;
                    } else {
                        console.warn('Only numbers are allowed in the input!');
                    }
                    trigger();
                }
            };
        });

        return {
            numericInput
        };
    }
}
</script>

18. is #

在 Vue 3 中,isRefisReactiveisReadonlyisProxy 是用于检查对象的响应式状态的辅助函数。

  1. isRef: 检查一个值是否为 ref 创建的响应式引用。
  2. isReactive: 检查一个对象是否是响应式的(由 reactive 创建)。
  3. isReadonly: 检查一个对象是否是只读的(由 readonly 创建)。
  4. isProxy: 检查一个对象是否是由 Vue 的响应式系统创建的代理(这包括由 reactivereadonly 创建的对象)。

18.1 Is.vue #

src\components\Is.vue

<template>
    <div>
        <p>isRef(refData): {{ isRefCheck }}</p>
        <p>isReactive(reactiveData): {{ isReactiveCheck }}</p>
        <p>isReadonly(readonlyData): {{ isReadonlyCheck }}</p>
        <p>isProxy(reactiveData): {{ isProxyReactiveCheck }}</p>
        <p>isProxy(readonlyData): {{ isProxyReadonlyCheck }}</p>
    </div>
</template>

<script>
import { ref, reactive, readonly, isRef, isReactive, isReadonly, isProxy } from 'vue';

export default {
    setup() {
        const refData = ref(0);
        const reactiveData = reactive({ count: 0 });
        const readonlyData = readonly({ message: 'Hello Vue 3!' });

        const isRefCheck = isRef(refData);
        const isReactiveCheck = isReactive(reactiveData);
        const isReadonlyCheck = isReadonly(readonlyData);
        const isProxyReactiveCheck = isProxy(reactiveData);
        const isProxyReadonlyCheck = isProxy(readonlyData);

        return {
            isRefCheck,
            isReactiveCheck,
            isReadonlyCheck,
            isProxyReactiveCheck,
            isProxyReadonlyCheck
        };
    }
}
</script>

在这个示例中,我们创建了三种不同的响应式数据:refreactivereadonly。然后,我们使用辅助函数来检查这些数据的响应式状态,并在模板中显示结果。

这些辅助函数在开发过程中非常有用,尤其是当你需要根据数据的响应式状态来执行特定的逻辑或操作时。

19. props #

在 Vue 3 的 Composition API 中,setup 函数是组件内部使用 Composition API 的入口点。setup 函数接受两个参数:propscontext。在这里,我们将专注于第一个参数:props

props 参数

props 参数提供了组件接收的所有 prop 的响应式引用。这意味着你可以直接在 setup 函数中访问这些 props,而不需要使用 this 关键字。但请注意,尽管你可以在 setup 函数中读取 props 的值,但你不应该尝试修改它们,因为 props 应该被视为只读的。

在下面的示例中,Child 组件接受一个 message prop 并在模板中显示它。我们在 Child 组件的 setup 函数中直接访问 props.message

总之,props 参数在 setup 函数中提供了一个方便的方式来访问组件的 props,而不需要使用 this 关键字。

19.1 src\App.vue #

src\App.vue

<template>
    <div>
      <Child message="Message passed from App to Child!" />
    </div>
  </template>

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

  export default {
    components: {
      Child
    },
    setup() {
    }
  }
  </script>

19.2 Child.vue #

src\components\Child.vue

<template>
    <div>
        {{ message }}
    </div>
</template>

<script>
export default {
    props: {
        message: {
            type: String,
            default: 'Hello from Child component!'
        }
    },
    setup(props) {
        // 在 setup 函数中,你可以直接访问 props.message
        console.log(props.message);

        // 由于 props 是响应式的,我们不需要再返回它们,它们已经在模板中可用
        return {};
    }
}
</script>

20. attrs #

在 Vue 3 的 Composition API 中,setup 函数的第二个参数是 context,它是一个对象,包含了组件的几个属性:attrsslotsemit

attrs 属性

attrs是一个对象,包含了传递给组件但不是 prop 的属性。这通常用于高阶组件或库组件,这些组件可能不知道它们会接收哪些特定的属性。

在下面的示例中,Child 组件接受一个 message prop 并在模板中显示它。我们还传递了一个额外的 class 属性,这不是 Child 组件的 prop,所以它会出现在 attrs 对象中。我们在 Child 组件的 setup 函数中直接访问 attrs 并在模板中使用它来设置 div 的类。

总之,attrssetup 函数的 context 参数中提供了一个方便的方式来访问传递给组件但不是 prop 的属性。

在模板中,Vue 提供了一些特殊的前缀,如 $attrs$slots,以区分它们和用户定义的响应式数据。

20.1 App.vue #

src\App.vue

<template>
  <div>
+   <Child message="Message passed from App to Child!" class="custom-class" />
  </div>
</template>

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

export default {
  components: {
    Child
  },
  setup() {
  }
}
</script>

20.2 Child.vue #

src\components\Child.vue

<template>
+   <div :class="$attrs.class">
        {{ message }}
    </div>
</template>
<script>
export default {
    props: {
        message: {
            type: String,
            default: 'Hello from Child component!'
        }
    },
    setup(props, { attrs }) {
        // 在 setup 函数中,你可以直接访问 attrs
+       console.log(attrs);
        // 由于 attrs 是响应式的,我们不需要再返回它们,它们已经在模板中可用
        return {};
    }
}
</script>

21.slots #

在 Vue 3 的 Composition API 中,setup 函数的第二个参数是 context,它是一个对象,包含了组件的几个属性:attrsslotsemit

slots 属性

slots 是一个对象,包含了传递给组件的所有插槽。你可以使用 slots 来访问和渲染组件的插槽内容。

在下面示例中,Child 组件有两个插槽:一个默认插槽和一个名为 header 的命名插槽。我们在 Child 组件的 setup 函数中直接访问 slots,并在模板中使用它们来渲染插槽内容。

21.1 src\App.vue #

src\App.vue

<template>
  <div>
    <Child>
      <template #header>
        This is the header slot content.
      </template>
      This is the default slot content.
    </Child>
  </div>
</template>

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

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

21.2 Child.vue #

src\components\Child.vue

<template>
    <div>
        <div class="header">
            <slot name="header"></slot>
        </div>
        <div class="body">
            <slot></slot>
        </div>
    </div>
</template>
<script>
export default {
    setup(_, { slots }) {
        // 在 setup 函数中,你可以直接访问 slots
        console.log(slots);
        // 由于 slots 是响应式的,我们不需要再返回它们,它们已经在模板中可用
        return {};
    }
}
</script>

22.emit #

在 Vue 3 的 Composition API 中,setup 函数的第二个参数是 context,它是一个对象,包含了组件的几个属性:attrsslotsemit

emit 属性

emit 是一个函数,用于触发组件的自定义事件。这对于子组件与父组件之间的通信非常有用。

在这个示例中,当 Child 组件的按钮被点击时,它会触发一个名为 buttonClicked 的自定义事件。在 App 组件中,我们监听这个事件,并在事件触发时显示一个警告。

总之,emitsetup 函数的 context 参数中提供了一个方便的方式来触发组件的自定义事件,从而实现子组件与父组件之间的通信。

22.1 App.vue #

src\App.vue

<template>
  <div>
    <Child @buttonClicked="showAlert" />
  </div>
</template>

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

export default {
  components: {
    Child
  },
  setup() {
    const showAlert = (message) => {
      alert(message);
    };

    return {
      showAlert
    };
  }
}
</script>

22.2 Child.vue #

src\components\Child.vue

<template>
    <button @click="handleClick">Click me!</button>
</template>

<script>
export default {
    setup(_, { emit }) {
        const handleClick = () => {
            // 触发一个自定义事件
            emit('buttonClicked', 'Button was clicked!');
        };

        return {
            handleClick
        };
    }
}
</script>

23.provide/inject #

在 Vue 3 中,provideinject 是 Composition API 的一部分,用于实现依赖注入,从而允许祖先组件提供属性,这些属性可以被其后代组件注入,而不必通过 props 一层一层地传递。

这对于跨多个组件层级共享状态或提供主题等功能特别有用。

provide 函数

provide 函数允许你在当前组件中提供一个值,这个值可以被任何后代组件注入。

inject 函数

inject 函数用于在组件中注入一个由其祖先组件提供的值。

下面个示例中,App 组件提供了一个 themeColor。虽然 Child 组件不使用这个颜色,但 GrandSon 组件注入并使用它来设置文本颜色。

总之,provideinject 提供了一种跨多个组件层级共享状态的方法,而不必通过 props 一层一层地传递。

23.1 App.vue #

src\App.vue

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

<script>
import { provide } from 'vue';
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  setup() {
    const themeColor = 'red';

    // 提供 themeColor
    provide('themeColor', themeColor);
  }
}
</script>

23.2 Child.vue #

src\components\Child.vue

<template>
    <div>
      <GrandSon />
    </div>
  </template>

  <script>
  import GrandSon from './GrandSon.vue';

  export default {
    components: {
      GrandSon
    },
    setup() {
      // Child 组件不需要注入 themeColor,所以这里没有其他逻辑
    }
  }
  </script>

23.3 GrandSon.vue #

src\components\GrandSon.vue

<template>
  <div :style="{ color: themeColor }">
    This text should be in the provided color!
  </div>
</template>

<script>
import { inject } from 'vue';

export default {
  setup() {
    // 从祖先组件中注入 themeColor
    const themeColor = inject('themeColor');

    return {
      themeColor
    };
  }
}
</script>

24.生命周期 #

在 Vue 3,生命周期钩子在 Composition API 中有了新的表示方式。这些新的钩子函数与 Vue 2 的选项式 API 中的生命周期钩子相对应,但它们的名称有所不同,并且是作为导入的函数使用的。

lifecycle

lifecycle

Vue 3 生命周期钩子与 Vue 2 的对比

选项 / 生命周期钩子

组合式 API:生命周期钩子

Vue 2 钩子 Vue 3 钩子 (Composition API)
beforeCreate setup (部分)
created setup (部分)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
activated onActivated
deactivated onDeactivated
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

24.1 App.vue #

src\App.vue

<template>
  <div>
    <!-- 使用 keep-alive 缓存组件 -->
    <keep-alive>
      <component :is="currentComponent" :count="count"></component>
    </keep-alive>
    <!-- 切换按钮 -->
    <button @click="toggleComponent">Toggle Component</button>
    <button @click="increment">increment</button>
  </div>
</template>

<script>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated } from 'vue';
import Child1 from './components/Child1.vue';
import Child2 from './components/Child2.vue';

export default {
  components: {
    Child1,
    Child2
  },
  beforeCreate() {
    console.log('App beforeCreate');
  },
  created() {
    console.log('App created');
  },
  beforeMount() {
    console.log('App beforeMount');
  },
  mounted() {
    console.log('App mounted');
  },
  beforeUpdate() {
    console.log('App beforeUpdate');
  },
  updated() {
    console.log('App updated');
  },
  beforeDestroy() {
    console.log('App beforeDestroy');
  },
  destroyed() {
    console.log('App destroyed');
  },
  activated() {
    console.log('App activated');
  },
  deactivated() {
    console.log('App deactivated');
  },
  setup() {
    // 初始显示 Child1
    const currentComponent = ref('Child1');
    const count = ref(0);

    // 切换组件的方法
    const toggleComponent = () => {
      currentComponent.value = currentComponent.value === 'Child1' ? 'Child2' : 'Child1';
    };
    const increment = () => {
      count.value++;
    };

    onBeforeMount(() => {
      console.log('App onBeforeMount');
    });
    onMounted(() => {
      console.log('App onMounted');
    });
    onBeforeUpdate(() => {
      console.log('App onBeforeUpdate');
    });
    onUpdated(() => {
      console.log('App onUpdated');
    });
    onBeforeUnmount(() => {
      console.log('App onBeforeUnmount');
    });
    onUnmounted(() => {
      console.log('App onUnmounted');
    });
    onActivated(() => {
      console.log('App onActivated');
    });
    onDeactivated(() => {
      console.log('App onDeactivated');
    });

    return {
      currentComponent,
      toggleComponent,
      increment,
      count
    };
  }
}
</script>

24.2 Child1.vue #

src\components\Child1.vue

<template>
  <div>
    Child1 {{count}}
  </div>
</template>

<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated } from 'vue';

export default {
  props:{
    count: Number
  },
  beforeCreate() {
    console.log('Child1 beforeCreate');
  },
  created() {
    console.log('Child1 created');
  },
  beforeMount() {
    console.log('Child1 beforeMount');
  },
  mounted() {
    console.log('Child1 mounted');
  },
  beforeUpdate() {
    console.log('Child1 beforeUpdate');
  },
  updated() {
    console.log('Child1 updated');
  },
  beforeDestroy() {
    console.log('Child1 beforeDestroy');
  },
  destroyed() {
    console.log('Child1 destroyed');
  },
  activated() {
    console.log('Child1 activated');
  },
  deactivated() {
    console.log('Child1 deactivated');
  },
  setup() {
    onBeforeMount(() => {
      console.log('Child1 onBeforeMount');
    });
    onMounted(() => {
      console.log('Child1 onMounted');
    });
    onBeforeUpdate(() => {
      console.log('Child1 onBeforeUpdate');
    });
    onUpdated(() => {
      console.log('Child1 onUpdated');
    });
    onBeforeUnmount(() => {
      console.log('Child1 onBeforeUnmount');
    });
    onUnmounted(() => {
      console.log('Child1 onUnmounted');
    });
    onActivated(() => {
      console.log('Child1 onActivated');
    });
    onDeactivated(() => {
      console.log('Child1 onDeactivated');
    });
  }
}
</script>

24.3 Child2.vue #

src\components\Child1.vue

<template>
  <div>
    Child2 {{count}}
  </div>
</template>

<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated } from 'vue';

export default {
  props:{
    count: Number
  },
  beforeCreate() {
    console.log('Child2 beforeCreate');
  },
  created() {
    console.log('Child2 created');
  },
  beforeMount() {
    console.log('Child2 beforeMount');
  },
  mounted() {
    console.log('Child2 mounted');
  },
  beforeUpdate() {
    console.log('Child2 beforeUpdate');
  },
  updated() {
    console.log('Child2 updated');
  },
  beforeDestroy() {
    console.log('Child2 beforeDestroy');
  },
  destroyed() {
    console.log('Child2 destroyed');
  },
  activated() {
    console.log('Child2 activated');
  },
  deactivated() {
    console.log('Child2 deactivated');
  },
  setup() {
    onBeforeMount(() => {
      console.log('Child2 onBeforeMount');
    });
    onMounted(() => {
      console.log('Child2 onMounted');
    });
    onBeforeUpdate(() => {
      console.log('Child2 onBeforeUpdate');
    });
    onUpdated(() => {
      console.log('Child2 onUpdated');
    });
    onBeforeUnmount(() => {
      console.log('Child2 onBeforeUnmount');
    });
    onUnmounted(() => {
      console.log('Child2 onUnmounted');
    });
    onActivated(() => {
      console.log('Child2 onActivated');
    });
    onDeactivated(() => {
      console.log('Child2 onDeactivated');
    });
  }
}
</script>

25.onErrorCaptured #

onErrorCaptured 是 Vue 3 中的一个生命周期钩子,它允许你捕获来自子组件的错误。当子组件抛出一个错误时,这个钩子会被触发。你可以在这个钩子中处理错误,例如记录错误或显示一个错误消息。

下面是一个简单的示例,其中 Child 组件会抛出一个错误,而 App 组件会使用 onErrorCaptured 钩子来捕获并处理这个错误。

在这个示例中,当你点击 Child 组件中的按钮时,它会抛出一个错误。这个错误会被 App 组件中的 onErrorCaptured 钩子捕获,并显示在页面上。同时,错误信息也会被打印到控制台。

25.1 App.vue #

src\App.vue

<template>
  <div>
    <Child />
    <div v-if="error">{{ error.message }}</div>
  </div>
</template>

<script>
import { ref } from 'vue';
import Child from './components/Child.vue';

export default {
    components:{
        Child
    },
    setup() {
        const error = ref(null);

        const handleError = (err, instance, info) => {
            error.value = err;
            console.error("Error captured:", err, "Info:", info);
        };

        return {
            error,
            onErrorCaptured: handleError
        };
    }
}
</script>

25.2 Child.vue #

src\components\Child.vue

<template>
  <button @click="throwError">Click to throw error</button>
</template>

<script>
export default {
    setup() {
        const throwError = () => {
            throw new Error("This is an error from Child component");
        };

        return {
            throwError
        };
    }
}
</script>

26.onRenderTracked #

onRenderTracked 是 Vue 3 中的一个调试用的生命周期钩子。当响应式依赖被访问并被跟踪时,这个钩子会被触发。这对于调试渲染函数中的响应式依赖关系非常有用。

下面是一个简单的示例,其中 Child 组件有一个响应式的 count 属性。当这个属性在模板中被访问时,onRenderTracked 钩子会被触发,并记录被跟踪的依赖。

在这个示例中,当 Child 组件的模板被渲染时,count 属性会被访问,这会触发 onRenderTracked 钩子。你可以在控制台中看到被跟踪的依赖的详细信息。

请注意,onRenderTracked 主要用于调试,所以在生产环境中,你可能不需要使用它。

26.1 App.vue #

src\App.vue

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

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

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

26.2 Child.vue #

src\components\Child.vue

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

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

  export default {
      setup() {
          const count = ref(0);

          const increment = () => {
              count.value++;
          };

          onRenderTracked(event => {
              console.log('Tracked:', event);
          });

          return {
              count,
              increment
          };
      }
  }
  </script>

27. onRenderTriggered #

onRenderTriggered 是 Vue 3 中的一个调试用的生命周期钩子。当组件的渲染被一个响应式依赖的变化所触发时,这个钩子会被调用。这对于调试和了解哪个响应式依赖导致了组件的重新渲染非常有用。

下面是一个简单的示例,其中 Child 组件有一个响应式的 count 属性。当这个属性改变并导致组件重新渲染时,onRenderTriggered 钩子会被触发,并记录导致重新渲染的依赖。

在这个示例中,当你点击 Child 组件中的按钮并改变 count 属性的值时,这会导致组件重新渲染,从而触发 onRenderTriggered 钩子。你可以在控制台中看到导致重新渲染的依赖的详细信息。

onRenderTracked 类似,onRenderTriggered 主要用于调试,所以在生产环境中,你可能不需要使用它。

27.1 App.vue #

src\App.vue

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

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

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

27.2 Child.vue #

src\components\Child.vue

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

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

export default {
    setup() {
        const count = ref(0);

        const increment = () => {
            count.value++;
        };

        onRenderTriggered(event => {
            console.log('Render triggered by:', event);
        });

        return {
            count,
            increment
        };
    }
}
</script>

28. 自定义 hook 函数 #

在 Vue 3 中,由于 Composition API 的引入,我们可以创建自定义的 hook 函数。这些函数可以封装和重用逻辑,从而使我们的代码更加模块化和可维护。这与 React 中的自定义 hook 非常相似。

什么是自定义 hook?

自定义 hook 是一个普通的 JavaScript 函数,但它可以利用 Vue 的响应式系统和其他 Composition API 函数。它的主要目的是提供一个封装特定逻辑的方法,这样你就可以在多个组件之间重用这些逻辑,而不必重复代码。

如何创建自定义 hook?

  1. 定义一个函数:自定义 hook 是一个函数,通常以 use 开头(这是一个约定,不是必须的)。
  2. 使用 Composition API:在这个函数内部,你可以使用任何 Composition API 的功能,如 ref, reactive, computed, watch, 等等。
  3. 返回值:自定义 hook 可以返回任何值,通常是一个响应式引用或一个函数,或者两者的组合。

28.1 UseLocalStorage.vue #

src\components\UseLocalStorage.vue

我们将使用 useLocalStorage 自定义 hook 来在 App.vue 中存储和读取一个用户的名字。当用户输入他们的名字并刷新页面时,他们的名字会被保存在本地存储中并在下次加载时显示出来。

<template>
    <div>
        <input v-model="name" placeholder="Enter your name" />
        <p>Hello, {{ name }}!</p>
    </div>
</template>

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

function useLocalStorage(key, defaultValue) {
    const storedValue = localStorage.getItem(key);
    const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
    watch(value, (newValue) => localStorage.setItem(key, JSON.stringify(newValue)));
    return value;
}

export default {
    setup() {
        const name = useLocalStorage('username', 'Guest');
        return {
            name
        };
    }
}
</script>

28.2 UseFetch.vue #

src\components\UseFetch.vue

我们将使用 useFetch 自定义 hook 来在 App.vue 中从一个 URL 获取数据。这个 hook 会返回数据、加载状态和任何错误。

<template>
    <div>
        <!-- 显示加载状态 -->
        <div v-if="loading">Loading...</div>

        <!-- 显示数据 -->
        <div v-else-if="data">
            <pre>{{ data }}</pre>
        </div>

        <!-- 显示错误 -->
        <div v-if="error">{{ error.message }}</div>
    </div>
</template>

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

function useFetch(url) {
    const data = ref(null);
    const loading = ref(true);
    const error = ref(null);
    fetch(url)
        .then(res => res.json())
        .then(result => { data.value = result; })
        .catch(e => { error.value = e; })
        .finally(() => { loading.value = false; });
    return { data, loading, error };
}

export default {
    setup() {
        const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1');

        return {
            data,
            loading,
            error
        };
    }
}
</script>

28.3 UseDebounce.vue #

src\components\UseDebounce.vue

我们将使用 useDebounce 自定义 hook 来在 App.vue 中实现一个防抖动的输入框。这意味着,当用户在输入框中输入时,我们将等待他们停止输入一段时间(例如 300 毫秒)后,再获取他们的输入值。

<template>
    <div>
      <input v-model="inputValue" placeholder="Type something..." />
      <p>Debounced value: {{ debouncedInputValue }}</p>
    </div>
  </template>

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

  function useDebounce(value, delay = 300) {
      const debouncedValue = ref(value.value);
      let timeout;
      watch(value, (newValue) => {
          clearTimeout(timeout);
          timeout = setTimeout(() => {
              debouncedValue.value = newValue;
          }, delay);
      });
      return debouncedValue;
  }

  export default {
      setup() {
          const inputValue = ref('');
          const debouncedInputValue = useDebounce(inputValue);

          return {
              inputValue,
              debouncedInputValue
          };
      }
  }
  </script>

28.4 UseThrottle.vue #

src\components\UseThrottle.vue

我们将使用 useThrottle 自定义 hook 来在 App.vue 中实现一个节流的输入框。这意味着,当用户在输入框中输入时,我们将在指定的时间间隔内只获取他们的输入值一次,而忽略该时间段内的其他输入。

在这个示例中,我们有一个输入框,用户可以在其中输入文本。输入的实时值存储在 inputValue 中,而节流的值存储在 throttledInputValue 中。只有当用户的输入间隔超过约 300 毫秒时,throttledInputValue 才会更新为用户的最新输入值。

<template>
    <div>
      <input v-model="inputValue" placeholder="Type something..." />
      <p>Throttled value: {{ throttledInputValue }}</p>
    </div>
  </template>

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

  function useThrottle(value, limit = 300) {
      const throttledValue = ref(value.value);
      let lastRan = Date.now();
      watch(value, (newValue) => {
          if (Date.now() - lastRan >= limit) {
              throttledValue.value = newValue;
              lastRan = Date.now();
          }
      });
      return throttledValue;
  }

  export default {
      setup() {
          const inputValue = ref('');
          const throttledInputValue = useThrottle(inputValue);

          return {
              inputValue,
              throttledInputValue
          };
      }
  }
  </script>

29. Fragment #

在 Vue 3 中,Fragment 是一个新特性,允许组件模板有多个根节点。在 Vue 2 中,每个组件模板必须有一个单独的根元素。但在 Vue 3 中,这个限制被移除了。

什么是 Fragment?

Fragment,简单来说,就是没有根元素的模板。这意味着你可以在组件模板中直接返回多个元素,而不需要将它们包裹在一个父元素中。

如何使用 Fragment?

在 Vue 3 中,你只需正常编写模板即可。如果模板有多个根节点,Vue 会自动处理它们作为 Fragment。

实用价值

  1. 简化 DOM:有时,为了满足 Vue 2 的单一根元素要求,我们可能会添加不必要的 DOM 元素。使用 Fragment 可以避免这种情况,从而得到一个更简洁、更直观的 DOM 结构。
  2. 更自然的组件:在某些情况下,将多个元素组合在一个组件中是有意义的,但不希望为它们添加额外的包裹元素。Fragment 使这成为可能。

  3. 更好的样式和布局:避免不必要的包裹元素可以简化 CSS 样式和布局,特别是在使用 Flexbox 或 Grid 布局时。

总之,Fragment 是 Vue 3 中的一个小但非常有用的特性,它使组件模板更加灵活和直观。

29.1 src\App.vue #

src\App.vue

<template>
    <h1>Welcome to Vue 3</h1>
    <p>This is a fragment example.</p>
</template>
<script>
export default {

}
</script>

30. Teleport #

在 Vue 3 中,Teleport 是一个新特性,允许你将组件模板的一部分“传送”到 DOM 的其他位置,而不是放在它的实际位置。这对于模态框、通知、弹出窗口等 UI 元素特别有用,因为这些元素通常需要从 DOM 的深层次中“浮出”来,避免父元素的样式或布局影响。

什么是 Teleport?

Teleport 提供了一种将组件的子元素渲染到 DOM 树中的其他位置的方法,而不是它们在 Vue 组件树中的位置。

如何使用 Teleport?

使用 <teleport> 标签,并使用 to 属性指定目标元素的选择器。

实用价值

  1. 避免样式和布局问题:Teleport 可以确保 UI 元素(如模态框或弹出窗口)不受其父元素或祖先元素的样式和布局影响。

  2. 更好的可访问性:对于模态框等 UI 元素,将它们渲染到 body 的直接子元素中可以提高可访问性。

  3. 灵活性:Teleport 提供了一种灵活的方式,可以根据需要将 UI 元素渲染到 DOM 的任何位置。

假设我们要创建一个模态框。我们希望模态框始终渲染在 body 的直接子元素中,而不是嵌套在其他 DOM 元素中。这样,我们可以更容易地控制模态框的样式和位置。

30.1 App.vue #

src\App.vue

<template>
    <div>
        <button @click="showModal = !showModal">Toggle Modal</button>

        <teleport to="#modal-container">
            <div v-if="showModal" class="modal">
                <h2>Modal Title</h2>
                <p>This is a modal content.</p>
                <button @click="showModal = false">Close</button>
            </div>
        </teleport>
    </div>
</template>

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

export default {
    setup() {
        const showModal = ref(false);

        return {
            showModal
        };
    }
}
</script>

<style>
.modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 80%;
    max-width: 500px;
    padding: 20px;
    background-color: #ffffff;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    z-index: 1000;
    animation: fadeIn 0.3s;
}

.modal h2 {
    margin-top: 0;
    font-size: 24px;
    color: #333;
}

.modal p {
    font-size: 16px;
    color: #666;
    margin: 20px 0;
}

.modal button {
    display: inline-block;
    padding: 10px 20px;
    font-size: 16px;
    color: #fff;
    background-color: #007BFF;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.modal button:hover {
    background-color: #0056b3;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translate(-50%, -60%);
    }

    to {
        opacity: 1;
        transform: translate(-50%, -50%);
    }
}
</style>

30.2 index.html #

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3</title>
</head>
<body>
    <div id="app"></div>
+   <div id="modal-container"></div>
    <script type="module" src="/src/main.js"></script>
</body>
</html>

31. Suspense #

在 Vue 3 中,<suspense> 是一个新的内置组件,用于等待嵌套的异步依赖项或组件直到它们解析为止。它与 Vue 3 的异步组件和 Composition API 中的 async setup() 配合使用,为你提供了一种优雅的方式来处理异步操作和显示加载状态。

什么是 Suspense?

Suspense 允许你“等待”组件的异步逻辑,直到它完成,并在此期间显示一些备用内容(例如加载指示器)。

如何使用 Suspense?

你可以使用 <suspense> 的两个插槽:#default#fallback#default 插槽包含你的主要内容,而 #fallback 插槽包含在等待异步组件或逻辑时要显示的内容。

实用价值

  1. 优雅的异步处理:与 Vue 2 中的异步组件相比,Suspense 提供了一种更加直观和优雅的方式来处理组件的异步逻辑。
  2. 更好的用户体验:通过使用 Suspense,你可以为用户提供即时的反馈,告诉他们内容正在加载,从而提供更好的用户体验。

  3. 与 Composition API 配合:Suspense 不仅可以与异步组件配合使用,还可以与 Composition API 中的 async setup() 配合使用,使你的组件逻辑更加清晰和模块化。

假设我们有一个异步组件,该组件从 API 获取数据。我们希望在数据加载时显示一个加载指示器。

AsyncComponent 加载数据时,用户会看到 "Loading..."。一旦数据加载完成,AsyncComponent 的内容将替换 "Loading..."。

31.1 App.vue #

src\App.vue

<template>
    <suspense>
        <template #default>
            <AsyncComponent />
        </template>
        <template #fallback>
            <div>Loading...</div>
        </template>
    </suspense>
</template>

<script>
import AsyncComponent from './AsyncComponent.vue';

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

31.2 AsyncComponent.vue #

src\AsyncComponent.vue

<template>
    <div>
        {{ data }}
    </div>
</template>

<script>
function fetchDataFromAPI() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Fetched data from API!");
        }, 2000);
    });
}

export default {
    async setup() {
        const data = await fetchDataFromAPI();
        return {
            data
        };
    }
}
</script>

32. createRenderer #

createRenderer API 来自 @vue/runtime-core,它允许开发者创建自定义的渲染器。这是 Vue 3 的一个强大特性,它使得 Vue 可以在不同的平台上运行,例如 web、native mobile、或 even 3D engines。

createRenderer 提供了一种低级的方式来定义如何将虚拟 DOM 转化为实际的平台特定的视图。Vue 3 的默认 web 渲染器就是使用这个 API 创建的。

如何使用 createRenderer?

createRenderer 接受一个对象,该对象定义了一系列的方法,这些方法描述了如何处理虚拟节点的各个方面,例如创建、更新、删除等。

为什么使用 createRenderer?

  1. 跨平台渲染:你可以为不同的平台或渲染目标创建自定义的渲染器。例如,为原生移动应用、WebGL、Canvas 或 even terminal interfaces 创建渲染器。

  2. 优化特定用例:如果你有一个非常特定的用例,你可以创建一个高度优化的渲染器,只包含你需要的功能。

  3. 实验和学习createRenderer 是一个很好的工具,用于深入了解 Vue 的内部工作原理,或进行与渲染相关的实验。

32.1 main.js #

src\main.js

import { createRenderer, h } from "@vue/runtime-core";
const { render } = createRenderer({
  createElement(element) {
    return document.createElement(element);
  },
  setElementText(el, text) {
    el.innerHTML = text;
  },
  insert(el, container) {
    container.appendChild(el);
  },
});
render(h("h1", "hello world"), document.getElementById("app"));

这段代码展示了如何使用 Vue 3 的 createRenderer API 来创建一个简单的自定义渲染器,该渲染器将虚拟 DOM 节点渲染为真实的 DOM 节点。让我们逐步解析这段代码:

  1. 导入必要的函数:

    import { createRenderer, h } from "@vue/runtime-core";
    
    • createRenderer:用于创建自定义渲染器。
    • h:一个帮助函数,用于创建虚拟节点。
  2. 创建自定义渲染器:

    const { render } = createRenderer({
      createElement(element) {
        return document.createElement(element);
      },
      setElementText(el, text) {
        el.innerHTML = text;
      },
      insert(el, container) {
        container.appendChild(el);
      },
    });
    
    • createElement:当渲染器需要创建一个新的 DOM 元素时,会调用此方法。
    • setElementText:当渲染器需要设置元素的文本内容时,会调用此方法。
    • insert:当渲染器需要将元素插入到容器中时,会调用此方法。
  3. 使用渲染器:

    render(h("h1", "hello world"), document.getElementById("app"));
    
    • h('h1', 'hello world'):使用 h 函数创建一个虚拟节点,表示一个 <h1> 元素,其内容为 "hello world"。
    • document.getElementById('app'):选择一个真实的 DOM 容器,用于插入渲染的内容。
    • render:使用上面创建的自定义渲染器将虚拟节点渲染为真实的 DOM 节点,并插入到指定的容器中。

结果:

这段代码将在页面上的 #app 元素中渲染一个 <h1> 标签,内容为 "hello world"。

33. v-bind-in-css #

v-bind-in-css

单文件组件的 style 标签支持使用 v-bind CSS 函数将 CSS 的值链接到动态的组件状态

<template>
  <div class="box">
    box
    <button @click="changeColor">changeColor</button>
  </div>
</template>

<script>
import { ref } from "vue";

export default {
  setup() {
    const color = ref("red"); // 默认颜色
    const bgColor = ref("green"); // 默认颜色
    const changeColor = () => {
      color.value = "green";
      bgColor.value = "red";
    };
    return {
      color,
      bgColor,
      changeColor,
    };
  },
};
</script>

<style>
.box {
  padding: 20px;
  color: v-bind(color);
  background-color: v-bind(bgColor);
}
</style>

34. 单文件组件 CSS 功能 #

34.1 组件作用域 CSS #

scoped-css

当 style 标签带有 scoped attribute 的时候,它的 CSS 只会影响当前组件的元素

src\App.vue

<template>
    <div class="box">hello</div>
</template>

<script>
export default {
    setup() {
        return {};
    }
}
</script>

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

34.2 深度选择器 #

深度选择器

处于 scoped 样式中的选择器如果想要做更“深度”的选择,也即:影响到子组件,可以使用 :deep() 这个伪类:

34.2.1 App.vue #

src\App.vue

<template>
    <div class="box">
        <Child></Child>
    </div>
</template>

<script>
import Child from './components/Child.vue';
export default {
    components:{
        Child
    },
    setup() {
        return {};
    }
}
</script>

<style scoped>
.box :deep(.child) {
    color: red;
}
</style>

34.2.2 Child.vue #

src\components\Child.vue

<template>
    <div class="child">
        Child
    </div>
</template>

<script>
export default {
    setup() {

        return {

        };
    }
}
</script>
<style scoped>

</style>

34.3 插槽选择器 #

插槽选择器

默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以明确地将插槽内容作为选择器的目标:

34.3.1 App.vue #

src\App.vue

<template>
    <div class="box">
        <Child>
            <p>Child</p>
        </Child>
    </div>
</template>

<script>
import Child from './components/Child.vue';
export default {
    components: {
        Child
    },
    setup() {
        return {};
    }
}
</script>

<style scoped></style>

34.3.2 Child.vue #

src\components\Child.vue

<template>
    <div>
        <slot></slot>
    </div>
</template>

<script>
export default {
    setup() {

        return {

        };
    }
}
</script>
<style scoped>
:slotted(p) {
  color: red;
}
</style>

34.4 全局选择器 #

全局选择器如果想让其中一个样式规则应用到全局,比起另外创建一个 <style>,可以使用 :global 伪类来实现:

34.4.1 App.vue #

src\App.vue

<template>
    <div class="box">
        <Child>
            <p>Child</p>
        </Child>
    </div>
</template>

<script>
import Child from './components/Child.vue';
export default {
    components: {
        Child
    },
    setup() {
        return {};
    }
}
</script>

<style scoped>
:global(.global-class) {
  color: red;
}
</style>

35. 其它更新 #

35.1 全局 API #

当 Vue 3 引入了新的应用 API,许多全局 API 和配置选项从 Vue 2 的全局范围转移到了 Vue 3 的应用实例。以下是 Vue 2 和 Vue 3 之间全局 API 转移的对比:

| Vue 2 API                         | Vue 3 API                           | 描述                                                               |
| --------------------------------- | ----------------------------------- | ------------------------------------------------------------------ |
| `new Vue({ ... })`                | `createApp({ ... })`                | 创建一个新的 Vue 应用实例。                                        |
| `Vue.component(name, definition)` | `app.component(name, definition)`   | 注册或获取全局组件。                                               |
| `Vue.directive(name, definition)` | `app.directive(name, definition)`   | 注册或获取全局指令。                                               |
| `Vue.filter(name, function)`      | Removed in Vue 3                    | Vue 3 中已移除全局过滤器。                                         |
| `Vue.mixin(mixin)`                | `app.mixin(mixin)`                  | 注册全局混入。                                                     |
| `Vue.use(plugin)`                 | `app.use(plugin)`                   | 安装 Vue.js 插件。                                                 |
| `Vue.prototype.customProperty`    | `app.config.globalProperties`       | 在 Vue 3 中,为所有组件实例定义全局属性。                          |
| `Vue.config.key = value`          | `app.config.key = value`            | 在 Vue 3 中,应用级别的配置。                                      |
| `Vue.set(target, key, value)`     | Use native JavaScript               | Vue 3 推荐使用原生 JavaScript 代替,因为它现在有更好的响应性系统。 |
| `Vue.delete(target, key)`         | Use native JavaScript               | 同上。                                                             |
| `Vue.observable(object)`          | `ref(object)` or `reactive(object)` | 使一个对象变得响应式。                                             |

这个表格提供了 Vue 2 和 Vue 3 之间全局 API 转移的简单对比。这种转移的目的是为了使 Vue 更加模块化,并允许多个 Vue 应用共享同一个运行时,而不会相互干扰。

35.2 过渡类名 #

在 Vue 3 中,过渡类名的默认值发生了一些变化,以使其更加直观。以下是 Vue 2 和 Vue 3 之间过渡类名的对比:

Vue 2 Class Names Vue 3 Class Names 描述
v-enter v-enter-from 定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
v-leave v-leave-from 定义离开过渡的开始状态。在离开过渡被触发时生效,在动画/过渡开始之后的下一帧移除。
v-enter-active v-enter-active 定义进入过渡的活动状态(例如,持续时间、延迟等)。在整个进入过渡期间生效。
v-leave-active v-leave-active 定义离开过渡的活动状态。在整个离开过渡期间生效。
v-enter-to v-enter-to 新引入于 Vue 2.1.8。定义进入过渡的结束状态。在元素被插入之后,下一帧开始生效。
v-leave-to v-leave-to 新引入于 Vue 2.1.8。定义离开过渡的结束状态。在动画/过渡开始之后的下一帧开始生效。

35.3 修饰符 #

从 Vue 2 到 Vue 3,一些事件修饰符和模式发生了变化。以下是关于 keyCode.native 修饰符的变更:

1. keyCode 事件修饰符

在 Vue 2 中,你可以使用 keyCode 修饰符来监听特定键的键盘事件:

<input @keyup.13="submit" />

在上面的例子中,.13Enter 键的键码。

变更: 在 Vue 3 中,keyCode 修饰符已被废弃,因为它不是一个推荐的实践(键码在不同的键盘布局和浏览器中可能会有所不同)。取而代之的是,你应该使用键盘事件的 key 属性:

<input @keyup.enter="submit" />

2. .native 修饰符

在 Vue 2 中,如果你想在一个组件上监听原生事件(而不是该组件触发的自定义事件),你可以使用 .native 修饰符:

<MyComponent @click.native="doSomething" />

变更: 在 Vue 3 中,.native 修饰符已被移除。如果你想在组件上监听原生事件,你应该在组件内部使用 v-on="$listeners" 或者使用新的 emits 选项来定义组件可以触发的事件。

但是,对于大多数常见的原生监听器(如 click),你可以直接在 Vue 3 的组件上使用它们,而不需要 .native 修饰符:

<MyComponent @click="doSomething" />

36. TodoApp #

36.1 TodoApp.vue #

src\components\TodoApp\TodoApp.vue

<template>
    <div>
        <todo-input @add="addTodo" />
        <todo-list :todos="todos" @edit="editTodo" @remove="removeTodo" />
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import TodoInput from './TodoInput.vue';
import TodoList from './TodoList.vue';

const newTodo = ref('');
const todos = reactive([]);

function addTodo(text) {
    todos.push({
        id: Date.now(),
        text: text,
        completed: false,
    });
}

function removeTodo(todo) {
    const index = todos.indexOf(todo);
    if (index !== -1) {
        todos.splice(index, 1);
    }
}

function editTodo(todo, newText) {
    todo.text = newText;
}
</script>

36.2 TodoInput.vue #

src\components\TodoApp\TodoInput.vue

<template>
    <input v-model="newTodo" @keyup.enter="add" placeholder="Add a new todo" />
</template>

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

const newTodo = ref('');
const emit = defineEmits();

const add = () => {
    if (newTodo.value.trim()) {
        emit('add', newTodo.value.trim());
        newTodo.value = '';
    }
};
</script>

36.3 TodoList.vue #

src\components\TodoApp\TodoList.vue

<template>
    <ul>
        <todo-item v-for="todo in todos" :key="todo.id" :todo="todo" @edit="editTodo" @remove="removeTodo" />
    </ul>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';
import TodoItem from './TodoItem.vue';

const props = defineProps({
    todos: Array
});

const emit = defineEmits();

const editTodo = (todo, newText) => {
    emit('edit', todo, newText);
};

const removeTodo = (todo) => {
    emit('remove', todo);
};
</script>

36.4 TodoItem.vue #

src\components\TodoApp\TodoItem.vue

<template>
    <li>
        <input type="checkbox" v-model="todo.completed" />
        <span @dblclick="edit" :class="{ completed: todo.completed }">{{ todo.text }}</span>
        <button @click="remove">Delete</button>
    </li>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
    todo: Object
});

const emit = defineEmits();

const edit = () => {
    const currentText = props.todo.text;
    const newText = prompt('Edit todo', currentText);
    if (newText !== null && newText.trim()) {
        emit('edit', props.todo, newText);
    }
};

const remove = () => {
    emit('remove', props.todo);
};
</script>

<style scoped>
.completed {
    text-decoration: line-through;
    color: gray;
}
</style>

37. Toast #

37.1 src\App.vue #

src\App.vue

<template>
    <button @click="displayToast">Display Toast</button>
</template>

<script setup>
import { showToast } from './components/Toast';

function displayToast() {
    showToast({
        message: "This is a toast message",
        duration: 3000
    });
}
</script>

37.2 Toast\index.js #

src\components\Toast\index.js

import Toast from "./Toast.vue";
import { render, h } from "vue";
export function showToast(options = {}) {
  const { message = "", duration = 3000 } = options;
  const container = document.createElement("div");
  render(
    h(Toast, {
      message,
      duration,
      onDestroy: () => {
        document.body.removeChild(container);
      },
    }),
    container
  );
  document.body.appendChild(container);
}

37.3 Toast.vue #

src\components\Toast\Toast.vue

<template>
    <transition name="fade" @after-leave="handleAfterLeave">
        <div class="toast" v-if="isVisible">
            <slot>{{ message }}</slot>
        </div>
    </transition>
</template>

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

const { message, duration } = defineProps(['message', 'duration']);
const emit = defineEmits(['destroy']);

const isVisible = ref(false);
let timer;
onMounted(() => {
    isVisible.value = true;
    timer = setTimeout(() => {
        isVisible.value = false;
    }, duration);
});
onUnmounted(() => {
    clearTimeout(timer);
})

function handleAfterLeave() {
    emit('destroy');
}
</script>

<style scoped>
.toast {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    padding: 20px;
    background: rgba(0, 0, 0, .7);
    color: white;
    border-radius: 5px;
    text-align: center;
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity 1s ease;
}

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

参考 #

1.SFC #

SFC(Single File Components)是 Vue 中的一个核心概念,它允许你在一个 .vue 文件中定义模板、脚本和样式。在 Vue 3 中,SFC 仍然是一个核心特性,并且与 Vue 2 中的使用方式非常相似,但也有一些新的特性和改进。

以下是 Vue 3 中 SFC 的主要部分和特性:

  1. 模板 (Template)

    • 使用 <template> 标签定义。
    • 包含组件的 HTML 结构。
    • 可以使用 Vue 指令,如 v-ifv-forv-bind 等。
  2. 脚本 (Script)

    • 使用 <script> 标签定义。
    • 可以使用 export default 导出一个对象,该对象定义了组件的数据、方法、生命周期钩子等。
    • Vue 3 中,你可以选择使用 Options API(类似于 Vue 2 的方式)或新的 Composition API。
  3. 样式 (Style)

    • 使用 <style> 标签定义。
    • 可以是全局的或模块化的(使用 scoped 属性)。
    • 支持多种预处理器,如 SCSS、LESS 等,只需相应地配置你的构建工具。
  4. 自定义块

    • 除了上述三个主要部分,你还可以在 SFC 中定义自定义块,如 <docs><i18n>,用于文档或国际化等目的。
    • 这些块可以被构建工具和插件使用。
  5. 新的 <script setup>

    • Vue 3.2 引入了一个新的 <script setup> 语法糖,它允许你在一个更简洁的环境中使用 Composition API。
    • 使用这种方式,你可以直接导出 refreactive 和其他 Composition API 的功能,而不需要一个 setup 函数。
  6. Vite 和 SFC

    • Vite 是一个新的构建工具,它为 Vue 3 提供了一流的支持。
    • 使用 Vite,你可以非常快速地启动一个 Vue 3 项目,并且 Vite 对 SFC 有原生支持,这意味着超快的热模块替换和其他优化。

总的来说,SFC 提供了一个清晰、模块化的方式来组织和定义 Vue 组件。在 Vue 3 中,尽管 SFC 的基本概念保持不变,但新的特性和改进使得开发体验更加流畅和高效。

2.type=module #

在 HTML 中,<script type="module"> 是一个相对较新的特性,它允许你在浏览器中直接使用 ES6+ 的模块功能。这意味着你可以使用 importexport 语句,而不需要构建工具或模块加载器。

以下是关于 <script type="module"> 的一些关键点:

  1. 默认延迟加载:

    • 使用 type="module" 的脚本默认具有 defer 的行为,这意味着它们会在文档解析完成后、DOMContentLoaded 事件之前执行,而不会阻塞 HTML 的解析。
    • 你不需要显式地添加 defer 属性,它已经是模块脚本的默认行为。
  2. 静态导入:

    • 你可以使用 ES6 的 import 语句来导入其他模块。
    • 这些导入是静态的,意味着它们必须位于模块的顶部,并且不能被条件化。
  3. 动态导入:

    • 你可以使用 import() 函数来动态地导入模块。这返回一个 promise,当模块加载和解析完成时,该 promise 会被解析。
  4. CORS 和 MIME 类型:

    • 模块脚本遵循 CORS(跨源资源共享)策略。这意味着从其他域加载的模块需要适当的 CORS 头部。
    • 服务器必须提供正确的 MIME 类型,通常是 application/javascript,否则浏览器不会执行模块。
  5. 没有顶级变量污染:

    • 在模块脚本中,顶级变量(如 letconstclassfunction)是局部的,不会污染全局命名空间。
  6. 浏览器支持:

    • 大多数现代浏览器都支持模块脚本,但旧版本的浏览器(如 Internet Explorer)不支持。
    • 为了确保向后兼容性,你可以使用 <script nomodule>。不支持模块的浏览器会执行 nomodule 脚本,而支持模块的浏览器则会忽略它。
  7. 使用示例:

    <!-- 导入外部模块 -->
    <script type="module" src="./my-module.js"></script>
    
    <!-- 内联模块脚本 -->
    <script type="module">
      import { myFunction } from "./my-module.js";
      myFunction();
    </script>
    

总的来说,<script type="module"> 提供了一种在浏览器中直接使用 ES6+ 模块的方式,无需依赖构建工具或模块加载器。这为前端开发带来了更大的灵活性和更现代的开发体验。

3.createApp #

createApp 是 Vue 3 中的一个核心函数,用于创建一个新的 Vue 应用实例。与 Vue 2 的 new Vue() 初始化方式相比,createApp 提供了一个更清晰、更模块化的方法来创建和配置 Vue 应用。

以下是关于 createApp 的详细解释:

  1. 基本使用: 你可以使用 createApp 函数创建一个新的 Vue 应用实例,并传入一个根组件:

    import { createApp } from "vue";
    import App from "./App.vue";
    
    const app = createApp(App);
    
  2. 挂载: 创建应用实例后,你需要将其挂载到某个 DOM 元素上。这通常是一个具有特定 ID 的 <div> 元素:

    app.mount("#app");
    
  3. 插件和全局配置: 使用 createApp 创建的应用实例提供了一系列方法,允许你在应用级别进行配置和扩展:

    • use: 允许你安装 Vue 插件。
    • mixin: 允许你添加全局混入。
    • componentdirective: 允许你注册全局组件和指令。

    示例:

    import MyPlugin from "./plugins/MyPlugin";
    
    app.use(MyPlugin);
    app.component("MyGlobalComponent", MyGlobalComponent);
    app.directive("my-global-directive", myGlobalDirective);
    
  4. 提供/注入: createApp 提供了一个新的方法来设置全局的提供者(providers),这在 Vue 2 中通常是通过原型属性或混入来完成的:

    app.provide("someKey", someValue);
    
  5. 与 Vue 2 的差异:

    • 在 Vue 2 中,你通常会使用 new Vue() 来创建和挂载应用。在 Vue 3 中,推荐使用 createApp,因为它提供了更清晰的应用边界和更好的模块化。
    • 使用 createApp,每个应用实例都有其独立的配置,这意味着全局 API 和配置不再是真正的“全局”。这有助于避免不同应用之间或第三方插件与应用之间的潜在冲突。

总的来说,createApp 是 Vue 3 中创建和配置 Vue 应用的主要方法。它提供了一种更模块化、更清晰的方式来初始化应用,使得应用的配置和扩展更加直观和灵活。

4.defineConfig #

defineConfig 是 Vite 提供的一个辅助函数,用于定义 Vite 的配置。虽然它在技术上不是必需的(你可以直接导出一个普通的配置对象),但它提供了几个主要的好处,特别是在使用 TypeScript 时。

以下是关于 defineConfig 的一些关键点:

  1. 类型提示:

    • 当你使用 TypeScript 编写 Vite 配置时,defineConfig 函数可以提供更好的类型推断和自动完成。这使得编写配置更加容易,因为你可以更清楚地知道哪些选项是可用的,以及它们的预期值。
  2. 编辑器支持:

    • 即使在不使用 TypeScript 的情况下,某些编辑器(如 VSCode)也可以利用类型定义来为 JavaScript 提供智能提示。使用 defineConfig 可以增强这种支持。
  3. 清晰的意图:

    • 使用 defineConfig 明确表示这是一个 Vite 配置文件,这可以为阅读代码的人提供更多的上下文。
  4. 使用示例:

    import { defineConfig } from "vite";
    
    export default defineConfig({
      // ... your Vite configuration here ...
    });
    
  5. 不使用 defineConfig 的示例: 如果你选择不使用 defineConfig,你的配置可能会像这样:

    export default {
      // ... your Vite configuration here ...
    };
    

总的来说,defineConfig 是一个为了提高开发者体验而存在的辅助函数。它不会改变配置的行为,但可以提供更好的类型支持和代码清晰度。如果你正在使用 TypeScript 或希望从智能提示中受益,那么使用 defineConfig 是一个好的选择。

5. @vitejs/plugin-vue #

@vitejs/plugin-vue 是一个 Vite 插件,专门用于支持 Vue 单文件组件(SFCs)的处理。当你使用 Vite 构建 Vue 项目时,这个插件是必不可少的,因为它允许 Vite 正确地解析和编译 .vue 文件。

以下是关于 @vitejs/plugin-vue 的一些关键点:

  1. 主要功能:

    • 解析 .vue 文件的三个部分:<template>, <script>, 和 <style>
    • 编译 Vue 模板为可执行的 JavaScript。
    • 使 Vite 能够处理 Vue 的特定特性,如 scoped CSS 和自定义块。
  2. 安装: 你可以使用 npm 或 yarn 安装这个插件:

    npm install --save-dev @vitejs/plugin-vue
    

    yarn add --dev @vitejs/plugin-vue
    
  3. 在 Vite 配置中使用: 一旦安装了插件,你需要在 Vite 的配置文件中导入并使用它:

    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    
    export default defineConfig({
      plugins: [vue()],
    });
    
  4. 与 Vue 2 的兼容性: 如果你正在使用 Vue 2,你需要使用另一个插件:vite-plugin-vue2。但请注意,随着 Vue 3 的普及,大多数新项目和库都在向 Vue 3 迁移。

  5. 扩展配置: @vitejs/plugin-vue 本身还有一些配置选项,如自定义块的处理、模板编译选项等。你可以查阅插件的官方文档以获取更多详细信息。

总的来说,@vitejs/plugin-vue 是 Vite 在处理 Vue 项目时的关键插件。它确保 Vite 能够正确地解析、编译和优化 Vue 单文件组件,从而为开发者提供快速、高效的开发体验。

6. Vue devtools #

Vue Devtools 是一个为 Vue.js 开发的浏览器扩展,它允许开发者更轻松地检查和调试 Vue 应用。Vue Devtools 提供了一系列工具和功能,使得开发者可以深入了解其应用的状态、组件结构、性能等。

以下是 Vue Devtools 的一些主要特点和功能:

  1. 组件树检查:

    • 查看整个 Vue 应用的组件结构。
    • 选择和检查特定的组件,查看其 props、data、computed properties 和 emitted events。
  2. 状态管理:

    • 如果你使用 Vuex 作为状态管理工具,Vue Devtools 会提供一个专门的 Vuex 选项卡,允许你查看 state、mutations、actions 和 getters。
  3. 实时编辑:

    • 直接在 Devtools 中编辑组件的 data 和 props,查看应用如何响应这些更改。
  4. 性能分析:

    • 使用 performance tab 检查组件的渲染性能,找出可能的性能瓶颈。
  5. 事件监听:

    • 查看和过滤组件发出的事件,这对于调试组件间的通信非常有用。
  6. 路由支持:

    • 如果你使用 Vue Router,Devtools 会提供一个专门的路由选项卡,显示当前的路由状态和历史。
  7. Pinpointed Logging:

    • 从控制台直接选择特定的组件或 Vuex mutation,方便地定位日志来源。
  8. 暗黑模式:

    • Vue Devtools 提供了一个暗黑模式,使得在深色主题的开发环境中使用更加舒适。
  9. 支持 Vue 2 和 Vue 3:

    • Vue Devtools 支持 Vue 2 和 Vue 3,确保开发者在任何版本的 Vue 项目中都能获得一致的开发体验。
  10. 安装:

  11. Vue Devtools 可以作为 Chrome、Firefox 和其他浏览器的扩展进行安装。此外,还有一个独立的 Electron 版本,可以用于非 web 场景,如 NativeScript 或其他非浏览器环境。

  12. 开发模式:

  13. 请注意,Vue Devtools 仅在开发模式下工作。为了安全考虑,它在生产模式下是禁用的。

总的来说,Vue Devtools 是每个 Vue 开发者工具箱中的必备工具。它提供了深入了解和调试 Vue 应用的强大功能,从而大大提高了开发效率和应用质量。

7.Composition API #

Vue 3 引入了 Composition API,这是一个新的、可选的方式来组织和复用组件逻辑。与 Vue 2 的 Options API 相比,Composition API 提供了更加灵活的代码组织方式,特别是对于更复杂的组件。

以下是关于 Composition API 的一些关键点和详细解释:

  1. 为什么需要 Composition API:

    • 当 Vue 组件变得复杂时,相关的逻辑可能会分散在 Options API 的多个部分(如 data, methods, computed)。这使得阅读和维护代码变得困难。
    • Composition API 允许你更自然地组合和重用逻辑,而不是基于组件选项。
  2. 基本使用:

    • 使用 Composition API 通常开始于 setup 函数,这是组件内部的入口点。
    • setup 函数中,你可以定义响应式数据、计算属性、方法和其他逻辑。
  3. 响应式数据:

    • 使用 refreactive 创建响应式数据。

      import { ref, reactive } from 'vue';
      
      setup() {
        const count = ref(0);
        const state = reactive({ name: 'Vue', version: 3 });
      
        return { count, state };
      }
      
  4. 计算属性和侦听器:

    • 使用 computedwatch

      import { computed, watch } from 'vue';
      
      setup() {
        const count = ref(0);
        const doubled = computed(() => count.value * 2);
      
        watch(count, (newValue, oldValue) => {
          console.log(`Count changed from ${oldValue} to ${newValue}`);
        });
      
        return { count, doubled };
      }
      
  5. 生命周期钩子:

    • Composition API 提供了与 Options API 中相对应的生命周期钩子,但它们是作为函数导入和使用的。

      import { onMounted } from 'vue';
      
      setup() {
        onMounted(() => {
          console.log('Component is mounted');
        });
      }
      
  6. 逻辑复用和抽象:

    • Composition API 的一个主要优势是能够更容易地复用和抽象逻辑。
    • 你可以将组件逻辑提取到可重用的函数(通常称为“composables”)中,并在多个组件之间共享。
  7. 与 Options API 的共存:

    • 你可以在同一个组件中同时使用 Composition API 和 Options API,但为了保持清晰和一致,建议选择其中一个作为主要的编写方式。
  8. 模板中的使用:

    • 你可以从 setup 函数返回任何你想在模板中使用的值。

总的来说,Composition API 是 Vue 3 的一个重要特性,它为开发者提供了一个更加灵活和模块化的方式来组织和复用组件逻辑。虽然它是可选的,但对于复杂的应用和组件,使用 Composition API 可以带来更好的开发体验。

8.setup #

在 Vue 3 中,setup 是 Composition API 的入口点,它是一个新的组件选项。setup 函数在组件内部的响应式数据、函数和生命周期钩子被初始化之前执行,这使得它成为使用 Composition API 的理想位置。

以下是关于 setup 的一些关键点和详细解释:

  1. 参数:

    • setup 函数可以接受两个参数:propscontext
      • props: 是组件的当前 props。
      • context: 是一个对象,包含以下三个属性:
        • attrs: 一个包含未被 props 捕获的 attribute 的对象。
        • slots: 一个包含组件的 slots 的对象。
        • emit: 一个用于触发事件的函数。
  2. 返回值:

    • 你可以从 setup 函数返回对象,这个对象的属性将会被合并到组件的模板上下文中。这意味着你可以在组件的模板中直接访问这些属性。
  3. 响应式数据:

    • setup 内部,你可以使用 refreactive 来创建响应式数据。
    • 例如:

      import { ref } from 'vue';
      
      setup() {
        const count = ref(0);
        return { count };
      }
      
  4. 方法和计算属性:

    • 你可以在 setup 内部定义方法、计算属性和侦听器。
    • 使用 computedwatch 为计算属性和侦听器。
  5. 生命周期钩子:

    • setup 内部,你可以使用新的生命周期钩子,如 onMounted, onUpdated 等。
    • 这些新的钩子与 Vue 2 中的钩子相对应,但它们是作为函数导入和使用的,而不是作为组件选项。
  6. 与 Options API 的关系:

    • 你可以在同一个组件中同时使用 Options API 和 Composition API,但建议尽量避免这样做,以保持代码的一致性和清晰性。
    • setup 函数在组件的 datamethodscomputed 和其他选项之前执行。
  7. 与模板的交互:

    • 你可以从 setup 返回任何你想在模板中使用的值。这包括响应式数据、方法、计算属性等。

总的来说,setup 是 Vue 3 中引入的新特性,它为开发者提供了一个更加灵活和组合的方式来组织组件的逻辑。通过使用 setup 和其他 Composition API 的功能,你可以更容易地重用和分享代码,同时保持组件的清晰和可维护性。

9.@vue/runtime-core #

@vue/runtime-core 是 Vue 3 的核心运行时包,它包含了 Vue 的核心逻辑,但不包括 DOM 特定的代码。这意味着它不包含用于更新真实 DOM 的代码,而是提供了一种机制,允许你为不同的平台(例如 web、native mobile 或 even 3D engines)创建自定义渲染器。

以下是 @vue/runtime-core 的主要内容和功能:

  1. Virtual DOM:包括虚拟节点的创建、diffing 和 patching 逻辑。

  2. Reactivity System:Vue 3 的响应式系统,包括 ref, reactive, computed, watch 等。

  3. Component Lifecycle:组件的生命周期钩子,如 onMounted, onUpdated 等。

  4. Component Logic:包括 setup 函数、provide/inject 依赖注入机制等。

  5. Custom Renderer API:允许你创建自定义渲染器的 API,如 createRenderer

  6. Utilities:各种内部工具函数和类型。

为什么需要 @vue/runtime-core?

  1. Platform Agnosticism:Vue 的核心逻辑与任何特定平台无关。这意味着你可以为不同的目标平台创建自定义渲染器,例如 web、native mobile、canvas、WebGL 或 even terminal interfaces。

  2. Tree Shaking:由于 Vue 的核心功能被细分为多个模块,所以当你只使用其中的一部分功能时,打包工具可以有效地摇掉未使用的代码,从而减小最终的包大小。

  3. Flexibility:高度模块化的结构使得 Vue 更加灵活。例如,如果你想创建一个自定义渲染器,或者只使用 Vue 的响应式系统而不使用其他功能,这都是可能的。

总结:

@vue/runtime-core 是 Vue 3 架构的核心,它包含了 Vue 的主要逻辑,但不依赖于任何特定平台。这使得 Vue 可以在各种不同的环境中运行,从浏览器到原生应用,甚至是游戏引擎或命令行界面。

10.组合式 API 语法糖 #

在 Vue 3 中,组合式 API 提供了一种更加灵活和组合友好的方式来组织组件的逻辑。而 <script setup> 是组合式 API 的语法糖,它允许你在单文件组件 (SFC) 中以更简洁的方式使用组合式 API。

基本用法

使用 <script setup>,你可以直接在 <script> 标签内部编写 setup 函数的内容,而无需明确定义 setup 函数。

例如,传统的组合式 API 的写法如下:

<script>
import { ref } from "vue";

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

    return {
      count,
      increment,
    };
  },
};
</script>

使用 <script setup>,上述代码可以简化为:

<script setup>
  import {ref} from "vue"; const count = ref(0); const increment = () =>
  count.value++;
</script>

特点和优势

  1. 简洁性:不需要明确地定义 setup 函数,代码更简洁。
  2. 直接返回:在 <script setup> 中,你不需要返回响应式引用或函数,它们会自动可用于模板中。
  3. 类型支持:与普通的 setup 函数一样,<script setup> 也提供了很好的 TypeScript 支持。

使用 definePropsdefineEmits

<script setup> 中,你不能直接使用 propscontext.emit,因为没有 setup 函数的签名。但你可以使用 definePropsdefineEmits 函数来定义它们。

例如:

<script setup>
import { defineProps, defineEmits } from "vue";

const props = defineProps(["message"]);
const emit = defineEmits();

const handleClick = () => {
  emit("clicked", "Hello from child");
};
</script>

<template>
  <button @click="handleClick">{{ message }}</button>
</template>

使用 <script setup><template>ref 语法糖

<script setup> 中,你可以直接使用 ref 关键字在模板中创建一个响应式引用,而无需导入 ref

例如:

<script setup>
let count = ref(0);
const increment = () => count++;
</script>

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

11. 响应式原理 #

Vue 3 的响应式原理与 Vue 2 在本质上有所不同。Vue 2 主要依赖于 ES5 的 Object.defineProperty 来实现响应式,而 Vue 3 则使用了 ES6 的 ProxyReflect。利用 Proxy 可以更为灵活和全面地拦截对象的操作,因此 Vue 3 的响应式更加强大。

下面是 Vue 3 响应式的核心原理简要概述:

  1. Reactive API: 通过 reactive 函数,我们可以使一个对象变得响应式。
const obj = reactive({ count: 0 });
  1. Proxy:

    • 当你访问 obj.count 时,Proxy 允许我们捕捉到这个“get”操作。
    • 当你设置 obj.count = 1 时,Proxy 也可以捕捉到这个“set”操作。
  2. 依赖收集:

    • 当某个响应式对象的属性被“get”时,Vue 会记录这个属性为“正在使用”的属性。这样,当这个属性变化时,Vue 知道需要重新执行与其相关的效果或计算。
    • 为了管理这些“正在使用”的属性,Vue 使用了“依赖收集”的概念。每个属性都可能有一个或多个相关的效果或计算。
  3. Effect 函数:

    • effect 函数是用来包裹那些应该随着响应式数据变化而重新执行的逻辑。
    • 当响应式数据变化时,先前由 effect 包裹的函数会被重新执行。
  4. Computed 属性:

    • Vue 3 提供了 computed API 来创建响应式的计算属性。
    • 它内部也是基于 effect,但与一般的 effect 不同,computed 会缓存其结果,并只在依赖的数据发生变化时重新计算。
  5. Ref API:

    • 对于不想使用完整对象,但仍想保持响应式的原始值,Vue 提供了 ref API。
    • 当你访问一个 ref 的值时,你需要使用 .value,例如 refCount.value
    • ref 内部也是使用 Proxy 实现的。
  6. Watch 和 WatchEffect:

    • Vue 3 提供了 watchwatchEffect 两个 API 来观察响应式数据的变化。
    • watch 需要明确指定观察哪个响应式源,而 watchEffect 会自动追踪它内部使用的所有响应式源。

这只是 Vue 3 响应式系统的一个高度概括。真正的实现涉及到很多细节和优化策略。如果你想深入了解,建议查看 Vue 的源代码或相关的深度分析文章。

11.2 Reflect #

Reflect 是 ES6 中引入的一个内置对象,提供了很多用于拦截 JavaScript 操作的方法。Reflect 不是一个函数对象,所以它不可构造。其主要目的是在某些情况下统一 Object 操作行为,以及在 Proxy handler 中更简洁地表达操作。

以下是关于 Reflect.getReflect.set 的简要说明:

  1. Reflect.get(target, propertyKey[, receiver])

这个方法读取 target 对象的 propertyKey 属性。

参数:

返回值:返回属性的值。

示例:

const obj = {
  foo: 123,
  get bar() {
    return "hello " + this.foo;
  },
};

console.log(Reflect.get(obj, "foo")); // 123
console.log(Reflect.get(obj, "bar")); // "hello 123"
  1. Reflect.set(target, propertyKey, value[, receiver])

这个方法设置 target 对象的 propertyKey 属性的值。

参数:

返回值:如果设置成功,返回 true,否则返回 false

示例:

const obj = {
  foo: 123,
  set bar(value) {
    this.foo = value;
  },
};

console.log(obj.foo); // 123
Reflect.set(obj, "bar", 456);
console.log(obj.foo); // 456

在 Proxy handler 中,Reflect.getReflect.set 很有用,因为它们允许我们轻松地进行默认操作,同时还可以在需要时添加自定义逻辑。

11.3 Proxy #

Proxy 是 ES6(即 ECMAScript 2015)中引入的一个新的内置对象。它用于创建一个对象的代理,从而允许你在对象上的基础操作进行自定义的拦截和行为。

基本上,通过 Proxy,你可以“控制”对象属性的读取、写入、删除等操作。

创建一个 Proxy

基本的 Proxy 创建语法如下:

const proxy = new Proxy(target, handler);

一些基本的拦截操作(traps)

  1. get: 拦截对象属性的读取。
const handler = {
  get(target, key) {
    console.log(`Get on property "${key}"`);
    return target[key];
  },
};

const target = { foo: "bar" };
const proxy = new Proxy(target, handler);

console.log(proxy.foo); // 输出: Get on property "foo"
//      bar
  1. set: 拦截对象属性的设置。
const handler = {
  set(target, key, value) {
    console.log(`Set on property "${key}" to "${value}"`);
    target[key] = value;
    return true; // 表示属性设置成功
  },
};

const target = {};
const proxy = new Proxy(target, handler);

proxy.foo = "bar"; // 输出: Set on property "foo" to "bar"
  1. has: 拦截 in 操作符。
const handler = {
  has(target, key) {
    console.log(`Check if "${key}" exists`);
    return key in target;
  },
};

const target = { foo: "bar" };
const proxy = new Proxy(target, handler);

console.log("foo" in proxy); // 输出: Check if "foo" exists
//      true

这只是 Proxy 的冰山一角。除了上述操作外,Proxy 还支持许多其他的拦截操作,如 deletePropertyownKeysgetOwnPropertyDescriptordefinePropertygetPrototypeOfsetPrototypeOf 等。

使用场景

注意点

虽然 Proxy 提供了强大的拦截能力,但也需要谨慎使用,因为增加了额外的抽象层可能会影响性能。此外,使用 Proxy 的代码可能难以理解和调试,特别是当存在多层代理时。

11.3 reactive #

let activeEffect = null;

class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  notify() {
    this.subscribers.forEach((effect) => effect());
  }
}

function effect(fn) {
  activeEffect = fn;
  fn(); // 运行函数来收集依赖
  activeEffect = null;
}

function ref(rawValue) {
  const dep = new Dep();
  let value = rawValue;

  return {
    get value() {
      dep.depend();
      return value;
    },
    set value(newVal) {
      value = newVal;
      dep.notify();
    },
  };
}

function reactive(target) {
  const depsMap = new Map();

  return new Proxy(target, {
    get(target, key) {
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
      dep.depend();
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      let dep = depsMap.get(key);
      if (dep) {
        dep.notify();
      }
      return true;
    },
  });
}
// 使用 ref
const count = ref(0);
effect(() => {
  console.log(count.value);
});
count.value++; // 会打印 1

// 使用 reactive
const state = reactive({ count: 0 });
effect(() => {
  console.log(state.count);
});
state.count++; // 会打印 1

11.4 Reflect #

  1. 统一性Reflect API 为大部分操作提供了对应的方法,这使得代码更加统一和简洁。当你看到 Reflect.get(...)Reflect.set(...),你可以清晰地知道这是一个对对象属性的操作。

  2. 返回值:与直接的对象属性操作相比,Reflect 的方法返回更多有用的信息。例如,Reflect.set 不仅仅返回 truefalse 来表示操作是否成功,它还可以为某些操作返回其他有意义的结果。

  3. 错误处理:当使用直接的属性操作时,例如设置一个不可写的属性,它会抛出错误。但是,使用 Reflect.set 进行设置时,它只会返回 false,不会抛出错误。这使得错误处理更加灵活。

  4. 边界情况:在某些边界情况下,直接使用对象属性操作可能会出现问题,而 Reflect 的方法则更能确保操作的正确性。例如,当属性名称是一个 Symbol 时,直接使用 target[key] 可能会有问题,而 Reflect.get(target, key) 则可以正确处理。

  5. 未来的 JavaScript 特性兼容性:随着 ECMAScript 标准的进化,某些操作的语义可能会发生改变。使用 Reflect 可以确保 Vue 的响应式系统更好地适应未来的变化。

11.4.1 返回值 #

考虑下面的对象:

const obj = {
  name: "Vue",
};

当你使用普通方式设置属性时:

obj.name = "React"; // 这会返回 "React"

现在,考虑一个不可写的属性:

Object.defineProperty(obj, "name", {
  value: "Vue",
  writable: false,
});

如果你尝试更改该属性:

obj.name = "React"; // 在严格模式下,这会抛出错误

但是,如果你使用 Reflect.set

const result = Reflect.set(obj, "name", "React");
console.log(result); // 这会输出 false,而不是抛出错误

Reflect.set 返回 false 来表示属性没有被成功设置,而不是抛出错误。

对于 Reflect.get,考虑以下的代理:

const objWithDefault = new Proxy(
  {},
  {
    get(target, property) {
      return Reflect.get(target, property) || "default"; // 如果属性不存在,返回 "default"
    },
  }
);

console.log(objWithDefault.name); // 输出 "default"

在这里,我们使用 Reflect.get 来获取属性的值,并在属性不存在时提供默认值。这样的模式对于处理默认值或回退逻辑非常有用。

11.4.2 边界情况 #

当谈到“边界情况”,我们实际上是指某些特定场景或条件下,直接的对象属性操作可能不如使用 Reflect API 那么稳定或直观。

一个经典的边界情况是处理 Symbol 作为属性键。

考虑以下例子:

const sym = Symbol("key");
const obj = {
  [sym]: "value",
};

如果你直接使用 obj[sym] 来获取值,当然也是可以的:

console.log(obj[sym]); // 输出 "value"

但考虑以下的代理:

const proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    if (typeof prop === "string" && prop.startsWith("symbol:")) {
      const realKey = Symbol.for(prop.split(":")[1]);
      return Reflect.get(target, realKey, receiver);
    }
    return Reflect.get(target, prop, receiver);
  },
});

此代理的意图是允许我们使用字符串形式的 "symbol:key" 来访问使用 Symbol 作为键的属性。

在不使用 Reflect 的情况下,处理这种逻辑可能会更加复杂,因为我们必须处理可能的类型转换和符号查找。而使用 Reflect.get 可以简化这个过程,并确保操作的正确性。

这只是处理 Symbol 键时的一个例子。还有其他的边界情况,如处理原型链、处理非对象的目标等,其中 Reflect API 可以提供更加稳定和一致的结果。

11.4.3 receiver #

在使用 Proxy 进行对象拦截时,receivergetset 陷阱函数的第三个参数。它引用了原始的代理对象(或继承代理对象的另一个对象),而不是底层目标对象。receiver 的存在使得我们可以在代理对象上实现一些高级的行为,特别是与原型链和 this 值相关的行为。

以下是一个 receiver 的基本使用场景:

const target = {
  message: "Hello, world!",
  get greeting() {
    return this.message;
  },
};

const handler = {
  get(target, prop, receiver) {
    console.log("Accessed property:", prop);
    return Reflect.get(target, prop, receiver);
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.greeting); // 输出 "Accessed property: greeting" 和 "Hello, world!"

在上面的例子中,当我们通过代理访问 greeting 属性时,get 陷阱会被触发。然后我们使用 Reflect.get 获取该属性的值,其中传递了 receiver 作为其第三个参数。

如果我们没有传递 receiver,则 this 值在 greeting getter 内部将引用底层的 target 对象,而不是 proxy 对象。因此,使用 receiver 确保了正确的 this 绑定。

另一个使用场景是与继承和原型链相关:

const target = {
  value: 42,
};

const handler = {
  get(target, prop, receiver) {
    if (prop === "value") {
      return Reflect.get(target, prop, receiver) * 2;
    }
    return Reflect.get(target, prop, receiver);
  },
};

const proxy = new Proxy(target, handler);

const childObject = Object.create(proxy);
console.log(childObject.value); // 输出 84

在这里,childObject 继承自 proxy。当我们通过 childObject 访问 value 属性时,它实际上在原型链上查找到了 proxy,并触发了 get 陷阱。在这种情况下,receiver 引用的是 childObject,而不是 proxy

通过传递 receiver,我们可以确保即使属性查找来自原型链中的其他对象,代理的行为也是正确的。

总之,receiver 在处理代理和原型链交互、确保正确的 this 绑定等场景中非常有用。

11.4.4 Symbol #

Symbol 是 ES6(即 ECMAScript 2015)中引入的一个新的原始数据类型。与 numberstringboolean 等原始数据类型相比,Symbol 的独特之处在于,每次创建的 Symbol 都是唯一的。这使得 Symbol 成为了创建对象属性的理想选择,尤其是当你想避免与其他属性名冲突时。

以下是关于 Symbol 的一些关键点:

  1. 创建 Symbol: 通过 Symbol() 函数创建一个新的 Symbol。你可以为其提供一个描述,但这只是为了调试的目的,并不影响 Symbol 的唯一性。

    const symbol1 = Symbol();
    const symbol2 = Symbol("description");
    console.log(symbol1 === symbol2); // 输出 false
    
  2. 为对象添加 Symbol 属性: 你可以使用 Symbol 作为对象的属性键。

    const obj = {};
    const key = Symbol("key");
    obj[key] = "value";
    console.log(obj[key]); // 输出 "value"
    
  3. Symbol 作为对象属性的私有性: 由于 Symbol 是唯一的,因此它们不会与其他属性名冲突。此外,常规的对象属性访问和迭代方法(如 Object.keysfor...in 循环)不会返回 Symbol 属性。

    for (let prop in obj) {
      console.log(prop); // 不会输出 Symbol 属性
    }
    console.log(Object.keys(obj)); // 输出 []
    
  4. 访问对象的 Symbol 属性: 使用 Object.getOwnPropertySymbols() 方法可以获取对象的所有 Symbol 属性。

    console.log(Object.getOwnPropertySymbols(obj)); // 输出 [Symbol(key)]
    
  5. 预定义的 Symbol: ES6 提供了一些预定义的 Symbol 值,它们代表了语言内部的特定行为。例如,Symbol.iterator 代表了一个对象的默认迭代器。

    const array = [1, 2, 3];
    const iterator = array[Symbol.iterator]();
    console.log(iterator.next()); // 输出 { value: 1, done: false }
    
  6. Symbol.for() 和 Symbol.keyFor(): 这两个方法允许在全局范围内共享 SymbolSymbol.for() 会检查给定的描述是否已经存在一个相应的 Symbol 值,如果存在,则返回该 Symbol;否则,它将创建一个新的 Symbol

    const globalSymbol = Symbol.for("name");
    const sameGlobalSymbol = Symbol.for("name");
    console.log(globalSymbol === sameGlobalSymbol); // 输出 true
    console.log(Symbol.keyFor(globalSymbol)); // 输出 "name"
    

总的来说,Symbol 提供了一种创建不可变、唯一属性键的方法,从而使得你可以为对象定义独特的、不容易与其他属性冲突的属性。在某些情况下,这可以增加代码的封装性和安全性。