Vue3技术面提升之灵魂拷问(不懂得还是看看吧)

webpack & vite 构建工具对比

1. 构建方式

  • webpack:采用打包的方式,首先将所有的模块和资源静态分析,生成一个或多个包含所有依赖的打包文件。这个过程在开发过程中可能较慢,因为每次代码修改后都需要重新打包。
  • Vite:使用原生的ES模块(ESM),支持热模块替换(HMR),在开发时不进行打包,而是通过服务器直接提供文件,速度更快。只有在生产环境中,Vite才会打包代码(使用rollup进行打包)。因此开发节点优势明显,vite使用的是ES Module,因此在代码中不可使用Commonjs。

2. 启动速度

  • webpack:首次构建较慢,尤其是在项目较大或者依赖较多的时候。增量构建时可能会有所改善,但整体来说启动速度一般。
  • Vite:由于不进行打包(不需要编译,不需要分析模块依赖),启动速度非常快,通常能够在几秒钟内启动开发服务器。(项目越复杂模块越多,vite优势越明显)

3. 热模块替换(HMR)

  • webpack:支持HMR,但在某些情况下(如全局状态变更)可能会导致页面闪烁。(需要把该模块的相关依赖全部编译一遍)
  • Vite:HMR机制非常高效,能够在大部分情况下保持应用状态,更新速度也很快。(改动一个模块仅需要让浏览器重新请求该模块即可)

4. 配置复杂度

  • webpack:配置相对复杂,需要许多插件和loader来处理不同类型的文件。对于新手来说,学习曲线较陡。
  • Vite:配置简单,提高了开发效率,内置了许多常用功能,适合快速开发和原型设计。

5. 插件生态

  • webpack:拥有丰富的插件生态,几乎可以处理任何构建需求,适合大型和复杂项目。
  • Vite:虽然插件生态相对较新,但正在快速发展,支持多种插件来扩展功能。

6. 性能

  • webpack:在生产构建时性能强大,能够进行代码分割、懒加载等优化。
  • Vite:在开发时性能优越,但在某些情况下,生产构建相较于webpack可能稍显不足(不过随着Vite不断更新,这一点在逐渐改善)。

7. 支持的框架 (兼容性)

  • webpack:兼容性好,支持React、Vue、Angular等多种框架。
  • Vite:同样支持现代框架(如Vue 3、React、Svelte等),并提供了以"框架"为中心的优化。

Vue 3 实现了重大的性能提升体现的具体方面

1. 静态提升(Static Tree Hoisting)

html 复制代码
<template>
  <div>
    <h1>Hello, World!</h1> <!-- 静态提升 -->
    <p>{{ message }}</p>
  </div>
</template>

<script>
// `<h1>Hello, World!</h1>` 是一个静态节点,Vue 3 会在编译时识别并提升它,避免在每次渲染时都重新创建
export default {
  data() {
    return {
      message: 'Welcome to Vue 3!'
    };
  }
}
</script>

静态提升是指在编译阶段识别出不变的部分,将其提升为静态节点,从而避免不必要的重渲染。

  • 静态节点:对于那些不会改变的节点,Vue 3 会在编译时将它们提取出来,只在首次渲染时创建,而后续更新时不需要再次处理这些节点。
  • 减少重渲染:通过静态提升,Vue 3 减少了对于静态内容的重复渲染,从而提高了性能,特别是在大型列表和复杂组件中。

2. 预字符串化(Pre-Rendering)

html 复制代码
<template>
  <p>{{ concatenatedMessage }}</p>
</template>

<script setup>
const hello = 'Hello';
const world = 'World';

const concatenatedMessage = `${hello}, ${world}!`; // 预字符串化
</script>

预字符串化是指将模板编译为字符串形式,并在运行时直接使用。

  • 编译优化:在运行时直接使用预编译的字符串,避免了复杂的解析过程。
  • 提高渲染速度:通过减少不必要的编译和解析步骤,提升了渲染速度。

3. 缓存事件处理函数(Cache Event Handler Functions)

html 复制代码
<template>
  <button @click="handleClick">Click me</button>
</template>

<script>
// 在 Vue 3 中,`handleClick` 函数只会创建一次,并被缓存,当组件更新时不会重新创建
export default {
  methods: {
    handleClick() {
      console.log('Button clicked!');
    }
  }
}
</script>

Vue 3 对事件处理函数进行了缓存优化。

  • 函数重用:在组件更新时,重复的事件处理函数不会被重新创建,而是复用先前创建的函数。
  • 提升性能:这样做减少了内存开销和函数创建的次数,提高了性能和效率。

4. 块树(Block Tree)

html 复制代码
<template>
  <div>
    <h1>{{ title }}</h1>
    <p v-for="item in items" :key="item.id">{{ item.text }}</p>
  </div>
</template>

