200字
VUE3基础[精简版]
2023-07-22
2026-04-30

1. vit原理

vite: https://github.com/vitejs/vite

面试题:谈谈你对vite的理解,最好对比webpack说明

webpack 原理图

webpack 原理图

webpack是打包所有模块后生成bundle文件,之后服务器访问bundle文件进行呈现;

这就导致修改某一个模块后需要全部重新打包

vite 原理图

vite 原理图

vite没有打包过程,vite是直接启动一个服务器;

面试题答案:

webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。 而vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。 当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段。另外,由于vite利用的是ES Module,因此在代码中不可以使用CommonJS

2. 效率的提升

客户端渲染效率比vue2提升了1.3~2倍

SSR渲染效率比vue2提升了2~3倍


面试题:vue3的效率提升主要表现在哪些方面?

  1. 静态提升
  2. 预字符串化
  3. 缓存事件处理函数
  4. Block Tree
  5. PatchFlag

2-1. 静态提升

预编译时: 生成render函数时

2-1-1. 静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
<h1>Hello World</h1>
// vue2 的静态节点
render(){
  createVNode("h1", null, "Hello World")
  // ...
}

// vue3 的静态节点
const hoisted = createVNode("h1", null, "Hello World")
function render(){
  // 直接使用 hoisted 即可
}

因为数据一发生改变就会运行render函数;而静态节点是不需要重复编译的;

2-1-2. 静态属性会被提升

<div class="user">
  {{user.name}}
</div>
const hoisted = { class: "user" }

function render(){
  createVNode("div", hoisted, user.name)
  // ...
}

2-2. 预字符串化

在模板编译中

<div class="menu-bar-container">
  <div class="logo">
    <h1>logo</h1>
  </div>
  <ul class="nav">
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
    <li><a href="">menu</a></li>
  </ul>
  <div class="user">
    <span>{{ user.name }}</span>
  </div>
</div>

当编译器遇到大量连续的静态内容,会直接将其编译为一个普通字符串节点

const _hoisted_2 = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>")

根据动静比,智能将大量的连续的静态标签编译为一个不同的字符串节点 (现阶段连续20个静态节点会进行预字符串化)

Vue2虚拟节点树

Vue3虚拟节点树

2-3. 缓存事件处理函数

在模板编译中

<button @click="count++">plus</button>
// vue2
render(ctx){
  return createVNode("button", {
    onClick: function($event){
      ctx.count++;
    }
  })
}

// vue3
render(ctx, _cache){
  return createVNode("button", {
    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))
  })
}

vue3 中认为事件基本是不会变化的,所以加入了缓存处理

2-4. Block Tree

vue2在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

<form>
  <div>
    <label>账号:</label>
    <input v-model="user.loginId" />
  </div>
  <div>
    <label>密码:</label>
    <input v-model="user.loginPwd" />
  </div>
</form>

vue2 再节点对比式再用广度优先进行逐个对比;由于静态节点是不会发生变化的所以这样会浪费大量时间;

vue2

vue3 在预编译时,生成虚拟节点时会标记节点是静态节点、动态节点或哪部分为动态部分(PatchFlag),这样对比时只比较动态部分

Block节点中记录了动态节点,对比时直接遍历即可

左block: [input1, input2] 右block: [input1, input2]

vue3

当树的某一个分支不稳定的时候,会生成一个新的块,即生成一个新的block节点;

2-5. PatchFlag

vue3针对每一个节点对比时进行了优化

vue2在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

<div class="user" data-id="1" title="user name">
  {{user.name}}
</div>

PatchFlag

3. API和数据响应式的变化

面试题1:为什么vue3中去掉了vue构造函数?

面试题2:谈谈你对vue3数据响应式的理解

3-1. 去掉了Vue构造函数

在过去,如果遇到一个页面有多个vue应用时,往往会遇到一些问题

<!-- vue2 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
  Vue.use(...); // 此代码会影响所有的vue应用
  Vue.mixin(...); // 此代码会影响所有的vue应用
  Vue.component(...); // 此代码会影响所有的vue应用
                
  new Vue({
    // 配置
  }).$mount("#app1")
  
  new Vue({
    // 配置
  }).$mount("#app2")
</script>

vue3中,去掉了Vue构造函数,转而使用createApp创建vue应用

<!-- vue3 -->
<div id="app1"></div>
<div id="app2"></div>
<script>  
  createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
  createApp(根组件).mount("#app2")
</script>

更多vue应用的api:https://vuejs.org/api/application.html

