Contents

Vue官方文档

Beautiful landscape

Nature's Beauty

Explore the wonders of the world

参考资料

学习目标

Learning Outcomes

掌握 Vue 3 核心概念(响应式、组件化)
理解选项式 API 和组合式 API 的区别

基础

创建一个应用

应用需要一个 根组件 App.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// App.vue
import { createApp } from "vue";

const app = createApp({
  data() {
    return {
      count: 0,
    };
  },
});

app.mount("#app");

然后通过 createApp 创建一个 应用实例

1
<div id="app"></div>
1
2
3
4
5
import { createApp } from "vue";
import App from "./App.vue";

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

应用实例需要进行挂载才能被渲染
.mount() 将实例挂载在 CSS 选择器字符串 ‘#app’ 对应的 DOM 元素 div 上

可以对应用实例注册组件
注册后,该组件在应用的任何地方都是可用的

1
app.component("TodoDeleteButton", TodoDeleteButton);

createApp 允许在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域

模板语法

Vue 的模板基于 HTML,可以声明式地进行数据绑定和逻辑控制

  • 文本插值

    1
    
    <p>Count: {{ count }}</p>
    

    双大括号内支持 js 表达式(如 {{ count + 1 }}),但不支持语句(如 if 语句)
    当 count 属性的值变化的时候,会同步更新

  • 原始 HTML 绑定

    1
    
    <p v-html="rawHtml"></p>
    

    注意动态渲染 HTML 容易导致 XSS 攻击,需要确保内容可信

  • 属性绑定

    1
    2
    3
    4
    
    <div v-bind:id="dynamicId"></div>
    
    <!-- 或者简写 -->
    <div :id="dynamicId"></div>
    
    • 动态绑定多个属性

      1
      
      <div v-bind="{ id: dynamicId, class: 'active' }"></div>
      

      等价于

      1
      
      <div :id="dynamicId" class="active"></div>
      
  • 绑定 js 表达式
    如果一个 js 表达式可以合法地写在 return 后面,则可以进行数据绑定或者属性绑定

    1
    2
    3
    
    {{ number + 1 }} {{ ok ? 'YES' : 'NO' }}
    
    <div :id="`list-${id}`"></div>
    

    注意如果绑定了一个 js 方法,该方法在组件每次更新时都会被重新调用

  • 指令(Directives)

指令 作用 示例
v-if 条件渲染(销毁/重建 DOM) <p v-if="show">Hello</p>
v-show 切换 display: none <p v-show="show">Hello</p>
v-for 列表渲染 <li v-for="item in items" :key="item.id">{{ item.text }}</li>
v-on 事件绑定 <button @click="handleClick">Click</button>
v-model 表单双向绑定 <input v-model="message">

v-if 适合切换频率低的场景(性能开销大)
v-show 适合频繁切换(初始渲染开销大)

v-for 必须提供唯一 :key,避免渲染错误

绑定多个属性的常见应用

  • 批量设置元素属性

    1
    
    <input v-bind="formInputAttrs" />
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    data() {
      return {
        formInputAttrs: {
          type: 'text',
          placeholder: 'Enter your name',
          class: 'form-control'
        }
      }
    }
    
  • 批量传递组件 props

    1
    
    <child-component v-bind="childProps"></child-component>
    
    1
    2
    3
    4
    5
    6
    7
    8
    
    data() {
      return {
        childProps: {
          title: 'Hello',
          content: 'World'
        }
      }
    }
    

响应式基础

ref

使用 ref() 函数来声明响应式状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { ref } from "vue";

export default {
  setup() {
    const count = ref(0);
    console.log(count); // { value: 0 }
    console.log(count.value); // 0

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

    // 暴露 ref 对象和 increment 函数给模板
    return {
      count,
      increment,
    };
  },
};

注意到 ref 对象含有 value 属性,需要通过 .value 访问
但是在模板中使用时会自动解包,无需 .value

若变量和方法很多,在 setup() 函数中手动暴露会变得非常繁琐
而在单文件组件中使用 <script setup>​ 可以简化这个过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<script setup>
  import { ref } from "vue";

  const count = ref(0);

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

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