<script>
// `<h1>` 和 `<p>` 组成了不同的块,Vue 3 可以单独处理它们的更新。
// 例如,如果 `title` 修改,只有 `<h1>` 会被重新渲染
export default {
  data() {
    return {
      title: 'Items',
      items: [
        { id: 1, text: 'Item 1' },
        { id: 2, text: 'Item 2' }
      ]
    };
  }
}
</script>

块树是指在虚拟 DOM 中引入的一种新数据结构,用于优化渲染。

  • 组件分块:将组件分为多个块,每个块独立处理自己的更新,提升了更新时的粒度。
  • 控制渲染:通过高效地管理不同块之间的逻辑关系,Vue 3 可以更精准地控制哪些部分需要重新渲染,从而提高整体性能。

5. 补丁标记(Patch Flags)

html 复制代码
<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="changeTitle">Change Title</button>
  </div>
</template>

<script>
// 当 `title` 改变时,补丁标记可以标识出这是一处"文本更新",而不是解析整个组件,这样可以更高效地进行 DOM 更新
export default {
  data() {
    return {
      title: 'Initial Title'
    };
  },
  methods: {
    changeTitle() {
      this.title = 'Updated Title';
    }
  }
}
</script>

补丁标记是一种机制,用于标识虚拟 DOM 更新的类型。

  • 优化更新过程:通过使用补丁标记,Vue 3 可以在 diff 过程中快速识别哪些节点发生了变化,并以最小的成本进行更新。
  • 细粒度更新:补丁标记的引入使得 Vue 3 的更新变得更加细粒度,这样在页面更新时可以避免不必要的完整渲染,进一步提升性能。

为何vue3去掉了构造函数

1. 简化 API 设计

在 Vue 2 中,开发者需要通过 new Vue() 构造函数来创建应用实例,这种方式虽然直观,但在大型项目中可能会导致代码冗余和复杂性增加。Vue 3 引入了 createApp 函数,使得创建应用实例更加简洁和直观

js 复制代码
// Vue 2 即是一个应用又是一个特殊的vue组件 概念模糊
const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue 2!'
  }
});

// Vue 3 把组件实例和应用分开 提供的方式的针对整个应用 而不是一个特殊的组件
import { createApp } from 'vue';

const app = createApp({
  data() {
    return {
      message: 'Hello Vue 3!'
    };
  }
});

app.mount('#app');

2. 提高灵活性

Vue 2 的构造函数方式限制了应用的扩展性。例如,全局配置(如 Vue.useVue.mixin 等)会影响到所有实例,这在某些场景下可能会导致冲突或不可预期的行为。

Vue 3 通过 createApp 创建的应用实例是独立的,每个实例可以有自己的配置,避免了全局污染:

js 复制代码
const app1 = createApp({});
app1.use(plugin1);

const app2 = createApp({});
app2.use(plugin2);

3. Tree Shaking

vue2构造函数集成了太多功能,不利于Tree Shaking,vue3把这些功能使用普通函数导出充分使用Tree Shaking 减少打包体积。

小结

Vue 3 去掉构造函数,改用 createApp 函数,主要是为了:

  • 简化 API 设计,使代码更简洁;
  • 提高应用的灵活性和隔离性;
  • 更好地支持 TypeScript;
  • 符合现代 JavaScript 生态;
  • 优化 Tree Shaking,减小打包体积;
  • 提供更清晰的代码结构。

vue3数据响应式的理解

vue不再不在使用 Object.defineProperty 方式定义完成数据响应式,而是使用Proxy, 除了Proxy本身效率比Object.defineProperty高,由于不用遍历所有属性直接得到一个Proxy,所以vue3中对数据的访问是动态的,访问某个属性时候再动态的获取和设置,极大提升了组件初始阶段的效率问题。同时,Proxy可以监控到成员的新增和删除,并且均可以出发视图重新渲染 这些在vue2`中直接是难以做到的。

1. Proxy 和 Reflect

Vue 3 使用 Proxy 来拦截对象的操作(如读取、写入、删除等),并通过 Reflect 来执行这些操作。Proxy 可以监听对象的所有属性变化,而无需像 Vue 2 那样通过 Object.defineProperty 逐个定义属性的 gettersetter

js 复制代码
const target = { count: 0 };
const handler = {
  get(target, key, receiver) {
    console.log(`读取属性: ${key}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`设置属性: ${key} = ${value}`);
    return Reflect.set(target, key, value, receiver);
  }
};
const proxy = new Proxy(target, handler);

proxy.count; // 输出: 读取属性: count
proxy.count = 1; // 输出: 设置属性: count = 1

2. Reactive 和 Ref

Vue 3 提供了 reactiveref 两个函数来创建响应式数据。

  • reactive:用于将对象转换为响应式对象。
  • ref:用于将基本类型数据(如 numberstring 等)转换为响应式对象,通过 .value 访问和修改值。
js 复制代码
import { reactive, ref } from 'vue';