3-2. 组件实例中的API

vue3中,组件实例是一个Proxy,它仅提供了下列成员,功能和vue2一样

属性:https://vuejs.org/api/component-instance.html

方法:https://vuejs.org/api/component-instance.html

3-3. 对比数据响应式

vue2和vue3均在相同的生命周期完成数据响应式,但做法不一样

在beforeCreate之后created之前完成数据响应式

数据响应对比

3-4. 面试题参考答案

面试题1:为什么vue3中去掉了vue构造函数?

vue2的全局构造函数带来了诸多问题:
1. 调用构造函数的静态方法会对所有vue应用生效,不利于隔离不同应用
2. vue2的构造函数集成了太多功能,不利于tree shaking,vue3把这些功能使用普通函数导出,能够充分利用tree shaking优化打包体积
3. vue2没有把组件实例和vue应用两个概念区分开,在vue2中,通过new Vue创建的对象,既是一个vue应用,同时又是一个特殊的vue组件。vue3中,把两个概念区别开来,通过createApp创建的对象,是一个vue应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件。

面试题2:谈谈你对vue3数据响应式的理解

vue3不再使用Object.defineProperty的方式定义完成数据响应式,而是使用Proxy。
除了Proxy本身效率比Object.defineProperty更高之外,由于不必递归遍历所有属性,而是直接得到一个Proxy。所以在vue3中,对数据的访问是动态的,当访问某个属性的时候,再动态的获取和设置,这就极大的提升了在组件初始阶段的效率。
同时,由于Proxy可以监控到成员的新增和删除,因此,在vue3中新增成员、删除成员、索引访问等均可以触发重新渲染,而这些在vue2中是难以做到的。

4. 模板中的变化

4-1. v-model

vue2比较让人诟病的一点就是提供了两种双向绑定:v-model.sync,在vue3中,去掉了.sync修饰符,只需要使用v-model进行双向绑定即可。

.async 修饰符详情

为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改

  • 当对自定义组件使用v-model指令时,绑定的属性名由原来的value变为modelValue,事件名由原来的input变为update:modelValue

    <!-- vue2 -->
    <ChildComponent :value="pageTitle" @input="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent v-model="pageTitle" />
    
    <!-- vue3 -->
    <ChildComponent
      :modelValue="pageTitle"
      @update:modelValue="pageTitle = $event"
    />
    <!-- 简写为 -->
    <ChildComponent v-model="pageTitle" />
    
  • 去掉了.sync修饰符,它原本的功能由v-model的参数替代

    <!-- vue2 -->
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent :title.sync="pageTitle" />
    
    <!-- vue3 -->
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent v-model:title="pageTitle" />
    
  • model配置被移除

vue2 时用来修改v-model绑定的的属性名(value)和默认事件名称(input) vue3 可以可以配置多个,所以移除了该配置

  • 允许自定义v-model修饰符

    vue2 无此功能

    v-model

    **注意:**属性名称和方法名称是固定的

    • 默认:modelValuemodelModifiers
    • 自定义:属性名属性名+Modifiers

    使用:直接在props中声明即可

    代码示例:

    export default {
      props: {
        modelValue: Boolean,
        text: String,
        textModifiers: {
          default: () => ({}),
        },
      },
      setup(props, ctx) {
        ...
        const handleTextChange = (e) => {
          let value = e.target.value;
          if (props.textModifiers && props.textModifiers.trim) {
            value = value.trim();
          }
          ctx.emit("update:text", value);
        };
        return {
          ...
          handleTextChange,
        };
      },
    };
    

4-2. v-if v-for

v-if 的优先级 现在高于 v-for

<!-- vue2 -->
<!-- vue2虽然支持但是官方也建议不要这么写,会影响效率 -->
<template v-for="(item,index) in datas" v-if="item.sell">
  <!-- 只有sell为true时才显示 -->
</template>

<!-- 正确使用:使用计算属性处理 datas -->
<template v-for="(item,index) in sells"></template>

4-3. key

  • 当使用<template>进行v-for循环时,需要把key值放到<template>中,而不是它的子元素中

  • 当使用v-if v-else-if v-else分支的时候,不再需要指定key值,因为vue3会自动给予每个分支一个唯一的key

    即便要手工给予key值,也必须给予每个分支唯一的key不能因为要重用分支而给予相同的 key

4-4. Fragment

vue3现在允许组件出现多个根节点

5. 组件变化

Vue 3 迁移指南

以下示例个功能为组件的加载方式(同步&异步)

<!-- home.vue -->
<template>
  <div class="container">
    <div class="block">
      <h2>区域1</h2>
      <p>
        <button @click="modalVisible = true">打开朦层</button>
      </p>
      <Teleport to="body">
        <Modal v-if="modalVisible">
          <button @click="modalVisible = false">关闭朦层</button>
        </Modal>
      </Teleport>
    </div>
    <div class="block mid">
      <h2>区域2</h2>
    </div>
    <div class="block big"><Block3 /></div>
    <div class="block big"><h2>区域4</h2></div>
    <div class="block mid"><Block5 /></div>
    <div class="block"><h2>区域6</h2></div>
  </div>
</template>

<script>
import { ref } from "vue";
import Modal from "../components/Modal.vue";
import { getAsyncComponent } from "../util";
const Block3 = getAsyncComponent("../components/Block3.vue");
const Block5 = getAsyncComponent("../components/Block5.vue");
export default {
  components: {
    Block3,
    Block5,
    Modal,
  },
  setup() {
    const modalVisibleRef = ref(false);
    return {
      modalVisible: modalVisibleRef,
    };
  },
};
</script>

异步加载功能:

注意:以下代码为测试代码

import { defineAsyncComponent, h } from "vue";

// 延时函数
export function delay(duration) {
  if (!duration) {
    duration = random(1000, 5000);
  }
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, duration);
  });
}

// 生成随机数
export function random(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

// 得到一个异步页面
export function getAsyncPage(path) {
  return defineAsyncComponent({
    loader: async () => {
      NProgress.start();
      await delay();
      const comp = await import(path);
      NProgress.done();
      return comp;
    },
    loadingComponent: Loading, // 当promise在pending状态时,将显示这里的组件
  });
}

// 得到一个异步组件
export function getAsyncComponent(path) {
  return defineAsyncComponent({
    loader: async () => {
      await delay();
      if (Math.random() < 0.5) {
        throw new TypeError();
      }
      return import(path);
    },
    loadingComponent: Loading, // 当promise在pending状态时,将显示这里的组件
    errorComponent: {
      render() {
        return h(Error, "出错了!!!");
      },
    },
  });
}

6. ReactivityApi

reactivity api: https://cn.vuejs.org/api/reactivity-utilities.html

vue3把数据响应式给暴露出来了,vue2中写在data中的数据会自动注入变成响应式数据;

import {onMounted, onUnmounted, ref, computed, watchEffect} from "vue"

在上述方法中: onMountedonUnmounted 属于 compositionApi refcomputedwatchEffect 属于 ReactivityApi

6-1. 获取响应式数据

API 传入 返回 备注
reactive plain-object 对象代理 深度代理对象中的所有成员
readonly plain-object or proxy 对象代理 只能读取代理对象中的成员,不可修改
ref any { value: ... } 对value的访问是响应式的
如果给value的值是一个对象,
则会通过reactive函数进行代理
如果已经是代理,则直接使用代理
computed function { value: ... } 当读取value值时,
根据情况决定是否要运行函数

应用:

  • 如果想要让一个对象变为响应式数据,可以使用reactiveref
  • 如果想要让一个对象的所有属性只读,使用readonly
  • 如果想要让一个非对象数据变为响应式数据,使用ref
  • 如果想要根据已知的响应式数据得到一个新的响应式数据,使用computed

6-1-1. 笔试题1:下面的代码输出结果是什么?

import { reactive, readonly, ref, computed } from "vue";

const state = reactive({
  firstName: "Fang",
  lastName: "Bi",
});
const fullName = computed(() => {
  console.log("changed");
  return `${state.lastName}, ${state.firstName}`;
});
console.log("state ready");
console.log("fullname is", fullName.value);
console.log("fullname is", fullName.value);
const imState = readonly(state);
console.log(imState === state);

const stateRef = ref(state);
console.log(stateRef.value === state);

state.firstName = "Star";
state.lastName = "Lucky";

console.log(imState.firstName, imState.lastName);
console.log("fullname is", fullName.value);
console.log("fullname is", fullName.value);

const imState2 = readonly(stateRef);
console.log(imState2.value === stateRef.value);

结果: state ready changed fullname is Bi, Fang fullname is Bi, Fang false true Star Lucky changed fullname is Lucky, Star fullname is Lucky, Star false

6-1-2. 笔试题2:按照下面的要求完成函数

笔试题2:按照下面的要求完成函数

function useUser(){
  // 在这里补全函数
  return {
    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
  }
}

代码示例:

import { reactive, readonly } from "vue";

function useUser(){
  const userOrigin = reactive({});
  const user = readonly(userOrigin);
  const setUserName = (name) => {
    userOrigin.name = name;
  };
  const setUserAge = (age) => {
    userOrigin.age = age
  };
  return {
    user,
    setUserName,
    setUserAge,
  }
}

6-1-3. 笔试题3:按照下面的要求完成函数

function useDebounce(obj, duration){
  // 在这里补全函数
  return {
    value, // 这里是一个只读对象,响应式数据,默认值为参数值
    setValue // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
  }
}

代码示例:

import { reactive, readonly } from "vue";

function useDebounce(obj, duration){
  
  const valueOrigin = reactive(obj);
  const value = readonly(valueOrigin);
  let timer = null;
  const setValue = (newValue) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      Object.entries(newValue).forEach([k, v] => {
        valueOrigin[k] = v;
      })
    }, duration)
  }
  return {
    value, 
    setValue 
  }
}

6-2. 监听数据变化

6-2-1. watchEffect

const stop = watchEffect(() => {
  // 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
})

// 通过调用stop函数,会停止监听
stop(); // 停止监听

6-2-2. watch

// 等效于vue2的$watch

// 监听单个数据的变化
const state = reactive({ count: 0 })
watch(() => state.count, (newValue, oldValue) => {
  // ...
}, options)

const countRef = ref(0);
watch(countRef, (newValue, oldValue) => {
  // ...
}, options)

// 监听多个数据的变化
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
  // ...
});

注意:无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列)

应用:除非遇到下面的场景,否则均建议选择watchEffect

  • 不希望回调函数一开始就执行
  • 数据改变时,需要参考旧值
  • 需要监控一些回调函数中不会用到的数据(检测新旧数据是否发生改变)

笔试题: 下面的代码输出结果是什么?

import { reactive, watchEffect, watch } from "vue";
const state = reactive({
  count: 0,
});
watchEffect(() => {
  console.log("watchEffect", state.count);
});
watch(
  () => state.count,
  (count, oldCount) => {
    console.log("watch", count, oldCount);
  }
);
console.log("start");
setTimeout(() => {
  console.log("time out");
  state.count++;
  state.count++;
});
state.count++;
state.count++;

console.log("end");

结果: watchEffect 0 start end watchEffect 2 watch 2 0 time out watchEffect 4 watch 4 2

6-3. 判断

API 含义
isProxy 判断某个数据是否是由reactivereadonly
isReactive 判断某个数据是否是通过reactive创建的
详细:https://v3.vuejs.org/api/basic-reactivity.html#isreactive
isReadonly 判断某个数据是否是通过readonly创建的
isRef 判断某个数据是否是一个ref对象

6-4. 转换

6-4-1. unref

等同于:isRef(val) ? val.value : val

应用:

function useNewTodo(todos){
  todos = unref(todos);
  // ...
}

6-4-2. toRef

得到一个响应式对象某个属性的ref格式

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo'); // fooRef: {value: ...}

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

6-4-2. toRefs

把一个响应式对象的所有属性转换为ref格式,然后包装到一个plain-object中返回

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs: not a proxy
{
  foo: { value: ... },
  bar: { value: ... }
}
*/

应用:

setup(){
  const state1 = reactive({a:1, b:2});
  const state2 = reactive({c:3, d:4});
  return {
    ...state1, // lost reactivity
    ...state2 // lost reactivity
  }
}

setup(){
  const state1 = reactive({a:1, b:2});
  const state2 = reactive({c:3, d:4});
  return {
    ...toRefs(state1), // reactivity
    ...toRefs(state2) // reactivity
  }
}

// composition function
function usePos(){
  const pos = reactive({x:0, y:0});
  return pos;
}

setup(){
  const {x, y} = usePos(); // lost reactivity
  const {x, y} = toRefs(usePos()); // reactivity
}

6-5. 降低心智负担

所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactivereadonly直接产生的数据

function usePos(){
  const pos = reactive({ x:0, y:0 });
  return toRefs(pos); //  {x: refObj, y: refObj}
}
function useBooks(){
  const books = ref([]);
  return {
    books // books is refObj
  }
}
function useLoginUser(){
  const user = readonly({
    isLogin: false,
    loginId: null
  });
  return toRefs(user); // { isLogin: refObj, loginId: refObj }  all ref is readonly
}

setup(){
  // 在setup函数中,尽量保证解构、展开出来的所有响应式数据均是ref
  return {
    ...usePos(),
    ...useBooks(),
    ...useLoginUser()
  }
}

7. Composition Api

不同于reactivity api,composition api提供的函数很多是与组件深度绑定的,不能脱离组件而存在。

7-1. setup

// component
export default {
  setup(props, context){
    // 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数,this指向undefined;
    // 该函数只运行一次
    // props 是一个对象,包含了所有的组件属性值
    // context 是一个对象,提供了组件所需的上下文信息
  }
}

context对象的成员

成员 类型 说明
attrs 对象 vue2this.$attrs
slots 对象 vue2this.$slots
emit 方法 vue2this.$emit

7-2. 生命周期函数

vue2 option api vue3 option api vue 3 composition api
beforeCreate beforeCreate 不再需要,代码可直接置于setup中
created created 不再需要,代码可直接置于setup中
beforeMount beforeMount onBeforeMount
mounted mounted onMounted
beforeUpdate beforeUpdate onBeforeUpdate
updated updated onUpdated
beforeDestroy ==改== beforeUnmount onBeforeUnmount
destroyed ==改==unmounted onUnmounted
errorCaptured errorCaptured onErrorCaptured
- ==新==renderTracked onRenderTracked
- ==新==renderTriggered onRenderTriggered

新增钩子函数说明:

钩子函数 参数 执行时机
renderTracked DebuggerEvent 渲染vdom收集到的每一次依赖时
renderTriggered DebuggerEvent 某个依赖变化导致组件重新渲染时

DebuggerEvent:

  • target: 跟踪或触发渲染的对象
  • key: 跟踪或触发渲染的属性
  • type: 跟踪或触发渲染的方式

面试题: composition api相比于option api有哪些优势?

从两个方面回答:

  1. 为了更好的逻辑复用和代码组织
  2. 更好的类型推导
有了composition api,配合reactivity api,可以在组件内部进行更加细粒度的控制,使得组件中不同的功能高度聚合,提升了代码的可维护性。对于不同组件的相同功能,也能够更好的复用。
相比于option api,composition api中没有了指向奇怪的this,所有的api变得更加函数式,这有利于和类型推断系统比如TS深度配合。

8. 共享数据

8-1. vuex方案

在vue2中不适合中小型项目, vue3中多了些新的选择,但是vuex还是比较复杂些,不适合中小型项目

安装vuex@4.x

两个重要变动:

  • 去掉了构造函数Vuex,而使用createStore创建仓库
  • 为了配合composition api,新增useStore函数获得仓库对象

代码示例[store]:

// index.js
import loginUser from "./loginUser";
import { createStore, createLogger } from "vuex";
export default createStore({
  modules: {
    loginUser,
  },
  plugins: [createLogger()],  //插件:日志
});

// loginUser.js
export default {
  namespaced: true, //开启命名空间
  state: {
    user: null,
    loading: false,
  },
  mutations: {
    ...
    setLoading(state, payload) {
      state.loading = payload;
    }
  },
  actions: {
    async login({ commit }, { loginId, loginPwd }) {
      ...
      commit("setLoading", false);
      ...
    }
  },
};
<!-- xxx.vue -->
<script>
...
import { useStore } from "vuex";  //vue3 vuex 使用
import { useRouter } from "vue-router"; //vue3 vueRouter 使用
export default {
  setup() {
    ...
    const store = useStore();
    const router = useRouter();

    const handleSubmit = async () => {
      const user = await store.dispatch("loginUser/login", {
        ...
      });
      ...
      router.push("/"); //路由跳转
    };
    return {
      ...
    };
  },
};
</script>

8-2. global state

由于vue3的响应式系统本身可以脱离组件而存在,因此可以充分利用这一点,轻松制造多个全局响应式数据

global state

代码示例:

// store/useLoginUser 提供当前登录用户的共享数据
// 以下代码仅供参考
import { reactive, readonly } from "vue";
import * as userServ from "../api/user"; // 导入api模块
// 创建默认的全局单例响应式数据,仅供该模块内部使用
const state = reactive({ user: null, loading: false });
// 对外暴露的数据是只读的,不能直接修改
// 也可以进一步使用toRefs进行封装,从而避免解构或展开后响应式丢失
export const loginUserStore = readonly(state);

// 登录
export async function login(loginId, loginPwd) {
  state.loading = true;
  const user = await userServ.login(loginId, loginPwd);
  state.user = user;
  state.loading = false;
}
// 退出
export async function loginOut() {
  state.loading = true;
  await userServ.loginOut();
  state.loading = false;
  state.user = null;
}
// 恢复登录状态
export async function whoAmI() {
  state.loading = true;
  const user = await userServ.whoAmI();
  state.loading = false;
  state.user = user;
}
<script>
import { login, loginUserStore } from "../store/useLoginUser";

...
</script>

8-3. Provide&Inject

vue2中,提供了provideinject配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用

Provide&Inject

除了兼容vue2的配置式注入,vue3composition api中添加了provideinject方法,可以在setup函数中注入和使用数据

Provide&Inject

考虑到有些数据需要在整个vue应用中使用,vue3还在应用实例中加入了provide方法,用于提供整个应用的共享数据

creaetApp(App)
  .provide("foo", ref(1))
  .provide("bar", ref(2))
  .mount("#app");

Provide&Inject

因此,我们可以利用这一点,在整个vue应用中提供共享数据

示例代码功能:在一个项目中存在多个vue实例;不同实例使用不同的共享数据

// store/useLoginUser 提供当前登录用户的共享数据
// 以下代码仅供参考
import { readonly, reactive, inject } from "vue";
const key = Symbol(); // Provide的key

// 在传入的vue应用实例中提供数据
export function provideStore(app) {
  // 创建默认的响应式数据
  const state = reactive({ user: null, loading: false });
  // 登录
  async function login(loginId, loginPwd) {
    state.loading = true;
    const user = await userServ.login(loginId, loginPwd);
    state.loginUser = user;
    state.loading = false;
  }
  // 退出
  async function loginOut() {
    state.loading = true;
    await userServ.loginOut();
    state.loading = false;
    state.loginUser = null;
  }
  // 恢复登录状态
  async function whoAmI() {
    state.loading = true;
    const user = await userServ.whoAmI();
    state.loading = false;
    state.loginUser = user;
  }
  // 提供全局数据
  app.provide(key, {
    state: readonly(state), // 对外只读
    login,
    loginOut,
    whoAmI,
  });
}

export function useStore(defaultValue = null) {
  return inject(key, defaultValue);
}

// store/index
// 应用所有store
import { provideStore as provideLoginUserStore } from "./useLoginUser";
// 继续导入其他共享数据模块...
// import { provideStore as provideNewsStore } from "./useNews"

// 提供统一的数据注入接口
export default function provideStore(app) {
  provideLoginUserStore(app);
  // 继续注入其他共享数据
  // provideNewsStore(app);
}

// main.js
import { createApp } from "vue";
import provideStore from "./store";
const app = createApp(App);
provideStore(app);
app.mount("#app");

8-4. 对比

vuex global state Provide&Inject
组件数据共享
可否脱离组件
调试工具
状态树 自行决定 自行决定
量级

9. script setup

script setup官方文档

script setup 是一个语法糖,本身并没有带来任何新功能

轮播图部分代码示例:

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

export default {
  props: {
    imgs: {
      type: Array,
      default: () => [],
    },
  },
  emits: ['change'],  //配置用于声明组件可以触发的自定义事件[非必选项]
  setup(props, { emit, expose }) {
    const index = ref(0);

    function switchTo(i) {
      if (i < 0) {
        i = 0;
      } else if (i > props.imgs.length - 1) {
        i = props.imgs.length - 1;
      }
      if (i === index.value) {
        return;
      }
      index.value = i;
      emit('change', i);
    }
    // 设置需要暴露的方法
    expose({
      switchTo,
    });
    return { index, switchTo };
  },
};
</script>

setup语法糖示例:

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

const props = defineProps({
  imgs: {
    type: Array,
    default: () => [],
  },
});
const emit = defineEmits(['change']);
defineExpose({
  switchTo,
});
const index = ref(0);
function switchTo(i) {
  if (i < 0) {
    i = 0;
  } else if (i > props.imgs.length - 1) {
    i = props.imgs.length - 1;
  }
  if (i === index.value) {
    return;
  }
  index.value = i;
  emit('change', i);
}
</script>

区别:

传统方式(setup函数)中返回的所有方法和属性都会附着在组件实例上;父组件可以通过ref获取到组件实例进行调用方法和修改属性;可以通过expose配置需要暴露的方法和属性,传递空则不会暴露任何方法;

script setup:组建中的方法和实例是不会附着在组件自身上的,可以通过defineExpose配置需要暴露的方法和属性,无该配置则不会暴露任何属性和方法

评论