推荐使用单文件组件 + <script setup> 的语法

  • 为什么要使用 ref?​
    这与 Vue 的响应式系统的工作方式有关
    当使用 ref 函数声明一个 ref 对象后,该对象的值若发生了改变,Vue 会自动检测到这个变化,并且相应地更新 DOM

    在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的
    在 Vue 中我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作

    .value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改
    在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发

    还可以将 ref 对象传递给函数,同时保留对最新值和响应式连接的访问

  • 深层响应性 ​

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    import { ref } from "vue";
    
    const obj = ref({
      nested: { count: 0 },
      arr: ["foo", "bar"],
    });
    
    function mutateDeeply() {
      obj.value.nested.count++;
      obj.value.arr.push("baz");
    }
    

    ref 使得上述的 obj 具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到
    也可以通过 ShallowRef 来放弃深层响应性,用于优化性能,减少对大型数据的响应性开销

  • DOM 更新时机 ​
    DOM 更新不是同步的,Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次
    常常使用 nextTick() 等待 DOM 更新完成,再执行额外的代码

    1
    2
    3
    4
    5
    6
    7
    
    import { nextTick } from "vue";
    
    async function increment() {
      count.value++;
      await nextTick();
      // 现在 DOM 已经更新了
    }
    
  • 模板中的解包问题
    在模板渲染上下文中,只有顶级的 ref 属性才会被解包

    1
    2
    
    const count = ref(0);
    const object = { id: ref(1) };
    

    上述的 count 和 object 是顶级属性,但 object.id 不是

    1
    2
    3
    4
    5
    
    {{ count + 1 }}
    <!-- 1 -->
    
    {{ object.id + 1 }}
    <!-- [object Object]1 -->
    

    为了解决这个问题,我们可以将 id 解构为一个顶级属性

    1
    
    const { id } = object;
    
    1
    2
    
    {{ id + 1 }}
    <!-- 2 -->
    

计算属性

推荐使用计算属性来描述依赖响应式状态的复杂逻辑
也防止在模板中重复多次相同的计算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
  import { reactive, computed } from "vue";

  const author = reactive({
    name: "John Doe",
    books: [
      "Vue 2 - Advanced Guide",
      "Vue 3 - Basic Guide",
      "Vue 4 - The Mystery",
    ],
  });

  // 一个计算属性 ref
  const publishedBooksMessage = computed(() => {
    return author.books.length > 0 ? "Yes" : "No";
  });
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

computed() 返回值为一个计算属性 ref 对象
Vue 的计算属性会检测到 publishedBooksMessage 依赖于 author.books
所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新

当然我们在表达式中调用类似的函数(判断 author.books 的长度返回布尔值),其结果与计算属性是一模一样的
不同之处在于:计算属性值会基于其响应式依赖被缓存,仅会在其响应式依赖更新时才重新计算
故只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行
而函数调用总是会在 重渲染发生时 再次执行

可以把计算属性的值看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照
更改快照是没有意义的,因此不应该去修改计算属性的返回值

js 的 reverse () 方法就地反转数组中的元素,并返回同一数组的引用,sort 也是同理
如果在计算属性中使用这些方式,记得考虑是否需要创建一个原数组的副本

创建可写计算属性

计算属性默认是只读的,但是可以通过 getter 和 setter 来实现可写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<script setup>
  import { ref, computed } from "vue";

  const firstName = ref("John");
  const lastName = ref("Doe");

  const fullName = computed({
    // getter
    get() {
      return firstName.value + " " + lastName.value;
    },

    // setter
    set(newValue) {
      // 解构赋值语法
      [firstName.value, lastName.value] = newValue.split(" ");
    },
  });
</script>

之后若执行 fullName.value = ‘xxx xxx’ 时,相应的 firstName 和 lastName 都会更新

类与样式绑定

常规写法:

1
2
const isActive = ref(true);
const hasError = ref(false);
1
2
3
4
<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

渲染结果为

1
<div class="static active"></div>

当响应式变量 isActive 或者 hasError 改变时,class 会随之更新
也可以写成对象的形式再绑定在 div 上

1
<div :class="classObject"></div>

还可以通过计算属性来处理逻辑

1
2
3
4
5
6
7
const isActive = ref(true);
const error = ref(null);

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  "text-danger": error.value && error.value.type === "fatal",
}));

样式的绑定也是类似的

1
2
3
<div :style="{ 'color': activeColor, 'font-size': fontSize + 'px' }"></div>

<div :style="styleObject"></div>

条件渲染

1
2
3
4
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>
v-if v-show
指令所在的元素会被销毁与重建 指令所在的元素仍保留在 DOM 中,仅切换了该元素上的 display 属性
可以在 <template> 元素上使用,实现同时切换多个元素 不支持在 <template> 元素上使用
如果开始时条件值为 false,则不会做任何事。只有当条件首次变为 true 时才进行渲染 元素始终会被渲染,只有 CSS display 属性会被切换
切换开销高 初始化渲染开销高

如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适

列表渲染

v-for 遍历列表 items
第二个参数表示当前项的位置索引

1
2
3
<li v-for="(item, index) in items" :key="index">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

v-for 也可以遍历对象

1
2
3
4
5
const myObject = reactive({
  title: "How to do lists in Vue",
  author: "Jane Doe",
  publishedAt: "2016-04-10",
});
1
2
3
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

当列表的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是 就地更新 每个元素,在原本指定的索引位置上渲染元素
通过指定 key,给 Vue 一个提示以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素
key 应该是唯一的

事件处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const name = ref("Vue.js");

function greet(otherMsg, event) {
  alert(`Hello ${name.value}!`);
  alert(otherMsg);
  // `event` 是 DOM 原生事件
  if (event) {
    alert(event.target.tagName);
  }
}
1
2
3
4
5
6
7
<!-- 使用特殊的 $event 变量 -->
<button @click="greet('Form cannot be submitted yet.', $event)">Submit</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => greet('Form cannot be submitted yet.', event)">
  Submit
</button>

将 greet 方法绑定在按钮上,event.target 可以访问到所绑定的 DOM 元素

事件修饰符

  • 监听键盘、鼠标事件

表单输入绑定

利用 v-model 指令简化 事件 的绑定操作
也即双向绑定

1
<input :value="text" @input="event => text = event.target.value" />
1
<input v-model="text" />

v-model.trim="msg" 会自动去除用户输入内容中两端的空格

侦听器

watch 函数会在每次响应式状态发生变化时触发回调函数
第一次参数可以是 ref 对象、getter 函数或者多个来源组成的数组

假设要对下面的响应式对象进行监听

1
const obj = reactive({ count: 0 });
1
2
3
4
// 错误
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`);
});

之所以不行,是因为传给 watch 函数的 obj.count 是个 number,而非响应式对象
考虑使用 getter 函数返回该属性值

1
2
3
4
5
6
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`);
  }
);

如果直接对整个 obj 响应式对象进行监听:

1
2
3
4
5
watch(obj, (newValue, oldValue) => {
  console.log(`Count is: ${count}`);
});

obj.count++;

发现不会有内容打印出来,因为 newValueoldValue 始终是相等的,它们是同一个对象

注意
避免在侦听器中修改监听的数据,可能导致循环更新
不能直接侦听响应式对象的属性值,由上可知,需要使用 getter 函数

  • 深层侦听器
    添加 deep: true 参数

    1
    2
    3
    4
    5
    6
    7
    
    watch(
      () => state.someObject,
      (newValue, oldValue) => {
        // 。。。
      },
      { deep: true }
    );
    

还有 immediate: true 参数,能让侦听器创建时,立即执行一遍回调
(默认是等到监听对象变化时,才会执行回调)

once: true 参数让侦听器只触发一次

模板引用

给一个 DOM 元素添加 ref 属性,可以在该元素被挂载后,获得对它的直接引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script setup>
  import { ref, onMounted } from "vue";

  // 声明一个 ref 来存放该元素的引用
  // 必须和模板里的 ref 同名
  const myInput = ref(null);

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

<template>
  <input ref="myInput" />
</template>

注意只可以在组件挂载后才能访问模板引用,否则获取到的引用为 null

模板引用也可以被用在一个子组件上,父组件对子组件的每一个属性和方法都有完全的访问权
但是多数情况下,应该首先使用标准的 props 和 emit 接口来实现父子组件交互

组件基础

每一个组件都维护着自己的状态。即使是相同的组件,但也是不同的实例

在单文件组件中,推荐使用 PascalCase 形式为子组件命名,以此来和原生的 HTML 元素作区分
若不是单组件,则需要遵从浏览器中 HTML 的解析行为,使用 kebab-case 形式并显式地关闭组件的标签

假设有两个 vue 单文件,blogHomePage.vue 和 BlogPost.vue
其中 blogHomePage.vue 作为父组件调用 BlogPost.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- BlogPost.vue -->
<script setup>
  defineProps(["title"]);
</script>

<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
  </div>
</template>

上述的 defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入
声明的 props 会自动暴露给模板

如果没有使用 <script setup>,props 必须以 props 选项的方式声明

1
2
3
4
5
6
export default {
  props: ["title"],
  setup(props) {
    console.log(props.title);
  },
};

注意:Props 是 单向数据流,子组件不能直接修改

父组件中多次调用子组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- blogHomePage.vue -->
<script setup>
  import { ref } from "vue";

  const posts = ref([
    { id: 1, title: "My journey with Vue" },
    { id: 2, title: "Blogging with Vue" },
    { id: 3, title: "Why Vue is so fun" },
  ]);
</script>

<template>
  <BlogPost v-for="post in posts" :key="post.id" :title="post.title" />
</template>

现在需要让父组件可以控制子组件内容的字体大小
父组件添加一个响应式 ref 对象表示字体大小,并给子组件添加 enlarge-text 事件,以监听子组件上抛的事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- blogHomePage.vue -->
<script setup>
    import { ref } from "vue"

    const posts = ref([
      { id: 1, title: "My journey with Vue" },
      { id: 2, title: "Blogging with Vue" },
      { id: 3, title: "Why Vue is so fun" },
    ])
  +  const postFontSize = ref(1)
</script>

<template>
  +
  <div :style="{ fontSize: postFontSize + 'em' }">
    <BlogPost
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
      +
      @enlarge-text="postFontSize += 0.1"
    />
    +
  </div>
</template>

给子组件添加一个按钮,点击按钮会通知父组件更新 postFontSize
defineEmits 所定义的 emit 发送信号给父组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- BlogPost.vue -->
<script setup>
    defineProps(["title"])
  +  const emit = defineEmits(['enlarge-text'])

  +  emit('enlarge-text')
</script>

<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    + <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

父组件如果要传递内容到子组件中,可以在子组件中利用 <slot> 元素进行占位

常用生命周期

选项式 API 组合式 API 触发时机
created setup() 实例创建后
mounted onMounted DOM 挂载完成
updated onUpdated 数据变更导致 DOM 更新后
unmounted onUnmounted 组件销毁时

深入组件

组件注册

  • 全局注册
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import App from "./App.vue";
import ComponentA from "./ComponentA.vue";
import ComponentB from "./ComponentB.vue";
import ComponentC from "./ComponentC.vue";

const app = createApp(App);
// 链式调用
app
  .component("ComponentA", ComponentA)
  .component("ComponentB", ComponentB)
  .component("ComponentC", ComponentC);

// 挂载
app.mount("#app");
  • 局部注册
1
2
3
4
5
6
7
<script setup>
  import ComponentA from "./ComponentA.vue";
</script>

<template>
  <ComponentA />
</template>

Props

  • 使用一个对象绑定多个 prop

    1
    2
    3
    4
    
    const post = {
      id: 1,
      title: "My Journey with Vue",
    };
    
    1
    2
    3
    4
    5
    
    <BlogPost v-bind="post" />
    
    <!-- 等价于 -->
    
    <BlogPost :id="post.id" :title="post.title" />
    
  • 单向数据流 ​
    props 遵循单向绑定原则
    每次父组件更新后,所有的子组件中的 props 都会跟着更新到最新值
    props 自然地将新的状态向下流往子组件,而不会逆向传递
    避免了子组件意外修改父组件的状态的情况

所有 prop 默认都是可选的,除非声明了 required: true

组件事件

前面说到父组件可以通过 @xxx 监听子组件触发的事件
子组件通过 $emit 触发该事件

$emit 函数中也可以传递参数

1
$emit("foo", 1, 2, 3);
1
2
3
<MyButton @foo="increaseCount" />

function increaseCount(x,y,z) { count.value += x }

vue 父组件和子组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
  <!-- 只有在数据准备好后才渲染子组件 -->
  <modify-vendor-data
    v-if="tobeModifyVendor && modifyBy"
    ref="modifyVendorData"
    :tobe-modify-data.sync="tobeModifyVendor"
    :modify-by.sync="modifyBy"
    @fetch-data="getVendorData"
  />
</template>

<script>
  import ModifyVendorData from "@/components/ModifyVendorData.vue";

  export default {
    components: { ModifyVendorData },
    data() {
      return {
        tobeModifyVendor: null, // 初始占位
        modifyBy: "", // 初始占位
      };
    },
    async created() {
      // 假设这里是异步获取数据的过程
      const vendor = await api.getVendorDetail();
      const user = await api.getCurrentUser();
      this.tobeModifyVendor = vendor;
      this.modifyBy = user.modifyBy;
    },
    methods: {
      getVendorData() {
        // 刷新列表的逻辑
      },
    },
  };
</script>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<template>
  <el-dialog
    title="修改供应商信息"
    :visible.sync="isShowModifyDialog"
    :close-on-click-modal="false"
    top="50px"
  >
    <div v-loading="dialogLoading" element-loading-text="请稍后...">
      <el-form ref="form" :model="localForm" label-width="80px">
        <!-- 省略表单项,保持与你原来一致,只是绑定到 localForm -->
        <el-form-item label="活动名称">
          <el-input v-model="localForm.name"></el-input>
        </el-form-item>
        <!-- 其余表单项同理 -->
        <el-form-item>
          <el-button type="primary" @click="onSubmit">立即修改</el-button>
          <el-button @click="isShowModifyDialog = false">取消</el-button>
        </el-form-item>
      </el-form>
    </div>
  </el-dialog>
</template>

<script>
  import { modifyVendorFileData } from '@/api/pyapi/supplyChain'

  export default {
    name: 'ModifyVendorData',
    props: {
      tobeModifyData: {
        type: Object,
        required: true,
        default: () => ({})
      },
      modifyBy: {
        type: String,
        default: ''
      }
    },
    data() {
      return {
        isShowModifyDialog: false,
        dialogLoading: false,
        // 本地副本,便于编辑后再同步回父组件
        localForm: {}
      }
    },
    watch: {
      // 当父组件传进来的数据变化时,更新本地副本
      tobeModifyData: {
        immediate: true,
        handler(val) {
          this.localForm = { ...val }
        }
      }
    },
    methods: {
      // 父组件可以通过 ref 调用此方法打开弹框
      async showContent() {
        this.isShowModifyDialog = true
      },

      async onSubmit() {
        this.dialogLoading = true
        try {
          const resp = await modifyVendorFileData(this.localForm) // 传递编辑后的数据
          if (resp.code === 200) {
            // 将最新的表单数据同步回父组件
            this.$emit('update:tobeModifyData', { ...this.localForm })
            this.$emit('fetch-data')
            this.$message.success修改成功')
            this.isShowModifyDialog = false
          } else {
            this.$message.warning(`修改失败, ${resp.error || '未知错误'}`)
          }
        } catch (e) {
          console.error(e)
          this.$message.warning(`修改失败, ${e}`)
        } finally {
          this.dialogLoading = false
        }
      }
    }
  }
</script>

<style scoped>
  /* 如有需要自行补充样式 */
</style>
产生 解决思路
tobeModifyDatamodifyBy 初始为 null 父组件在子组件渲染前还在异步获取数据(如接口请求) ① 在子组件使用 v-if 等待数据准备好;② 为 props 设置 默认值;③ 在父组件先给一个空对象/字符串占位。
子组件内部 formData 只读,无法修改后同步回父组件 computed 只实现了 get,没有 set,且 props 本身不可变 computed 添加 set 并在内部 emit 更新;或直接在 data 中维护本地副本。
handleModify 调用 modifyVendorFileData() 时没有传递要修改的内容 API 需要参数,但你只调用了空函数 this.formData(或需要的字段)作为参数传进去。
父组件通过 ref 调用子组件方法时,子组件未在 mounted 前渲染 父组件在 created/mounted 时立即调用 showContent,但子组件可能还未挂载 在父组件中使用 nextTick 或在子组件内部提供 打开对话框的公开方法,并在父组件确认子组件已挂载后再调用。
  1. 本地副本 localForm:避免直接修改 props(只读),编辑时操作副本,提交后通过 update:tobeModifyData 把最新值同步回父组件。
  2. watch + immediate:父组件首次把 tobeModifyData 传进来时立即填充 localForm,解决“初始为 null 报错”。
  3. .sync 用法:在父组件里写 :tobe-modify-data.sync="tobeModifyVendor",子组件内部只要 this.$emit('update:tobeModifyData', newValue) 就能自动更新父组件的变量。
  4. API 调用:把编辑后的表单对象 this.localForm 作为参数传给 modifyVendorFileData,确保后端能收到正确的数据。
  5. 错误提示:统一使用 this.$message,并在 finally 中关闭 loading。

这样写既符合 Vue 官方推荐的单向数据流,也能确保 父子组件数据同步,并且避免了 null 报错。

逻辑复用

组合式函数

为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数,这是复用 无状态逻辑

“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用 有状态逻辑 的函数
例如跟踪当前鼠标在页面中的位置

1
2
3
4
5
6
7
8
// event.js
import { onMounted, onUnmounted } from "vue";

// 添加和清除 DOM 事件监听器
export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback));
  onUnmounted(() => target.removeEventListener(event, callback));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// mouse.js
import { ref } from "vue";
import { useEventListener } from "./event";

// 更新坐标
export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  useEventListener(window, "mousemove", (event) => {
    x.value = event.pageX;
    y.value = event.pageY;
  });

  return { x, y };
}

使用上述封装好的组合式函数
它们一般以 use 开头

1
2
3
4
5
6
7
<script setup>
  import { useMouse } from "./mouse.js";

  const { x, y } = useMouse();
</script>

<template> Mouse position is at: {{ x }}, {{ y }} </template>

在选项式中使用组合式函数,需要在 setup() 钩子中调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useMouse } from "./mouse.js";

export default {
  setup() {
    const { x, y } = useMouse();
    return { x, y };
  },
  mounted() {
    // setup() 暴露的属性可以在通过 `this` 访问到
    console.log(this.x);
  },
  // ...
};

自定义指令

custom directives
下面自定义一个指令:当 Vue 将元素插入到 DOM 中后,会将指定 class 添加到元素中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script setup>
  // 在模板中启用 v-highlight
  const vHighlight = {
    mounted: (el) => {
      el.classList.add("is-highlight");
    },
  };
</script>

<template>
  <p v-highlight>This sentence is important!</p>
</template>

只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令

内置组件

Transition

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<script setup>
  import { ref } from "vue";

  const docState = ref("saved");
</script>

<template>
  <span style="margin-right: 20px">Click to cycle through states:</span>
  <div class="btn-container">
    <Transition name="slide-up" mode="out-in">
      <button v-if="docState === 'saved'" @click="docState = 'edited'">
        Edit
      </button>
      <button v-else-if="docState === 'edited'" @click="docState = 'editing'">
        Save
      </button>
      <button v-else-if="docState === 'editing'" @click="docState = 'saved'">
        Cancel
      </button>
    </Transition>
  </div>
</template>

<style>
  .btn-container {
    display: inline-block;
    position: relative;
    height: 1em;
  }

  button {
    position: absolute;
  }

  .slide-up-enter-active,
  .slide-up-leave-active {
    transition: all 0.25s ease-out;
  }

  .slide-up-enter-from {
    opacity: 0;
    transform: translateY(30px);
  }

  .slide-up-leave-to {
    opacity: 0;
    transform: translateY(-30px);
  }
</style>

一共有 6 个应用于进入与离开过渡效果的 CSS class
上面指定了 name=“slide-up” 作为 Transition 的过渡效果名
指定 mode=“out-in” 可以使得先执行 离开动画,然后在其完成之后再执行元素的 进入动画

KeepAlive

默认情况下,一个组件实例在切换时会被销毁
当这个组件再一次被显示时,会创建一个带有 初始状态的新实例,原有变化的状态会丢失

而 KeepAlive 就是用于在多个组件间动态切换时 对组件实例进行缓存


选项式

创建应用

1
2
3
4
5
import { createApp } from "vue"; // createApp 对象其实是一个组件
import App from "./App.vue"; // 导入单文件形式编写的应用实例

// createApp 用于创建应用实例
const myapp = createApp(App);
  • 挂载应用
    将上述创建的应用 myapp 挂载到 id 为 app 的元素上

    1
    2
    3
    
    <div id="app"></div>;
    
    myapp.mount("#app");
    
  • 多个应用实例
    可以在同一个页面中创建多个共存的应用
    每个应用都拥有自己的 配置以及资源 的作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    const app1 = createApp({
      // ...
    });
    const app2 = createApp({
      // ...
    });
    
    app1.mount("#container1");
    app2.mount("#container2");
    

    当我们想用 Vue 去控制一个大型页面中的特定一小部分时
    可以创建多个小的应用实例,然后分别将它们挂载到所需的元素上去

模板语法

Vue 模板都是语法层面合法的 HTML
Vue 会将模板编译成高度优化的 JS 代码

  • 文本插值
    双括号法(mustache 语法)用于渲染动态文本

    1
    
    <p>信息:{{ msg }}</p>
    

    msg 是相应组件实例中的变量,双大括号会将变量值解释为 纯文本

    双括号中可以编写 js 代码、路径、标识符(给类、方法等起的名字)

  • 插入原始 HTML

    1
    2
    3
    4
    
    <p>
      插入一段 HTML 内容:
      <span v-html="myhtml"></span>
    </p>
    

    上述 v-html 属性被称为一个 指令[^指令]
    v- 的前缀表示它是 Vue 特殊的属性

    需要确保 HTML 内容是安全可信的,才能使用 v-html 属性(避免 XSS 漏洞)

属性绑定

给元素的 attribute 绑定一个动态值

  • 为元素 div 的 id 属性响应式地绑定为 dynamicId

    1
    2
    3
    4
    
    <div v-bind:id="dynamicId"></div>
    
    <!-- 简写 -->
    <div :id="dynamicId"></div>
    

    v-bind:id="null" 给 id 绑定上 null 字符串
    用于移除标签的 id 属性

  • 为 disabled 属性绑定上一个 布尔类型 的变量
    用于控制按钮是否被禁用

    1
    
    <button v-bind:disabled="isBtnDisabled">按钮</button>
    
  • 通过 不带参数的形式,将一个包含多个键值对的 js 对象 stu 绑定到元素 div 上

    1
    
    <div v-bind="stu"></div>
    
    1
    2
    3
    4
    5
    
    const stu = {
      name: "Tom",
      age: "20",
      grade: "grade three",
    };
    
    • 结合 js 表达式绑定数据

      1
      2
      
      {{ number + 1 }} {{ ok ? 'YES' : 'NO' }}
      <div v-bind:id="`list-${id}`"></div>
      

响应式基础(选项式)

  • data 选项
    用来声明组件的响应式状态
    Vue 会在 创建新组件实例 的时候调用此函数
    未准备好的值可以使用 null、undefined 赋值

  • methods 选项
    是包含所有方法的对象
    Vue 自动为 methods 中的方法绑定了 永远指向组件实例的 this

    注意避免使用 箭头函数 定义,因为箭头函数中,无法使用 this 获取组件实例

  • 响应式渲染
    也叫做 声明式渲染,能 在改变时触发更新 的状态被认为是响应式的
    将视图与数据进行绑定,通过改变数据的状态来自动更新视图

计算属性

为了减少模板中的逻辑内容,推荐将逻辑判断写在计算属性中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export default {
  data() {
    return {
      author: {
        name: 'Tom',
        books: [
          'Basic',
          'Mystery'
        ]
      }
    }
  },

  // 计算属性
  computed: {
    BooksMsg() {
      // 返回书籍的数量
      return this.author.books.length
      // 更改 data 中的 books 数组的值后, BooksMsg 的值也会随之改变
    }
  },

  // 也可以将计算属性写为方法
  methods: {
    getBooksMsg() {
      return this.author.books.length
    }
  },
}


// html 文件,模板绑定
// 通过计算属性绑定
<p>{{ BooksMsg }}</p>
// 通过方法绑定
<p>{{ getBooksMsg() }}</p>
  • 计算属性和方法的区别
    计算属性值会 缓存,在其响应式依赖更新时才重新计算
    只要 books 数组的值没有发生变化,计算属性 BookMsg 始终返回先前的计算结果,不会重复执行
    对于需要做很多计算且非常消耗性能的逻辑,推荐使用计算属性
    而方法会在 重渲染 发生时执行

    计算属性默认是 只读
    如果要修改计算属性,则要考虑为计算属性创建 gettersetter 函数

类与样式绑定

v-bind:class

1
2
3
4
5
<div :class="{ active: isActive }"></div>

<!-- isActive 是一个 data 组件中的返回值 -->
<!-- 若 isActive 为真,则 class="active" 的属性就存在,否则不存在 -->
<!-- 即通过绑定一个 isActive 对象来控制 class 是否存在 -->

v-bind:class 可以与一般的 class 属性共存,例如:

1
2
3
4
<div :class="{ active: isActive }" class="static"></div>

<!-- 若 isActive 为真,则有: -->
<div class="static active"></div>

v-bind:style

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
data() {
  return {
    styleObject: {
      color: 'red',
      fontSize: '13px'
    }
  }
}

// 直接绑定一个样式对象
<div :style="styleObject"></div>

条件渲染

v-if

1
2
3
4
<button @click="chinese = !chinese"></button>

<h1 v-if="chinese">你好</h1>
<h1 v-else>hello</h1>

v-else 元素必须跟在 v-if 或者 v-else-if 元素后面
否则不会被识别

如果 v-if 依附的元素不只一个,考虑使用 template 标签

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<button @click="chinese = !chinese">toggle</button>

<template v-if="chinese">
  <h1>你好</h1>
  <h2>世界</h2>
</template>
<template v-else>
  <h1>hello</h1>
  <h2>world</h2>
</template>

v-show 也可以用来控制元素的渲染
区别是:v-show 仅切换了元素的 display css 属性值,并且不能在 template 标签上使用
v-show 更适合元素需要频繁切换的情况

列表渲染

v-for

1
2
3
4
5
6
<li v-for="(item, index) in [{ message: 'Foo' }, { message: 'Bar' }]">
  {{ index }} - {{ item.message }}
</li>
// 第二个参数 index 是位置索引 // 与之类似的 JS 变量作用域 const items = [ {
message: 'Foo' }, { message: 'Bar' } ] items.forEach((item, index) => {
console.log(index, item.message) })

v-for 支持多层嵌套
可以使用 of 替代 in 作为分隔符
同样支持使用 template 标签

v-forv-if 配合使用:

1
2
3
4
<template v-for="todo in todos">
  <!-- 遍历 todos 列表,如果某个事项未完成,则进行展示 -->
  <li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>
  • 唯一 key 标识
    key 标识提示 Vue 跟踪每一个节点,从而重用和重新排序现有的元素
    如果不传入 key,Vue 有默认模式

事件处理

v-on
监听 DOM 事件

1
2
3
4
<button v-on:click=""></button>

// 简写
<button @click=""></button>

接受一个内联 js 语句
或者组件上定义的一个方法

event 是 DOM 原生事件

1
2
3
4
5
// 传入特殊变量 event
warn('warning message', $event)

// 使用箭头函数
(event) => warn('warning message', event)

事件修饰符

  • 事件监听

    • 事件冒泡(Bubbling)
      在 DOM 树中,事件从最具体的元素(事件的实际目标)开始
      然后逐级向上传播到较为不具体的节点(通常是 document 对象)

    • 事件捕获(Capturing)
      与冒泡相反,事件从最不具体的节点(document 对象)开始
      然后逐级向下传播到最具体的节点(事件的实际目标)

    js 默认情况下,事件监听是在冒泡阶段触发的

.stop 阻止冒泡行为。即当前元素事件触发后,阻止事件继续向父元素传播
.prevent 用于阻止事件的默认行为。例如,阻止链接的跳转行为,阻止表单的提交行为等
.self 确保事件是由绑定的元素本身触发的才去执行函数。如果事件是由内部元素冒泡上来的,则会忽略
.capture 将 js 的监听阶段改为捕获阶段,事件从根节点向下传播到目标节点的阶段
.once 确保事件处理函数只执行一次
.passive 滚动行为会立即触发,而不需要等待事件处理函数执行完毕。可以提升移动端的性能

其中的 stop 和 self
stop 是阻止当前元素向外冒泡
self 是当前元素不受影响,但是内部的冒泡行为还会继续向外触发
二者作用的对象不一样

上述修饰符支持链式使用:
.click.prevent.once 表示仅阻止一次行为
但是需要注意调用顺序

  • 应用
    阻止表单的默认提交行为
    使用 .prevent 修饰符可以确保页面不会刷新,内容不会丢失
    然后使用自定义的 addTodo 方法来处理表单提交的内容

    1
    2
    3
    4
    
    <form @submit.prevent="addTodo">
      <input v-model="newTodo" required placeholder="new todo" />
      <button>Add Todo</button>
    </form>
    

按键修饰符

.enter
.tab
.delete(捕获“Delete”和“Backspace”两个按键)
.esc
.space
.up
.down
.left .right
.middle(鼠标按键)

1
// 仅在用户按下 enter 键的时候才进行提交 <input v-on:keyup.enter="submit" />
  • 系统按键
    .ctrl
    .alt
    .shift
    .meta

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 当用户按住 Ctrl 键的同时点击这个按钮时,会触发 doSomething
    <button v-on:click.ctrl="doSomething"></button>
    
    // 在用户松开其他键(比如字母键)但仍然按住 Ctrl 键的时候被触发
    <div v-on:keyup.ctrl="doSomething"></div>
    // 如果想要监听 Ctrl 键被单独松开的事件,keyup.ctrl 无法满足 // 因为
    keyup.ctrl 只有在其他键被松开的同时 Ctrl 键仍然被按住时才会触发 //
    如果单独松开 Ctrl 键,不会触发 keyup.ctrl 事件
    
  • .exact 修饰符

    1
    2
    
    // 精确控制,仅按下 Ctrl 按键的时候才触发
    <button v-on:click.ctrl.exact="doSomething"></button>
    

表单输入绑定

同时使用 v-bindv-on 来在表单的输入元素上创建 双向绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script type="module">
  import { createApp } from "vue";

  createApp({
    data() {
      return {
        text: "",
      };
    },
    methods: {
      onInput(e) {
        // e 即 event,是 DOM 原生事件
        this.text = e.target.value;
      },
    },
  }).mount("#app");
</script>

<div id="app">
  <input :value="text" @input="onInput" placeholder="Type here" />
  <p>{{ text }}</p>
</div>

或者使用 v-model="text"
会自动根据所使用的元素和对应的 DOM 属性和事件结合
免去了麻烦的手动连接值绑定和更改事件监听器

比如对 input 标签进行绑定后,在输入框输入内容,text 值会与之同步

注意,在 input 标签中,v-model 无法监听 拼字阶段 的更新
例如中文日文韩文等等
这时候只能使用 value 绑定和 input 事件监听器

  • 修饰符
    .lazy 不会即时更新,仅在 change 事件后更新数据
    .number 将用户输入内容转换为数字
    .trim 将输入内容两端的空格去除

生命周期钩子

生命周期是指 Vue 实例从创建到销毁的过程

注意避免使用箭头函数定义生命周期钩子
箭头函数中,无法使用 this 获取组件实例

  • 创建阶段

    • created
      在实例创建完成后被立即调用
      此时,实例已完成数据观测、属性和方法的运算
      $el 属性还未显示出来
  • 挂载阶段

    • mounted
      在组件 完成初次渲染并创建 DOM 节点 后调用
      el 被新创建的 vm.$el 替换,并挂到实例上去之后调用该钩子
  • 更新阶段

    • updated
      在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后被调用
  • 销毁阶段

    • beforeDestroy
      在实例销毁之前调用。在这一步,实例仍然完全可用
    • destroyed
      在实例销毁之后调用
      调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁

侦听器

watch 选项
侦听器是一种可以监听数据变化的工具
在每次响应式属性发生变化时,执行一些特定的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
data() {
  return {
    stu: {
      name: 'Tom',
      age: '20',
      grade: 'grade 1'
    }
  }
}

watch: {
  // 仅侦听 stu 属性的 name
  'stu.name'(newValue, oldValue) {
    // ...
  },

  'stu': {
    handler(newValue, oldValue) {
      // ...
    },
    // 深层侦听,可以侦听嵌套属性的变更
    // 需要注意性能开销
    deep: true,

    // watch 默认是懒执行的,只有数据源发生变化时才回调
    // 有时候我们希望创建之时就立即执行一次回调,以获取初始数据
    immediate: true,

    // 侦听只触发一次
    once: true
  }
}
  • 触发时机
    默认情况下,侦听器回调是在
    父组件更新之后;所属组件的 DOM 更新之前 执行的

    flush: ‘post’ 使得回调是在 所属组件的 DOM 更新之后 执行
    flush: ‘sync’ 回调会在 任何更新之前 触发

模板引用

ref 属性访问底层的 DOM 元素
可以用它获取被挂载的实例对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<script>
  export default {
    data() {
      return {
        list: [1, 2, 3, 4, 6, 7, 8, 9],
      };
    },
    mounted() {
      // 获取到 li 标签的数据
      console.log(this.$refs.myitems);
    },
  };
</script>

<template>
  <ul>
    <li v-for="item in list" ref="myitems">{{ item }}</li>
  </ul>
</template>

ul 是 unordered list(无序列表)的缩写
li 是 list item(列表项)的缩写

组件

使用子组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<script>
  // 导入子组件
  import Pagination from "./pagination.vue";

  export default {
    components: {
      // 将子组件注册到模板中
      Pagination,
    },
  };
</script>

<template>
  // 调用子组件,可以重用任意多次
  <Pagination />
</template>

原生 HTML 不区分标签名的大小写
而 Vue 单文件组件可以在编译中区分大小写

组件一般使用 kebab-case 的名字来引用
也可以使用 PascalCase 的方式

传递数据

  • 父传子
    props 选项
    在子组件中通过 props 从父组件接受动态数据
    在父组件中可以通过 v-bind 传入参数,以渲染子组件的内容

    父组件可以通过插槽将模板片段传递给子组件
    子组件使用 <slot> 作为一个占位符
    父组件传入的内容会被渲染在相应位置
    子组件 <slot> 标签中的内容将被当作“默认”内容
    它会在父组件没有传递任何插槽内容时显示

  • 子组件抛出事件给父组件
    emits 选项
    父组件可以使用 v-on 监听子组件上抛的事件,更新对应的值

组件切换

1
<component v-bind:is="组件对象/组件名"></component>