// 响应式对象
const state = reactive({ count: 0 });
state.count++; // 自动触发更新

// 响应式基本类型
const count = ref(0);
count.value++; // 自动触发更新

3. 依赖收集与触发更新

Vue 3 的响应式系统通过依赖收集和触发更新来实现数据的动态响应。

  • 依赖收集:当组件渲染时,Vue 会追踪哪些响应式数据被使用,并建立依赖关系。
  • 触发更新:当响应式数据发生变化时,Vue 会通知所有依赖该数据的组件进行重新渲染。
js 复制代码
import { reactive, watchEffect } from 'vue';

const state = reactive({ count: 0 });

// 依赖收集
watchEffect(() => {
  console.log(`count 的值是: ${state.count}`);
});

// 触发更新
state.count++; // 输出: count 的值是: 1

4. Computed 和 Watch

Vue 3 提供了 computedwatch 来处理复杂的响应式逻辑。

  • computed:用于创建基于响应式数据的计算属性。
  • watch:用于监听响应式数据的变化并执行回调函数。
js 复制代码
import { reactive, computed, watch } from 'vue';

const state = reactive({ count: 0 });

// 计算属性
const doubleCount = computed(() => state.count * 2);

// 监听器
watch(() => state.count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`);
});

state.count++; // 输出: count 从 0 变为 1

5. 响应式系统的优势

  • 性能更好Proxy 可以监听整个对象,无需像 Object.defineProperty 那样逐个定义属性。
  • 支持更多操作Proxy 可以拦截更多操作,如 deletePropertyhas 等。
  • 更灵活reactiveref 提供了更灵活的方式来创建响应式数据。

vue3中v-model的变化

1. 更灵活的自定义组件支持

在Vue 3中,你可以通过指定不同的属性名和事件来改变v-model的行为。

父组件:

html 复制代码
<template>
  <CustomInput v-model="message" />
</template>

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

export default {
  components: { CustomInput },
  setup() {
    const message = ref('Hello Vue 3');
    return { message };
  }
};
</script>

子组件 (CustomInput.vue):

html 复制代码
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
};
</script>

2. 多值绑定

Vue 3允许在一个组件上使用多个v-model绑定。

父组件:

html 复制代码
<template>
  <UserForm v-model:name="name" v-model:age="age" />
</template>

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

export default {
  components: { UserForm },
  setup() {
    const name = ref('John');
    const age = ref(30);
    return { name, age };
  }
};
</script>

子组件 (UserForm.vue):

html 复制代码
<template>
  <div>
    <input :value="name" @input="$emit('update:name', $event.target.value)" placeholder="Name" />
    <input :value="age" @input="$emit('update:age', $event.target.value)" placeholder="Age" />
  </div>
</template>

<script>
export default {
  props: ['name', 'age'],
  emits: ['update:name', 'update:age']
};
</script>

3. 性能优化

虽然性能优化是内部实现的变化,但你可以在实际项目中感受到更流畅的数据更新。这里不再提供具体代码示例,因为这是框架层面的改进。

4. 移除.sync修饰符

在Vue 3中,.sync修饰符已经被移除。你可以使用命名的v-model来替代它。

旧方式(Vue 2):

html 复制代码
<ChildComponent :count.sync="parentCount" />

新方式(Vue 3):

html 复制代码
<ChildComponent v-model:count="parentCount" />

5. 自定义v-model指令

虽然官方文档中并没有提到通过Vue.directive来定义自己的v-model指令,但在Vue 3中,你可以通过组件选项来定制v-model的行为。

子组件 (CustomCheckbox.vue):

html 复制代码
<template>
  <label>
    <input type="checkbox" :checked="modelValue" @change="$emit('update:modelValue', $event.target.checked)" />
    {{ label }}
  </label>
</template>

<script>
export default {
  props: {
    modelValue: Boolean,
    label: String
  },
  emits: ['update:modelValue']
};
</script>

父组件:

html 复制代码
<template>
  <CustomCheckbox v-model="isChecked" label="Check me!" />
</template>

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

export default {
  components: { CustomCheckbox },
  setup() {
    const isChecked = ref(false);
    return { isChecked };
  }
};
</script>

清醒认识vue3异步组件用法

在 Vue 3 中,可以使用 defineAsyncComponent 来定义异步组件。它接受一个返回 Promise 的工厂函数,通常用于动态导入组件

1. 基本用法

在 Vue 3 中,可以使用 defineAsyncComponent 来定义异步组件。它接受一个返回 Promise 的工厂函数,通常用于动态导入组件。

js 复制代码
import { defineAsyncComponent } from 'vue';

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

export default {
  components: {
    AsyncComponent
  }
};

2. 加载状态和错误处理

defineAsyncComponent 支持配置加载状态和错误处理,通过 loadingComponenterrorComponent 参数来实现。

js 复制代码
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./components/MyComponent.vue'),
  loadingComponent: LoadingSpinner, // 加载时显示的组件
  errorComponent: ErrorComponent,   // 加载失败时显示的组件
  delay: 200,                       // 延迟显示加载组件的时间(毫秒)
  timeout: 3000                     // 超时时间(毫秒)
});

export default {
  components: {
    AsyncComponent
  }
};

3. 结合 Suspense 使用

Vue 3 引入了 Suspense 组件,可以更好地处理异步组件的加载状态。Suspense 提供了两个插槽:#default 用于渲染异步组件,#fallback 用于显示加载状态。

html 复制代码
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue';
import LoadingSpinner from './components/LoadingSpinner.vue';

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

export default {
  components: {
    AsyncComponent,
    LoadingSpinner
  }
};
</script>

4. 动态导入

异步组件通常与动态导入(import())结合使用,以实现按需加载。这种方式可以减小初始包体积,提升应用性能。

js 复制代码
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
);

5. 结合路由使用

在 Vue Router 中,异步组件常用于实现路由懒加载,从而优化应用的加载速度。

js 复制代码
import { defineAsyncComponent } from 'vue';

const routes = [
  {
    path: '/about',
    component: defineAsyncComponent(() =>
      import('./views/AboutView.vue')
    )
  }
];

6. 高级用法:自定义加载逻辑

如果需要更复杂的加载逻辑,可以在 defineAsyncComponent 的工厂函数中实现。

js 复制代码
const AsyncComponent = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(import('./components/MyComponent.vue'));
    }, 1000); // 模拟延迟加载
  });
});

7. 与 Vue 2 的对比

在 Vue 2 中,异步组件通常通过 Vue.componentcomponent 选项来定义,语法较为繁琐:

js 复制代码
// Vue 2
Vue.component('async-component', () => import('./components/MyComponent.vue'));

8. 封装加载异步组件的工具类

js 复制代码
// utils/loadAsync.js
import { defineAsyncComponent } from 'vue';

/**
 * 动态加载异步组件或页面
 * @param {string} path - 组件或页面的路径
 * @param {Object} options - 配置项
 * @param {Component} options.loadingComponent - 加载时显示的组件
 * @param {Component} options.errorComponent - 加载失败时显示的组件
 * @param {number} options.delay - 延迟显示加载组件的时间(毫秒)
 * @param {number} options.timeout - 超时时间(毫秒)
 * @returns {Promise<Component>} - 异步组件或页面
 */
export function loadAsync(path, options = {}) {
  return defineAsyncComponent({
    loader: () => import(/* @vite-ignore */ `@/${path}`),
    loadingComponent: options.loadingComponent || null,
    errorComponent: options.errorComponent || null,
    delay: options.delay || 200,
    timeout: options.timeout || 3000
  });
}

Vue 3 的 defineAsyncComponent 提供了更简洁、更灵活的 API,同时支持加载状态和错误处理,提升了开发体验。如果需要更灵活地处理加载状态,可以结合 Suspense 使用。

composition api 相比 options api有什么优势

  • 更好的逻辑复用和代码组织
  • 更好的类型推导(TS)没有this指向导致的奇怪问题产生 更符合函数式编程规范

vue3 setup作用和原理 以及 scrip setup做了啥事

作用

  1. 组织逻辑setup 函数是 Vue 3 中用于替代 Vue 2 的 datamethodscomputed 等选项的核心函数,它将组件的逻辑集中在一个地方。
  2. 响应式数据 :在 setup 函数中,可以使用 refreactive 等响应式 API 来创建和管理响应式数据。
  3. 生命周期钩子 :可以通过 onMountedonUpdated 等钩子函数来监听组件的生命周期。
  4. 暴露数据和方法setup 函数返回的对象中的属性和方法可以直接在模板中使用。

原理

  1. 执行时机setup 函数在组件实例创建之前执行,因此无法访问 this,因为此时组件实例还未创建。
  2. 响应式系统setup 函数内部使用 Vue 3 的响应式 API(如 refreactive)来创建响应式数据,这些 API 会追踪数据的变化并自动更新视图。
  3. 生命周期钩子setup 函数中可以使用生命周期钩子函数(如 onMountedonUpdated),这些钩子函数会在相应的生命周期阶段被调用。
  4. 返回值setup 函数返回一个对象,该对象中的属性和方法会被暴露给模板和组件的其他部分使用。

<script setup> 的作用

<script setup> 是 Vue 3 提供的一种语法糖,用于简化 setup 函数的使用。它允许开发者直接在 <script> 标签中编写 setup 函数的逻辑,而无需显式定义 setup 函数。

做了什么

  1. 自动暴露变量和方法 :在 <script setup> 中定义的变量和函数会自动暴露给模板,无需通过 return 显式返回。
  2. 简化代码:减少了模板和逻辑之间的代码量,使组件更加简洁。
  3. 支持顶层 await :在 <script setup> 中可以直接使用 await,方便处理异步逻辑。
  4. 更好的 TypeScript 支持<script setup> 对 TypeScript 的支持更加友好,类型推断更加准确。

setup<script setup> 的区别

特性 setup 函数 <script setup>
代码量 需要显式定义 setup 函数并返回对象 无需显式定义 setup 函数,代码更简洁
暴露变量和方法 需要手动 return 自动暴露
顶层 await 不支持 支持
TypeScript 支持 需要额外配置 更友好,类型推断更准确
js 复制代码
export default {
    setup(props,context){
      // ...
    }
}
html 复制代码
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

const count = ref(0);

function increment() {
  count.value++;
}
</script>

介绍 pinia 以及其持久化最佳实践

Pinia 是 Vue 3 的轻量级状态管理库,它提供了两种风格的使用方式:Options API 风格组合式 API 风格

sh 复制代码
# 安装
pnpm add pinia

pinia持久化

1. 安装 Pinia 和持久化插件

sh 复制代码
# 安装
pnpm add pinia pinia-plugin-persistedstate

2. 配置 Pinia 和持久化插件

js 复制代码
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const pinia = createPinia();
// 是一个官方推荐的插件
pinia.use(piniaPluginPersistedstate);

export default pinia;

3. 定义 Store

  • 3.1 state:定义状态

  • 3.2 getters:定义计算属性

  • 3.3 actions:定义同步和异步方法

  • 3.4 persist:配置持久化

js 复制代码
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  // 状态(State)
  state: () => ({
    userInfo: null, // 用户信息
    token: '', // 用户 token
    isLoading: false, // 加载状态
  }),

  // 计算属性(Getters)
  getters: {
    isLoggedIn: (state) => !!state.token, // 是否已登录
    userName: (state) => state.userInfo?.name || 'Guest', // 用户名
  },

  // 方法(Actions)
  actions: {
    // 同步方法:设置用户信息
    setUserInfo(userInfo) {
      this.userInfo = userInfo;
    },

    // 同步方法:设置 token
    setToken(token) {
      this.token = token;
    },

    // 异步方法:模拟登录
    async login(credentials) {
      this.isLoading = true;
      try {
        // 模拟 API 请求
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials),
        });
        const data = await response.json();

        // 更新状态
        this.setUserInfo(data.userInfo);
        this.setToken(data.token);
      } catch (error) {
        console.error('Login failed:', error);
      } finally {
        this.isLoading = false;
      }
    },

    // 异步方法:模拟退出登录
    async logout() {
      this.isLoading = true;
      try {
        // 模拟 API 请求
        await fetch('/api/logout', { method: 'POST' });

        // 清空状态
        this.setUserInfo(null);
        this.setToken('');
      } catch (error) {
        console.error('Logout failed:', error);
      } finally {
        this.isLoading = false;
      }
    },
  },

  // 持久化配置
  persist: {
    enabled: true,
    strategies: [
      {
        key: 'user-store', // 存储的键名
        storage: localStorage, // 存储方式
        paths: ['token', 'userInfo'], // 只持久化 token 和 userInfo
      },
    ],
  },
});

4. 在组件中使用 Store

  • 4.1 使用 stategetters

  • 4.2 调用 actions 方法

  • 4.3 使用辅助函数(如 mapStatemapGettersmapActions

html 复制代码
<template>
  <div>
    <p v-if="isLoading">Loading...</p>
    <div v-else>
      <p>User Name: {{ userName }}</p>
      <p>Logged In: {{ isLoggedIn ? 'Yes' : 'No' }}</p>
      <button @click="handleLogin">Login</button>
      <button @click="handleLogout">Logout</button>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'pinia';
import { useUserStore } from './stores/user';

export default {
  computed: {
    // 使用 mapState 辅助函数映射 state
    ...mapState(useUserStore, ['userInfo', 'token', 'isLoading']),
    // 使用 mapGetters 辅助函数映射 getters
    ...mapGetters(useUserStore, ['isLoggedIn', 'userName']),
  },
  methods: {
    // 使用 mapActions 辅助函数映射 actions
    ...mapActions(useUserStore, ['login', 'logout']),

    // 处理登录
    handleLogin() {
      this.login({ username: 'admin', password: '123456' });
    },

    // 处理退出登录
    handleLogout() {
      this.logout();
    },
  },
};
</script>

基于 组合式 API 风格 的 Pinia 使用示例

1、2步骤和 options api 一致

3. 定义 Store

  • 3.1 state:定义状态

  • 3.2 getters:定义计算属性

  • 3.3 actions:定义同步和异步方法

  • 3.4 persist:配置持久化

js 复制代码
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useUserStore = defineStore('user', () => {
  // 状态(State)
  const userInfo = ref({ name: 'Guest', age: 18 });
  const token = ref('');
  const isLoading = ref(false);

  // 计算属性(Getters)
  const isLoggedIn = computed(() => !!token.value);
  const userName = computed(() => userInfo.value.name || 'Guest');

  // 方法(Actions)
  function setUserInfo(info) {
    userInfo.value = info;
  }

  function setToken(newToken) {
    token.value = newToken;
  }

  async function login(credentials) {
    isLoading.value = true;
    try {
      // 模拟 API 请求
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
      });
      const data = await response.json();

      // 更新状态
      setUserInfo(data.userInfo);
      setToken(data.token);
    } catch (error) {
      console.error('Login failed:', error);
    } finally {
      isLoading.value = false;
    }
  }

  async function logout() {
    isLoading.value = true;
    try {
      // 模拟 API 请求
      await fetch('/api/logout', { method: 'POST' });

      // 清空状态
      setUserInfo({ name: 'Guest', age: 18 });
      setToken('');
    } catch (error) {
      console.error('Logout failed:', error);
    } finally {
      isLoading.value = false;
    }
  }

  return {
    userInfo,
    token,
    isLoading,
    isLoggedIn,
    userName,
    setUserInfo,
    setToken,
    login,
    logout,
  };
}, {
  // 持久化配置
  persist: {
    enabled: true, // 启用持久化
    strategies: [
      {
        key: 'user-store', // 存储的键名
        storage: localStorage, // 存储方式 默认为 localStorage
        paths: ['token', 'userInfo'], // 存储字段 key
      },
    ],
  },
});

4. 在组件中使用 Store

  • 4.1 使用 storeToRefs 解构 stategetters

  • 4.2 调用 actions 方法

  • 4.3 使用 $patch 批量更新状态

html 复制代码
<template>
  <div>
    <p v-if="isLoading">Loading...</p>
    <div v-else>
      <p>User Name: {{ userName }}</p>
      <p>Logged In: {{ isLoggedIn ? 'Yes' : 'No' }}</p>
      <button @click="handleLogin">Login</button>
      <button @click="handleLogout">Logout</button>
      <button @click="updateUserInfo">Update User Info</button>
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from './stores/user';
import { storeToRefs } from 'pinia';

const userStore = useUserStore();
// 使用 storeToRefs 解构 state 和 getters
const { userInfo, token, isLoading, isLoggedIn, userName } = storeToRefs(userStore);
// 直接解构 actions
const { login, logout } = userStore;

// 处理登录
function handleLogin() {
  login({ username: 'admin', password: '123456' });
}

// 处理退出登录
function handleLogout() {
  logout();
}

// 使用 $patch 批量更新状态
function updateUserInfo() {
  userStore.$patch({
    userInfo: { name: 'Admin', age: 30 },
    token: 'new-token',
  });
}
</script>

pinia其他存储方案

手动实现持久化

  1. 保存状态到本地存储 :在 Store 的 actions 或生命周期钩子中,将状态保存到 localStoragesessionStorage
  2. 从本地存储恢复状态 :在 Store 初始化时,从 localStoragesessionStorage 中读取并恢复状态。
js 复制代码
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: JSON.parse(localStorage.getItem('count')) || 0,
  }),
  actions: {
    increment() {
      this.count++;
      this.saveState();
    },
    decrement() {
      this.count--;
      this.saveState();
    },
    saveState() {
      localStorage.setItem('count', JSON.stringify(this.count));
    },
  },
});

使用插件 pinia-plugin-persist

pinia-plugin-persist 是另一个流行的持久化插件,功能与 pinia-plugin-persistedstate 类似。

安装

sh 复制代码
npm install pinia-plugin-persist

配置

在创建 Pinia 实例时,使用该插件。

js 复制代码
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';

const pinia = createPinia();
pinia.use(piniaPluginPersist);

export default pinia;

在 Store 中使用

在定义 Store 时,添加 persist 配置。

js 复制代码
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
  persist: {
    enabled: true, // 启用持久化
    strategies: [
      {
        key: 'counter', // 存储的键名
        storage: localStorage, // 存储方式,默认为 localStorage
      },
    ],
  },
});

4. 持久化方案对比

方案 优点 缺点
手动实现持久化 无需额外依赖,完全可控 代码量较多,容易出错
pinia-plugin-persistedstate 官方推荐,配置简单,功能强大 需要额外安装插件
pinia-plugin-persist 功能丰富,支持多种存储方式 社区维护,可能不如官方插件稳定

ref与reactive的区别源码级别比较

refreactive 是 Vue 3 中用于创建响应式数据的两个核心 API。

ref 的源码实现

  • 核心逻辑

ref 用于将一个基本类型或对象转换为响应式数据。它的核心逻辑是:

  • 如果传入的值是对象,则调用 reactive 将其转换为响应式对象。

  • 如果传入的值是基本类型,则将其包装为一个包含 value 属性的对象。

  • 源码片段

js 复制代码
function ref(value) {
  return createRef(value);
}

function createRef(rawValue, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}

class RefImpl<T> {
  private _value: T;
  private _rawValue: T;

  public readonly __v_isRef = true;

  constructor(value: T, public readonly _shallow: boolean) {
    this._rawValue = _shallow ? value : toRaw(value);
    this._value = _shallow ? value : toReactive(value);
  }

  get value() {
    track(this, TrackOpTypes.GET, 'value');
    return this._value;
  }

  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = this._shallow ? newVal : toReactive(newVal);
      trigger(this, TriggerOpTypes.SET, 'value', newVal);
    }
  }
} 

关键点

  • ref 通过 RefImpl 类实现,内部维护了一个 _value 属性。
  • 访问 value 属性时,会触发依赖收集(track)。
  • 修改 value 属性时,会触发更新(trigger)。
  • 如果传入的值是对象,ref 会调用 toReactive,实际上是调用 reactive

reactive 的源码实现

  • 核心逻辑

reactive 用于将一个对象转换为响应式对象。它的核心逻辑是:

  • 使用 Proxy 代理对象,拦截对对象的访问和修改操作。

  • 通过 tracktrigger 实现依赖收集和更新触发。

  • 源码片段

js 复制代码
function reactive(target) {
  if (target && (target as any).__v_isReadonly) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  );
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    return target;
  }
  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
    return target;
  }
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}

关键点

  • reactive 使用 Proxy 代理对象,拦截对象的 getsetdeleteProperty 等操作。
  • get 操作中,调用 track 进行依赖收集。
  • set 操作中,调用 trigger 触发更新。
  • reactive 只能用于对象,不能用于基本类型。

refreactive 的区别

3.1 数据类型

  • ref:可以用于基本类型(如 numberstring)和对象。
  • reactive:只能用于对象。

3.2 访问方式

  • ref:需要通过 .value 访问或修改值。
  • reactive:直接访问或修改对象的属性。

3.3 实现方式

  • ref:通过 RefImpl 类实现,内部维护一个 _value 属性。
  • reactive:通过 Proxy 实现,直接代理对象。

3.4 性能

  • ref:对于基本类型,性能更高,因为不需要 Proxy 代理。
  • reactive:对于对象,性能与 ref 类似,但更直观。

3.5 使用场景

  • ref:适合用于基本类型或需要明确 .value 访问的场景。
  • reactive:适合用于对象,尤其是嵌套对象。

源码级别比较总结

特性 ref reactive
数据类型 基本类型和对象 对象
访问方式 通过 .value 访问 直接访问属性
实现方式 通过 RefImpl 类实现 通过 Proxy 实现
性能 对于基本类型性能更高 对于对象性能与 ref 类似
使用场景 基本类型或需要 .value 访问的场景 对象,尤其是嵌套对象

keep-alive 最佳实践

  • <keep-alive> 是 Vue 3 中用于缓存组件实例的内置组件,可以提升性能。
  • 适用于动态组件切换、路由切换等场景。
  • 通过 includeexcludemax 属性可以精确控制缓存行为。
  • 结合 activateddeactivated 生命周期钩子,可以更好地管理组件的状态

缓存动态组件

html 复制代码
<template>
  <keep-alive>
    <component :is="currentComponent" />
  </keep-alive>
  <button @click="toggleComponent">Toggle Component</button>
</template>

<script setup>
import { ref } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

const currentComponent = ref('ComponentA');

function toggleComponent() {
  currentComponent.value = currentComponent.value === 'ComponentA' ? 'ComponentB' : 'ComponentA';
}
</script>

缓存路由组件

html 复制代码
<template>
  <keep-alive>
    <router-view />
  </keep-alive>
</template>

注意事项

includeexclude 属性依赖于路由组件的 name 属性,因此需要确保路由组件设置了 name。 对于动态路由(如 /user/:id),默认情况下,不同参数的路由会共享同一个组件实例。如果需要为每个参数单独缓存,可以使用 key 属性。

html 复制代码
<template>
  <keep-alive>
    <router-view :key="$route.fullPath" />
  </keep-alive>
</template>

在嵌套路由中,<keep-alive> 只会缓存当前层级的组件。如果需要缓存嵌套的子组件,可以在子路由的 <router-view> 外层也包裹 <keep-alive>

缓存过多路由组件会导致内存占用过高,应根据实际需求合理使用 <keep-alive>。可以通过 max 属性限制缓存数量

当路由组件被缓存或激活时,会触发 activateddeactivated 生命周期钩子。可以在这些钩子中执行特定逻辑,如数据加载或清理

  • 结合 meta 字段控制缓存
html 复制代码
<template>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" v-if="$route.meta.keepAlive" />
    </keep-alive>
    <component :is="Component" v-if="!$route.meta.keepAlive" />
  </router-view>
</template>
  • 处理滚动位置

在缓存路由组件时,可能需要保留滚动位置。可以通过 scrollBehavior 配置实现。

js 复制代码
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition; // 恢复滚动位置
    } else {
      return { top: 0 }; // 滚动到顶部
    }
  },
});

vuex以及最佳实践以及其持久化方案

Vuex 是 Vue.js 的官方状态管理库,用于管理应用中的共享状态。它通过集中式存储管理应用的所有组件的状态,并以可预测的方式更新状态。

Vuex Store 定义 (vue2版本)

js 复制代码
import Vue from 'vue';
import Vuex from 'vuex';
// 持久化插件
import createPersistedState from 'vuex-persistedstate';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
    user: null,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    setUser(state, user) {
      state.user = user;
    },
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    },
  },
  plugins: [createPersistedState()],
});

export default store;

组件中使用

html 复制代码
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>User: {{ user ? user.name : 'Guest' }}</p>
    <button @click="increment">Increment</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count', 'user']),
  },
  methods: {
    ...mapActions(['incrementAsync']),
    increment() {
      this.$store.commit('increment');
    },
  },
};
</script>

Vue 3 组合式 API 结合 Vuex 的最佳实践以及持久化方案

安装 Vuex

sh 复制代码
pnpm add vuex@next

创建 Vuex Store

js 复制代码
import { createStore } from 'vuex';

const store = createStore({
  state: {
    count: 0,
    user: null,
  },
  mutations: {
    // 同步方法
    increment(state) {
      state.count++;
    },
    setUser(state, user) {
      state.user = user;
    },
  },
  actions: {
    // 异步方法
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    },
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
});

export default store;

在 Vue 应用中使用 Vuex

js 复制代码
// main.js / main.ts
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';

createApp(App).use(store).mount('#app');

组合式 API 结合 Vuex

使用 useStore 访问 Store

在组合式 API 中,可以使用 useStore 函数访问 Vuex Store。

html 复制代码
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
  setup() {
    const store = useStore();

    const count = computed(() => store.state.count);
    const doubleCount = computed(() => store.getters.doubleCount);

    function increment() {
      store.commit('increment');
    }

    function incrementAsync() {
      store.dispatch('incrementAsync');
    }

    return {
      count,
      doubleCount,
      increment,
      incrementAsync,
    };
  },
};
</script

使用 mapStatemapGettersmapActions 辅助函数

在组合式 API 中,可以使用 mapStatemapGettersmapActions 辅助函数简化代码。

html 复制代码
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount']),
  },
  methods: {
    ...mapActions(['incrementAsync']),
    increment() {
      this.$store.commit('increment');
    },
  },
};
</script>

最佳实践

模块化 将 store 分割成多个模块,便于管理和维护。

js 复制代码
const userModule = {
  state: () => ({ user: null }),
  mutations: {
    setUser(state, user) {
      state.user = user;
    },
  },
};

const store = createStore({
  modules: {
    user: userModule,
  },
});

使用 namespaced 模块 为模块启用命名空间,避免命名冲突。

js 复制代码
const userModule = {
  namespaced: true,
  state: () => ({ user: null }),
  mutations: {
    setUser(state, user) {
      state.user = user;
    },
  },
};

避免直接修改 state 始终通过 mutations 修改 state,确保状态变更可追踪。

使用 actions 处理异步操作 将异步逻辑放在 actions 中,保持 mutations 的同步性。

使用 vuex-persistedstate 插件 vuex-persistedstate 是一个流行的 Vuex 持久化插件,可以自动将 state 保存到 localStoragesessionStorage

sh 复制代码
# 安装
pnpm add vuex-persistedstate

配置

js 复制代码
import createPersistedState from 'vuex-persistedstate';

const store = createStore({
  state: { ... },
  mutations: { ... },
  actions: { ... },
  plugins: [createPersistedState()],
});

自定义配置

js 复制代码
createPersistedState({
  key: 'my-app-state', // 存储的键名
  storage: window.sessionStorage, // 存储方式
  paths: ['user', 'cart'], // 只持久化指定模块
});

使用 vuex-persist 插件

vuex-persist 是另一个 Vuex 持久化插件,支持更多存储方式(如 localForage)。

sh 复制代码
# 安装
npm install vuex-persist

配置

js 复制代码
import VuexPersistence from 'vuex-persist';

const vuexLocal = new VuexPersistence({
  storage: window.localStorage,
});

const store = createStore({
  state: { ... },
  mutations: { ... },
  actions: { ... },
  plugins: [vuexLocal.plugin],
});

vuex 小结

  • 组合式 API 提供了更灵活的方式来组织组件逻辑,结合 Vuex 可以更好地管理应用状态。
  • 最佳实践 :模块化、使用辅助函数、避免直接修改 state、处理异步操作。
  • 持久化方案 :使用 vuex-persistedstatevuex-persist 插件,确保状态在页面刷新后能够正确恢复。
相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰7 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen9 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马10 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu10 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss