跳至主要內容

Vue

大约 40 分钟约 12020 字

为什么选择Vue & 什么是Vue

Vue是一款渐进式JavaScript框架,易学易用,性能出色,生态丰富。

它基于HTML、CSS、JavaScript构建,并提供了多种声明式、组合式的编程模型,帮助我们高效地开发用户界面。

本笔记主要记录Vue3。

Vue的官网:https://cn.vuejs.orgopen in new window

在网页中嵌入Vue

Vue能够直接嵌入到网页中,像使用JQuery一样,但是比JQuery操作DOM的能力更强。

下面通过两种API风格演示如何在网页中嵌入Vue。有关API风格,请向下翻阅,这里只需要懂基本原理即可。

示例:

<!-- 选项式API -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
   <style>
   </style>
</head>
<body>
<div id="app">  <!-- 为Vue控制的区域创建一个选择器 -->
   <span>{{ count }}</span>
   <button @click="increment">添加</button>
</div>
<script type="module">  // 在这里导入Vue,添加type="module",这样浏览器就会将js文件当做模块来处理
import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';  // 导入Vue

createApp({  // 创建一个Vue实例
   data() {  // 在这里定义数据
      return {
         count: 0  // 初始化数据
      }
   },
   methods: {  // 方法定义
      increment: function () {
         this.count++;
      }
   },
   mounted: function () {  // 元素被挂载到页面上后执行
      console.log('Vue组件已被创建。');
   }
}).mount('#app');  // 在这里用mount方法渲染,将Vue组件挂载到id为app的元素上
</script>
</body>
</html>
<!-- 组合式API -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
    </style>
</head>
<body>
<div id="app">  <!-- 为Vue控制的区域创建一个选择器 -->
    <span>{{ count }}</span>
    <button @click="increment">添加</button>
</div>
<script type="module">  // 在这里导入Vue,添加type="module",这样浏览器就会将js文件当做模块来处理
import {createApp, ref, onMounted} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';

createApp({  // 创建一个Vue实例
    setup() {
        const count = ref(0);  // 创建一个响应式变量
        function increment() {
            count.value++;
        }
        onMounted(() => {  // 相当于选项式API的mounted函数
            console.log('Vue组件已经被挂载')
        })
        return {
            count, increment  //在这里返回要在页面中出现的方法和变量
        }
    }
}).mount('#app');  // 在这里用mount方法渲染,将Vue组件挂载到id为app的元素上
</script>
</body>
</html>

创建Vue项目

按顺序依次执行以下命令即可。

cd your_base_project_dir
npm init vue@latest

输入项目名称后,剩下的配置自己选择,这里全选【否】。

image-20240302173504346
image-20240302173504346
cd your_project_dir
npm install

安装完成后可以输入以下命令预览生成的默认页面。

npm run dev

若npm原registry缓慢,可以执行npm config set registry http://registry.npm.taobao.org/切换到国内镜像源。

Vue项目的文件夹结构

.vscode                # VSCode配置文件夹
  |-- extensions.json

public                 # 公共资源文件夹
  |-- favicon.ico

src                    # 源代码文件夹
  |-- assets           # 资源文件夹
  |   |-- base.css
  |   |-- logo.svg
  |-- components       # 组件文件夹
  |   |-- icons
  |       |-- ...
  |   |-- ...
  |-- App.vue          # 全局应用文件
  |-- main.js          # 创建Vue项目

index.html
.gitignore

第一个Vue文件

上述文件中,删除src/components和src/assets文件夹下的所有文件,保留一个HelloWorld.vue。将HelloWorld.vue的文件内容改成与下面一致。

<template>

</template>
<script>

</script>

然后在App.vue中删掉导入之前已删除的文件的代码,改成和下面一致。

App.vue实际上是一个组件,它一般用于统一引入其他组件,而App这个组件本身是被main.js所使用的,具体知识请查阅【组件】一节。

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <HelloWorld />
</template>

还有main.js,最好检查一下是否有其他导入没有删除。

接下来在HelloWorld.vue中开始写代码。

<script setup>
</script>

<template>
  <p>Hello World!</p>
</template>

执行如下命令,在localhost:5173预览界面。

npm run dev

运行结果为:Hello World!

Vue的API分为两种:选项式和组合式,具体看下面的两种代码风格。

API风格

选项式API

它使用含多个选项的对象描述组件的路基,如data、methods、mounted。它的定义方式是卸载export default上。选项定义的属性都会暴露在函数内部的this上,它会指向当前组件的实例。

<Script>
export default {
    data() {
        return {
            count: 0
        }
    },
    methods: {
        increment() {
            this.count++
        }
    },
    mounted() {
        console.log(`The initial count is ${this.count}`)
    }
}
// 所有的特性都写在这里面,这就是选项式API
</Script>
<template>
  <button @click="increment">
      Count is {{ count }}
    </button>
</template>

组合式API

它可以使用导入的API函数描述组件逻辑。

<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
function increment() {
    count.value++
}
onMounted(()=>{
 	console.log(`The initial count is ${count.value}`)    
})
    // 使用导入函数实现组件逻辑
</script>
<template>
  <button @onclick='increment'>
    Count is {{ count }}   
  </button>
</template>

选择API风格

两种API风格都能覆盖大部分的应用场景,它们之时同一个系统底层提供两套的不同接口。选项式API是在组合式API的基础上实现的,关于Vue的基础概念和知识在它们之间都是通用的。在生产项目中,若我们不打算使用构建工具,或在低复杂度的场景使用Vue,推荐使用选项式API。若使用Vue构建完整的单页应用,则可以采用组合式API。

关于导入的知识

在Vue或者任何使用ES6模块导入(import)语法的JavaScript项目中,大括号的使用与否取决于导入的成员类型。主要有两种导入方式:默认导入(Default Imports)和命名导入(Named Imports)。

  1. 默认导入(Default Import): 默认导入是指模块导出时使用export default语法导出的成员。默认导出每个模块只能有一个,而且导入时不使用大括号。例如,如果一个模块使用export default导出了一个函数或一个对象,那么在导入这个默认导出的成员时,你可以给它起任何名字,并且不需要大括号。

    // 假设有个模块defaultExport.js
    export default function() { ... }
    
    // 导入时
    import anyName from './defaultExport.js';
    

    这里的anyName可以是任何有效的变量名,它会被赋予defaultExport.js模块默认导出的成员。

  2. 命名导入(Named Imports): 命名导入用于导入模块中通过export(而非export default)导出的成员。模块可以导出多个命名成员,导入时需要使用大括号{}来指定想要导入的成员名。这要求导入时的名称与模块内部导出的名称完全匹配。

    // 假设有个模块namedExports.js
    export const name1 = ...;
    export function name2() { ... }
    
    // 导入时
    import { name1, name2 } from './namedExports.js';
    

    在这个例子中,name1name2必须与namedExports.js中导出的名称完全相同。

总结:当你看到导入语句使用大括号时,它表示这是一个或多个命名导入的情形,必须与导出时的名称完全匹配。如果没有大括号,那么它是一个默认导入,表示导入的是模块的默认导出成员,可以自由命名。在Vue3中,如refonMounted等都是Vue提供的API,需要通过命名导入的方式从vue包中导入,因为它们是作为命名导出提供的。

模板语法

文本

数据绑定最常见的形式就是使用“Mustache” (双大括号) 语法的文本插值,和Jinja2的语法相同。

<span>Message: {{ msg }}</span>  <!--`{{ msg }}`就是文本插值,类似于Python的Jinja2-->

在data()中可以定义变量,然后就可以使用文本插值在文档中渲染了。

export default {
  name: 'HelloWorld',
  data(){
    return{
      msg: "消息提示"  //这里的msg是一个变量
    }
  }
}

在上方介绍了一个Vue组件包含的基本标签,包含style、template、script三个标签,有时script会包含一个setup属性,这是组合式API,使用时要注意。

原始 HTML

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,我们需要使用v-html 指令。

<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>  <!--在v-html内部使用一个变量名称来让这个值渲染成html-->
data(){
    return{
        rawHtml:"<a href='https://www.itbaizhan.com'>百战</a>"
    }
}

带有v-html的标签内部不能有元素。如下方的语法是错误的。

<template>
<p v-html="html">123</p>
</template>

<script>
export default {
 data() {
  return {
   html: '<a href="https://google.com">超链接</a>'
  }
 }
}
</script>

属性 Attribute

Mustache 语法不能在 HTML 属性中使用,然而,可以使用 v-bind 指令,让属性也随变量动态地变化。

<div v-bind:id="dynamicId"></div>  <!--所有标签的属性若要动态的使用双花括号语法表示,就要使用v-bind动态绑定-->
data(){
    return{
        dynamicId:1001
    }
}

温馨提示

v-bind: 可以简写成 :,如:

<template>
  <p :id="id">你好</p>
</template>

<script>
export default {
  data() {
    return {
      id: 'id-attribute'
    }
  }
}
</script>

由于通过Class属性操作元素的方式非常常见,因此Vue给v-bind做了一个增强使用。(更详细的语法请参考https://cn.vuejs.org/guide/essentials/class-and-style.html#binding-html-classes)open in new window

我们可以给:class提供一个对象,类似于:

<div :class="{ active: isActive }"></div>

上面的语法表示 active 是否存在取决于数据属性 isActive 的真假值。

:class也可以与一般的class属性值共存,如:

data() {
  return {
    isActive: true,
    hasError: false
  }
}

配合下面的模板语法:

<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

渲染出的将是<div class="static active"></div>

:class也可以绑定一个数组来渲染多个class,但这种方式有些冗余。

使用 JavaScript 表达式

在我们的模板中,我们一直都只绑定简单的 property 键值,Vue.js 提供了完全的 JavaScript 表达式支持,文本插值语法可以计算一个正确的单表达式并渲染出来,但不能使用多个逻辑。

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

// 它们只能是单个表达式,类似于Jinja2

这些表达式会在当前活动实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。

<!--  这是语句,不是表达式:-->
{{ var a = 1 }}

<!-- 流程控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}

例:

<template>
  <p>number+1={{number+1}}</p>
  <p>str's words: {{str.split(' ')}}</p>
  <p>bool's value is {{ bool ? 'TRUE' : 'FALSE'}}</p>
</template>

<script>
export default {
  data() {
    return {
      number: 1,
      str: 'This is a String.',
      bool: true
    }
  }
}
</script>

条件渲染

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true 值的时候被渲染。

<p v-if="flag">我是孙猴子</p>  <!--会被渲染-->
data() {
    return {
        flag: true
    }
}

v-else

我们可以使用 v-else 指令来表示 v-if 的“else 块”。

<p v-if="flag">我是孙猴子</p>
<p v-else>你是傻猴子</p>    <!--会被渲染-->
data() {
    return {
        flag: false
    }
}

v-show

另一个用于条件性展示元素的选项是 v-show 指令,但它的本质是设置display属性,而不是选择是否渲染。

<h1 v-show="ok">Hello!</h1>  <!--当ok的值为真时它的属性display为block,否则为none,和v-if的区别是它已被渲染-->

v-if vs v-show 的区别

v-if 是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好

列表渲染

v-for 把一个数组映射为一组元素

我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名

<ul>
    <li v-for="item in items">{{ item.message }}</li>  <!--类似于Js的for in 语法-->
</ul>
data() {
    return {
        items: [{ message: 'Foo' }, { message: 'Bar' }]
    }
}

in关键字可以使用of关键字替代,效果是一样的。

维护状态

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一的 key attribute:

<div v-for="(item,index) in items" :key="item.id|index">
  <!-- 内容 -->
</div>

事件处理

监听事件

我们可以使用 v-on 指令 (通常缩写为 @ 符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。用法为 v-on:click="methodName" 或使用快捷方式 @click="methodName"

<button @click="counter += 1">Add 1</button>
data() {
    return {
        counter: 0
    }
}

如:

<template>
  <button @click="onclick">+1</button>
  <p>{{ number }}</p>
</template>

<script>
export default {
  data() {
    return {
      number: 0
    }
  },
  methods: {
    onclick() {
      this.number += 1
    }
  }
}
</script>

事件处理方法

然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on 指令中是不可行的。因此 v-on 还可以接收一个需要调用的方法名称。

<button @click="greet">Greet</button>  <!--若事件处理方法只有一个参数,而@click没有参数,则这个参数代表事件对象-->
methods: {
    greet(event) {
        // `event` 是原生 DOM event
        if (event) {
            alert(event.target.tagName)
        }
    }
}

内联处理器中的方法

这是官方的翻译称呼,其实我们可以直接叫他 "事件传递参数"。

<button @click="say('hi')">Say hi</button>
<button @click="say('what')">Say what</button>  <!--若显式传递参数了则函数参数就是普通参数-->
methods: {
    say(message) {
        alert(message)
    }
}

就是给方法传了个参数。

表单输入绑定

你可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖。它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。

<input v-model="message" placeholder="edit me" /> <!--该语法的意思是此input与变量message绑定了,它们的值始终一致-->
<p>Message is: {{ message }}</p>
data() {
    return {
        message:""
    }
}

如下面用一个简单的滑块改变数字显示:

<template>
  <input type="range" v-model="number">
  <p>{{ number }}</p>
</template>

<script>
export default {
  data() {
    return {
      number: 0
    }
  }
}
</script>

修饰符

.lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 。我们可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步。

<input v-model.lazy="message" />  <!--这种情况下只有每次此input失去焦点后message才会与其同步-->
<p>Message is: {{ message }}</p>
data() {
    return {
        message:""
    }
}

.trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符。

<input v-model.trim="message" />  <!--去除首尾空格-->
data() {
    return {
        message:""
    }
}

模板引用

为了更方便地操作DOM元素,Vue为元素提供了一个特殊的属性:ref。它的属性值是一个对象,此对象要在组件的script中声明。

<template>
  <input type="text" ref="input" @blur="print"/>
</template>
<script setup>
import {ref} from 'vue';
const input = ref(null);
function print() {
  console.log("输入框的内容为:" + input.value.value)
}
</script>
<template>
  <input type="text" ref="input" @blur="print"/>
</template>
<script>
export default {
  methods: {
    print: function () {
      console.log("输入框的内容为:" + this.$refs.input.value)
    }
  }
}
</script>

注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 $refs.input,在初次渲染时会是 undefined。这是因为在初次渲染前这个元素还不存在呢!

在v-for使用ref时,对象将会是一个数组。

除了和一个对象绑定,它还可以绑定一个函数,这个函数可以有一个参数,代表这个元素本身。这个函数将在组件更新后被调用。

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

若ref属性被应用在一个组件上,则对应的引用对象将是这个组件本身。

组件基础

单文件组件

Vue 单文件组件(又名 *.vue 文件,缩写为 SFC)是一种特殊的文件格式,它允许将 Vue 组件的模板、逻辑 样式封装在单个文件中。

<template>
    <h3>单文件组件</h3>
</template>

<script>
export default {
    name:"MyComponent"
}
</script>

<style scoped>
h3{
    color: red;
}
</style>

style标签添加scoped属性指此文件内的CSS样式只在此组件内生效,因此它里面的选择器可以和其它组件内的选择器重复。

加载组件

第一步:引入组件 import MyComponentVue from './components/MyComponent.vue'

第二步:挂载组件 ,在export default中填写components: { MyComponentVue }

第三步:显示组件,在template中使用 <my-componentVue />

例:在components文件夹下创建MyComponent1.vue和MyComponent2.vue两个文件,然后更改App.vue内容:

<script setup>
import MyComponent1 from './components/MyComponent1.vue'
</script>

<template>
  <MyComponent1 />
</template>

MyComponent1.vue:

<script>
import MyComponent2 from "@/components/MyComponent2.vue";  // 导入子组件,组件名称一般与文件名相同
export default {
  components: {
    MyComponent2  // 子组件名称,与上方导入的一致
  },
  data() {
    return {
     msg: '这是自定义组件MyComponent1的msg内容'
    }
  }
}
</script>

<template>
<MyComponent2 />  // 然后就可以在这里使用components变量声明过的组建了
<p>{{ msg }}</p>
</template>

<style scoped>

</style>

MyComponent2.vue:

<script>
export default {
  data() {
    return {
      msg: `这个是MyComponent2组件内的msg变量`
    }
  },
}
</script>

<template>
  <h2>这个是MyComponent2组件内的内容</h2>
  {{ msg }}
</template>

<style scoped>
h2 {
  color: red;
  font-size: 20px;
}
</style>
<!--此组件由于没有引入任何组件,因此就像普通组件那样写就可以-->

原理是在App.vue中指注册MyComponent1组件,然后在MyComponent1组件中注册MyComponent2组件,就达到了组件嵌套的效果。

组件的具体位置中,可以使用类似于大写字母开头的组件名称,如MyComponent,也可以使用连字符形式,如my-component。

组件交互

使用props属性进行组件交互

组件与组件之间是需要存在交互的,否则完全没关系,组件的意义就很小了。

我们可以在porps中定义可能由其他组件传递过来的变量列表,可以是一个数组,也可以是一个列表。

<my-componentVue title="标题"/>
<template>
    <h3>单文件组件</h3>
    <p>{{ title }}</p>
</template>

<script>
export default {
    name:"MyComponent",
    props:{
        title:{
            type:String,
            default:""
        },
        //第二种写法:props: ['title', 'subtitle', ...]
    }
}
</script>

如:

还是上面的例子,MyComponent1.vue:

<script>
import MyComponent2 from "@/components/MyComponent2.vue";
export default {
  components: {
    MyComponent2
  }
}
</script>

<template>
  <h1>MyComponent1的h1标签</h1>
  <MyComponent2 msg="组件传递信息" />
</template>

<style scoped>
</style>

MyComponent2.vue:

<script>
export default {
  props: ['msg']
}
</script>

<template>
  <h1>MyComponent2的h1标签</h1>
  <p>{{ msg }}</p>
</template>

<style scoped>
</style>

原理是在MyComponent1中在使用MyComponent2时传递了一个参数msg,然后在MyComponent2中使用props引出这个变量,最后输出就可以。

温馨提示:组件传递信息时,信息只能从父级组件传递到子级组件,反向传递的方法在下面有介绍。

若使用组合式API,则props需要单独声明。

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

若没有<script setup>,props需要作为setup的参数。

export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

Prop 类型

Prop传递参数其实是没有类型限制的,可以传递数组、字符串、数字、对象。

Prop常用的可传递的参数类型都是JavaScript的内置类型,有:Number、String、Array、Object等。

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function
}

例(MyComponent2.vue):

<script>
export default {
  props: {
    msg: {
      type: String,
      default: "默认值"
    }
  }
}
</script>

<template>
  <h1>MyComponent2的h1标签</h1>
  <p>{{msg}}</p>
</template>

<style scoped>
</style>

上方msg设置了类型和默认值,对于类型,若其他地方使用了这个组件时传递了错误的参数类型,控制台会警告但不会报错;对于默认值,若组件在引用时没有传递这个参数,那么这个参数在被渲染时会显示默认值。

温馨提示:

  1. 传递对象时,参数需要使用v-bind类型,如MyComponent1.vue:
<script>
import MyComponent2 from "@/components/MyComponent2.vue";
export default {
  components: {
    MyComponent2
  },
  data(){
    return {
      info: {
        id: 1,
        name: '张三'  // 这里使用对象类型
      }
    }
  }
}
</script>

<template>
  <MyComponent2 :info=info />  <!--这里使用v-bind类型-->
</template>

<style scoped>
</style>

MyComponent2.vue:

<script>
export default {
  props: ['info']
}
</script>

<template>
  <p>{{ info.id }}</p>
  <p>{{ info.name }}</p>
</template>

<style scoped>
</style>
  1. 数据类型为数组或者对象的时候,默认值需要返回工厂模式,如:
<script>
    export default {
        props: {
            info : {
                type: Object,
                default: () => {
                    return {
                        id: 1,
                        name: '张三'  //默认值使用工厂模式,使用匿名函数
                    }
                }
            }
        }
    }
</script>

<template>
<p>{{ info.id }}</p>
<p>{{ info.name }}</p>
</template>

<style scoped>
</style>

自定义事件组件交互

上述使用props方法在组件间传递数据时数据只能由父组件传递至子组件,不能反向传递。若要实现反向传递,有一种方法就是利用自定义组件交互。具体实现方法就是在子组件中为一个元素(如按钮)添加事件监听,监听后触发的函数内部调用this.$emit函数。第一个参数为父组件使用子组件时使用v-on绑定的自定义名称,第二个参数为函数参数。在父组件中,实现了子组件触发了事件监听的函数后,在调用子组件时,用v-on将子组件中用this.$emit函数的第一个参数定义的自定义名称传递到子组件,当子组件触发了事件监听时,emit就会执行父组件的函数。

<template>
    <h3>单文件组件</h3>
    <button @click="sendHandle">发送数据</button>  <!--这里写函数名称,函数内部要有this.$emit-->
</template>

<script>
export default {
    name: "MyComponent",
    methods:{
        sendHandle(){
            this.$emit("onCustom","数据")  //这里调用$emit
        }
    }
}
</script>

<style scoped>
h3 {
    color: red;
}
</style>
<template>
  <my-componentVue @onCustom="getData" />  <!--这里使用子组件$emit调用时传递的第一个参数,值写要调用的函数-->
</template>

<script>

import MyComponentVue from './components/MyComponent.vue'

export default {
  name: 'App',
  components: {
    MyComponentVue
  },
  methods: {
    getData(data) {
      console.log(data);
    }
  }
}
</script>

props只读

上方演示了在不同组件之间传递变量,这些变量实际上值都是不能改变的,即在一个组件传入另一个组件的值是不能改变的,如下方的语法是错误的。

<!--MyComponent1-->
<template>
  <MyComponent2 msg='msg'/>
</template>
<script>
import MyComponent2 from '@/components/MyComponent2.vue'
export default {
  components: {
    MyComponent2
  },
  data(){
    return {
      msg: '值1'
    }
  }
}
</script>
<!--MyComponent2-->
<template>
  <button @click='clickMethod'>点击更改msg的值</button>
  <p>msg = {{ msg }}</p>
</template>
<script>
export default {
    props: ['msg'],
    methods: {
        clickMethod(){
            this.msg = '值2' //错误的语法
        }
    }
}
</script>

Prop组件与v-model一起使用

子组件内,我们可以用v-model与一个变量绑定(在data中声明),然后在watch中添加一个和这个变量同名的方法,如下。

<!--MyComponent2-->
<template>
  <input v-model="number" type="range">
</template>
<script>
export default {
  data() {
    return {
      number: 0
    }
  },
  watch: {
    number(newValue, oldValue) {
      this.$emit('emitEvent', newValue);
    }
  }
}
</script>

父组件内,定义一个用于显示的元素,并实现一个设置此元素的方法。使用上面的方法导入并显示这个组件后,传递一个v-on参数,参数的名称是子组件内要改变的那个值,值写设置父组件值的函数。

<!--MyComponent1-->
<template>
  <MyComponent2 @emitEvent="setData"/>
  <p>msg = {{ data }}</p>
</template>
<script>
import MyComponent2 from '@/components/MyComponent2.vue'

export default {
  components: {
    MyComponent2
  },
  data() {
    return {
      data: 1
    }
  },
  methods: {
    setData(value) {
      this.data = value
    }
  }
}
</script>

这样子组件的变量会改变,并在父组件中显示。

组件生命周期

每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

为了方便记忆,我们可以将他们分类:

创建时:beforeCreatecreated

渲染时:beforeMountmounted

更新时:beforeUpdateupdated

卸载时:beforeUnmountunmounted

image-20240302175001596
image-20240302175001596

生命周期函数是和data()函数绑定在一起的,这八个函数要写在它里面。

比如mounted函数可以存放与网络请求有关的代码,unmounted可以存放与销毁组件有关的代码,可以销毁吃性能的组件。

插槽Slots

插槽是一个可以向组件中添加HTML标签的语法,具体看下面的例子。

在子组件中,添加一个没有任何文本的空slot标签,在父组件引用子组件时,使用闭合标签,里面添加的HTML标签,会替换子组件的slot标签。

<!--MyComponent1-->
<template>
  <MyComponent2>
    <h3>在MyComponent1中添加的h3</h3>
  </MyComponent2>
</template>
<script>
import MyComponent2 from '@/components/MyComponent2.vue'
export default {
  components: {
    MyComponent2
  }
}
</script>
<!--MyComponent2-->
<template>
  <h3>MyComponent2</h3>
  <slot></slot>
</template>
<script>
export default {
}
</script>

Vue引入第三方——以Swiper轮播图为例

Vue之所以是一个强大的Web框架,原因之一就是它有着强大的第三方插件支持。这里以Swiper为例,介绍一下如何安装第三方插件以及如何使用它们。

Swiper 开源、免费、强大的触摸滑动插件。

Swiper 是纯javascript打造的滑动特效插件,面向手机、平板电脑等移动终端。

Swiper 能实现触屏焦点图、触屏Tab切换、触屏轮播图切换等常用效果。

温馨提示

由于Swiper也是一个框架,它也会不断地更新。有时官方更新之后较以前的版本有较大的改动,有许多基于以前的版本实现的代码就需要重构,因此安装最新版本时不是最佳选择,这时我们就可以选择安装指定版本。

官方文档:https://swiperjs.com/vueopen in new window

安装指定版本: npm install --save [email protected]

基础实现

<template>
  <div class="hello">
    <swiper class="mySwiper">  <!--使用组件-->
      <swiper-slide>Slide 1</swiper-slide>
      <swiper-slide>Slide 2</swiper-slide>
      <swiper-slide>Slide 3</swiper-slide>
    </swiper>
  </div>
</template>

<script>
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';  // 导入CSS,不导入则无法显示样式

export default {
  name: 'HelloWorld',
  components: {
    Swiper,  // 设置组件
    SwiperSlide,
  }
}
</script>

为轮播图添加指示器

<template>
  <div class="hello">
    <swiper class="mySwiper" :modules="modules" :pagination="{ clickable: true }">  <!--这里也需要做一个改动-->
      <swiper-slide>
        <img src="../assets/logo.png" alt="">
      </swiper-slide>
      <swiper-slide>
        <img src="../assets/logo.png" alt="">
      </swiper-slide>
      <swiper-slide>
        <img src="../assets/logo.png" alt="">
      </swiper-slide>
    </swiper>
  </div>
</template>

<script>
import { Pagination } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/pagination';  //导入CSS

export default {
  name: 'HelloWorld',
  data(){
    return{
      modules: [ Pagination ]  // 这里加一个modules
    }
  },
  components: {
    Swiper,
    SwiperSlide,
  }
}
</script>

Axios网络请求

Axios 是一个基于 promise 的网络请求库

安装

Axios的应用是需要单独安装的 npm install --save axios

引入

组件中引入: import axios from "axios"

全局引用:

import axios from "axios"

const app = createApp(App);
app.config.globalProperties.$axios = axios
app.mount('#app')

// 在组件中调用
this.$axios

网络请求基本示例

get请求

axios({
    method: "get",
    url: "http://iwenwiki.com/api/blueberrypai/getChengpinDetails.php"
}).then(res => {
    console.log(res.data);
})

post请求

温馨提示

post请求参数是需要额外处理的

  1. 安装依赖: npm install --save querystring
  2. 转换参数格式: qs.stringify({})
axios({
    method:"post",
    url:"http://iwenwiki.com/api/blueberrypai/login.php",
    data:qs.stringify({
        user_id:"[email protected]",
        password:"iwen123",
        verification_code:"crfvw"
    })
}).then(res =>{
    console.log(res.data);
})

快捷方案

get请求

axios.get("http://iwenwiki.com/api/blueberrypai/getChengpinDetails.php")
    .then(res =>{
      console.log(res.data);
    })

post请求

axios.post("http://iwenwiki.com/api/blueberrypai/login.php", qs.stringify({
      user_id: "[email protected]",
      password: "iwen123",
      verification_code: "crfvw"
    }))
      .then(res => {
        console.log(res.data);
      })

Axios网络请求封装

在日常应用过程中,一个项目中的网络请求会很多,此时一般采取的方案是将网络请求封装起来

src目录下创建文件夹utils,并创建文件request,用来存储网络请求对象 axios

import axios from "axios"
import qs from "querystring"


const errorHandle = (status,info) => {
    switch(status){
        case 400:
            console.log("语义有误");
            break;
        case 401:
            console.log("服务器认证失败");
            break;
        case 403:
            console.log("服务器拒绝访问");
            break;
        case 404:
            console.log("地址错误");
            break;
        case 500:
            console.log("服务器遇到意外");
            break;
        case 502:
            console.log("服务器无响应");
            break;
        default:
            console.log(info);
            break;
    }
}


const instance = axios.create({
    timeout:5000
})

instance.interceptors.request.use(
    config =>{
        if(config.method === "post"){
            config.data = qs.stringify(config.data)
        }
        return config;
    },
    error => Promise.reject(error)
)

instance.interceptors.response.use(
    response => response.status === 200 ? Promise.resolve(response) : Promise.reject(response),
    error =>{
        const { response } = error;
        errorHandle(response.status,response.info)
    }
)

export default instance;

src目录下创建文件夹api,并创建文件indexpath分别用来存放网络请求方法和请求路径

// path.js
const base = {
    baseUrl:"http://iwenwiki.com",
    chengpin:"/api/blueberrypai/getChengpinDetails.php"
}

export default base
// index.js
import path from "./path"
import axios from "../utils/request"

export default {
    getChengpin(){
        return axios.get(path.baseUrl + path.chengpin)
    }
}

在组件中直接调用网络请求

import api from "../api/index"

api.getChengpin().then(res =>{
    console.log(res.data);
})

网络请求跨域解决方案

JS采取的是同源策略。

同源策略是浏览器的一项安全策略,浏览器只允许js 代码请求和当前所在服务器域名,端口,协议相同的数据接口上的数据,这就是同源策略。

也就是说,当协议、域名、端口任意一个不相同时,都会产生跨域问题,所以又应该如何解决跨域问题呢。

跨域错误提示信息

目前主流的跨域解决方案有两种:

  1. 后台解决:cors
  2. 前台解决:proxy
devServer: {
    proxy: {
      '/api': {
        target: '<url>',
        changeOrigin: true
      }
    }
}

温馨提示

解决完跨域配置之后,要记得重启服务器才行哦!

Vue引入路由配置

在Vue中,我们可以通过vue-router路由管理页面之间的关系

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。

在Vue中引入路由

第一步:安装路由 npm install --save vue-router

第二步:配置独立的路由文件

// index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

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

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

第三步:引入路由到项目

// main.js
import router from './router'
app.use(router)

第四步:指定路由显示入口 <router-view/>

第五步:指定路由跳转

<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>

路由传递参数

页面跳转过程中,是可以携带参数的,这也是很常见的业务

例如:在一个列表项,点击进入查看每个列表项的详情

第一步:在路由配置中指定参数的key

{
    path:"/list/:name",
    name:"list",
    component:() => import("../views/ListView.vue")
}

第二步:在跳转过程中携带参数

<li><router-link to="/list/内蒙">内蒙旅游十大景区</router-link></li>
<li><router-link to="/list/北京">北京旅游十大景区</router-link></li>
<li><router-link to="/list/四川">四川旅游十大景区</router-link></li>

第三步:在详情页面读取路由携带的参数

<p>{{ $route.params.name }}城市旅游景区详情</p>

嵌套路由配置

路由嵌套是非常常见的需求

第一步:创建子路由要加载显示的页面

第二步:在路由配置文件中添加子路由配置

{
    path:"/news",
    name:"news",
    redirect:"/news/baidu",
    component:() => import("../views/NewsView.vue"),
    children:[
       {
       		path:"baidu",
            component:() => import("../views/NewsList/BaiduNews.vue"),
       },
       {
            path:"wangyi",
            component:() => import("../views/NewsList/WangyiNews.vue"),
       }
    ]
}

第三步:指定子路由显示位置<router-view></router-view>

第四步:添加子路由跳转链接

<router-link to="/news/baidu">百度新闻</router-link> | 
<router-link to="/news/wangyi">网易新闻</router-link>

第五步:重定向配置 redirect:"/news/baidu"

Vue状态管理(Vuex)

组件中的信息传递需要在被引用的组件中声明参数,在引用组件中传参。若组件的层级过深则需要频繁声明变量和传参,比较繁琐。Vue官方提供了VueX,可以集中地管理这些参数/变量。我们只需要在VueX中声明需要在各个组件中使用的变量和函数,在其他需要的组件中引用这个变量就可以。并且这些变量是全局的,在一个组件中改变了它的值后,其他组件内的值也会跟着改变。

引入Vuex的步骤

第一步:安装Vuex npm install --save vuex

第二步:配置Vuex文件

在src文件夹下创建一个文件夹store,在store文件夹下创建一个index.js,里面写上如下代码。

import { createStore } from 'vuex'

export default createStore({
  state: {
      counter:0
  }
})

第三步:在主文件中引入Vuex

在main.js中添加如下代码,表示在全局应用对象中使用vuex。

import store from './store'
app.use(store)

第四步:在组件中读取状态

在组件的template中添加如下内容渲染文档。

<p>counter:{{ $store.state.counter }}</p>

也可以是:

<script>
import { mapState } from 'vuex';
computed:{
    ...mapState(["counter"])
}
</script>
<template>
<p>{{ counter }}</p>
</template>

Vue状态管理核心(Vuex)

最常用的核心概念包含: StateGetterMutationAction

Getter

对Vuex中的数据进行过滤

import { createStore } from 'vuex'

export default createStore({
  state: {
    counter: 0
  },
  getters: {
    getCount(state){
      return state.counter > 0 ? state.counter : "counter小于0,不符合要求"
    }
  }
})
import { mapState,mapGetters } from 'vuex';
computed:{
    ...mapGetters(["getCount"])
}

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数

import { createStore } from 'vuex'

export default createStore({
  state: {
    counter: 0
  },
  getters: {
  },
  mutations: {
    setCounter(state, num) {
      state.counter += num
    }
  }
})
import { mapState,mapMutations } from 'vuex';

methods:{
    ...mapMutations(["setCounter"]),
    clickHandler(){
      // this.$store.commit("setCounter",20)
	  // 或者
      // this.setCounter(10)
    }
}

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态
  • Action 可以包含任意异步操作
import { createStore } from 'vuex'
import axios from "axios"

export default createStore({
  state: {
    counter: 0
  },
  getters: {
    getCount(state){
      return state.counter > 0 ? state.counter : "counter小于0,不符合要求"
    }
  },
  mutations: {
    setCounter(state, num) {
      state.counter += num
    }
  },
  actions: {
    asyncSetCount({ commit }){
      axios.get("http://iwenwiki.com/api/generator/list.php")
      .then(res =>{
        commit("setCounter",res.data[0])
      })
    }
  }
})

import { mapState,mapMutations,mapGetters,mapActions } from 'vuex';

methods:{
    ...mapActions(["asyncSetCount"]),
    clickAsyncHandler(){
        // this.$store.dispatch("asyncSetCount")
        // 或者
        // this.asyncSetCount()
    }
}

Vue3新特性1

Vue3是目前Vue的最新版本,自然也是新增了很多新特性

六大亮点

  • Performance:性能更比Vue 2.0强。
  • Tree shaking support:可以将无用模块“剪辑”,仅打包需要的。
  • Composition API:组合API
  • Fragment, Teleport, Suspense:“碎片”,Teleport即Protal传送门,“悬念”
  • Better TypeScript support:更优秀的Ts支持
  • Custom Renderer API:暴露了自定义渲染API

ref或者reactive

在2.x中通过组件data的方法来定义一些当前组件的数据

data() {
  return {
    name: 'iwen',
    list: [],
  }
}

在3.x中通过ref或者reactive创建响应式对象

import { ref,reactive } from "vue"
export default {
  name: 'HelloWorld',
  setup(){
      const name = ref("iwen")
      const state = reactive({
          list:[]
      })

    return{
        name,
        state
    }
  }
}

methods中定义的方法写在setup()

在2.x中methods来定义一些当前组件内部方法

methods:{
    http(){}
}

在3.x中直接在setup方法中定义并return

setup() {
    const http = ()=>{
        // do something
    }
    return {
      http
    };
}

setup()中使用props和context

在2.x中,组件的方法中可以通过this获取到当前组件的实例,并执行data变量的修改,方法的调用,组件的通信等等,但是在3.x中,setup()在beforeCreate和created时机就已调用,无法使用和2.x一样的this,但是可以通过接收setup(props,ctx)的方法,获取到当前组件的实例和props

export default {
  props: {
    name: String,
  },
  setup(props,ctx) {
    console.log(props.name)
    ctx.emit('event')
  },
}

Vue3新特性2

在setup中使用生命周期函数

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup () 内部调用生命周期钩子

Options APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

Provide / Inject

  • provide() 和 inject() 可以实现嵌套组件之间的数据传递。
  • 这两个函数只能在 setup() 函数中使用。
  • 父级组件中使用 provide() 函数向下传递数据。
  • 子级组件中使用 inject() 获取上层传递过来的数据。
  • 不限层级
// 父组件
import { provide } from "vue"

setup() {
    provide("customVal", "我是父组件向子组件传递的值");
}
// 子组件
import { inject } from "vue"

setup() {
    const customVal = inject("customVal");
    return {
      customVal
    }
}

Fragment

Fragment翻译为:“碎片”

  • 不再限于模板中的单个根节点
<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App" />
</template>

Vue3加载Element-plus

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

Element Plus 基于 Vue 3,面向设计师和开发者的组件库

安装Element-Plus

npm install element-plus --save

完整引用

如果你对打包后的文件大小不是很在乎,那么使用完整导入会更方便

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

按需导入

按需导入才是我们的最爱,毕竟在真实的应用场景中并不是每个组件都会用到,这会造成不小的浪费

首先你需要安装unplugin-vue-componentsunplugin-auto-import这两款插件

npm install -D unplugin-vue-components unplugin-auto-import

然后修改vue.config.js配置文件

const { defineConfig } = require('@vue/cli-service')
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = defineConfig({
  transpileDependencies: true,
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ]
  }
})

最后,可以直接在组件中使用

<template>
	<el-button>Default</el-button>
	<el-button type="primary">Primary</el-button>
</template>

Vue3加载Element-plus的字体图标

Element-plus不仅仅是提供了各种组件,同时还提供了一整套的字体图标方便开发者使用

安装icons字体图标

npm install @element-plus/icons-vue

全局注册

在项目根目录下,创建plugins文件夹,在文件夹下创建文件icons.js文件

import * as components from "@element-plus/icons-vue";
export default {
    install: (app) => {
        for (const key in components) {
            const componentConfig = components[key];
            app.component(componentConfig.name, componentConfig);
        }
    },
};

引入文件

main.js中引入icons.js文件

import elementIcon from "./plugins/icons";
app.use(elementIcon)

使用方式

接下来就可以直接在组件中引入使用了

<el-icon class="expand" color="#409EFC" :size="30">
    <expand />
</el-icon>

组合式API相关

组合式API组件的相关变量是在beforeCreate事件前执行的。

<script>
export default {
  setup() {
    console.log('setup.')
  },
  beforeCreate() {
    console.log('beforeCreate.')
  }
}
</script>

执行结果是先输出setup后输出beforeCreate。

定义变量

在默认导出时若在setup函数中定义变量,则需要返回这些变量以便于外部使用。

<script>
export default {
  setup() {
    const message = 'Hello World';
    function print() {
      console.log(message)
    }
    return {
      message,
      print
    }
  }
}
</script>
<template>
  <span> Message is: {{ message }} </span>
  <button @click="print"> Print Message </button>
</template>

可以给script标签添加setup标签,添加了setup标签的script标签为我们省去了上述定义和返回的步骤,我们可以直接在组件中使用这些变量。

<script setup>
const message = 'Hello World'
function print() {
  console.log(message)
}
</script>
<template>
  <span> Message is: {{ message }} </span>
  <button @click="print"> Print Message </button>
</template>

这里带setup的script标签就是语法糖。

若要定义可变值的数据,不能简单地将它赋值给一个变量,而是要使用vue为我们提供的响应式函数。定义对象使用reactive函数,定义普通变量则使用ref函数。使用这两个函数定义响应式变量之后,只要这两个变量的内容被改变,包括对象的键值和普通变量的值,Vue都能检测到并更改每一个引用这个变量的组件中所有值。

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

const person = reactive({
  name: "张三",
  age: 18
})
const count = ref(0)
</script>
<template>
  <span> {{ person.name }} 的年龄为 {{ person.age }} </span>
  <button @click="person.age ++">增加person的年龄</button>
  <span>{{ count }}</span>
  <button @click="count++"> count+1 </button>
</template>

在脚本区域更改count的值时则必须通过.value属性来更改。

import {reactive, ref} from "vue"
const count = ref(0)
count++ // 错误
count.value++ // 正确

计算属性

由于响应式数据在更新后需要在其他组件内也实时更新,还是需要像上方那样使用vue提供的响应式函数来处理数据。计算变量的属性时需要使用vue提供的conputed函数,它接收一个函数作为参数,返回运算后的结果。

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

const arr = ref([1, 2, 3, 4])
const computedArr = computed(function () {
  return arr.value.filter(e => e > 2)
})

setTimeout(function () {
  arr.value.push(5, 6)
}, 3000)
</script>
<template>
  <span>原始响应式数组: {{ arr }}</span>
  <span>更新后的数组:{{ computedArr }}</span>
</template>

刚开始时输出的两个数组分别是1234和34,三秒之后数组元素被更新,两者就分别是123456和3456。

监听数据变化

组合式API的响应式数据更新时,使用watch函数侦听响应式数据源,并在数据更新后执行回调函数。watch函数需要导入,它接收两个参数。第一个参数为响应式对象,即ref对象,第二个参数为一个回调函数,可以是匿名函数。这个匿名函数可以包含两个参数,第一个参数为更新后的值,第二个参数为更新前的值。

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

const count = ref(0)
watch(count, function (newVal, oldVal) {
  console.log(`count被更新,更新前的值:${oldVal},更新后的值:${newVal}`)
})
</script>
<template>
  <button @click="count++"> {{ count }} + 1 </button>
</template>

这里的watch只侦听一个变量的变化,可以同时侦听多个变化,但这些变量中有一个变化后都会触发这个侦听器,这样会判断不出来哪个发生了变化。

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

const count1 = ref(0)
const count2 = ref(0)
watch([count1, count2], function (
    [newCount1, newCount2],
    [oldCount1, oldCount2]
) {
  console.log(`count1和count2有一个值发生改变,这两个数的新值:${newCount1}, ${newCount2},这两个数的旧值:${oldCount1}, ${oldCount2}`)
})
</script>
<template>
  <button @click="count1++"> {{ count1 }} + 1</button>
  <button @click="count2++"> {{ count2 }} + 1</button>
</template>

watch函数此时接受的第一个参数为一个列表,这个列表中要侦听的变量具有顺序,第二个参数还是回调函数,这个回调函数内的第一个参数仍然是新值,第二个参数仍然是旧值,这两个值也是列表,且和watch函数的第一个参数列表的变量声明顺序一致。

除watch函数还接受第三个参数,它是一个对象,它可以带有一个immediate属性,如果它为真,则在侦听器床架初期时会执行一次回调函数,在响应式数据改变之后还会执行回调函数。

watch(count, function(newVal, oldVal){ console.log(newVal, oldVal) }, {immediate: true})

watch函数侦听ref对象默认是浅层监听,直接修改对象的属性不会触发回调。watch的第三个参数可以加一个deep属性,若它为true,则代表开启深度监听,若响应式对象的属性值发生改变则会触发回调函数。

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

const obj = ref({count: 0})

watch(obj, function () {
      console.log('obj变化了')
    },
    {
      deep: true  // 若注释掉这行,在obj更改后不会触发回调函数
    }
)

</script>
<template>
  <button @click="obj.count++"> {{ obj.count }} + 1 </button>
</template>

deep深度监听会产生性能损耗,可以只监听对象的某一个属性。这时候watch的第一个参数就是一个返回此属性的回调函数。

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

const obj = ref({count: 0, age: 0})

watch(function () {
      return obj.value.count
    }, function () {
      console.log('obj变化了')
    },
    {
      deep: true  // 若注释掉这行,在obj更改后不会触发回调函数
    }
)

</script>
<template>
  <button @click="obj.count++"> {{ obj.count }} + 1</button>
</template>

组件生命周期

以组件渲染后为例,选项式API中使用mounted函数来在组件渲染完毕后执行回调函数。

<script>
export default {
  mounted: ()=> {
    console.log("组件被渲染完成")
  }
}
</script>
<template>  
</template>

在组合式API中,它对应的代码如下。

<script setup>
import {onMounted} from 'vue'

onMounted(function () {
  console.log("组件被渲染完成")
})
</script>
<template>
</template>

除beforeCreate和created,其他生命周期都是对应的名称前面加on,如beforeMounted在组合式API中的生命周期名称为onBeforeMounted。

beforeCreate和created对应的组合式API的生命周期则被封装进了setup中。

组件数据传递 - 父传子

组合式API中,组件间的信息传递和选项式API一样,都是通过属性名="属性值"传递的,不同的地方还是在脚本代码上。得益于setup提供的语法糖,在父组件中导入子组件后,可以直接使用。

<script setup>
import ChildComponent from "./ChildComponent.vue";
</script>
<template>
  <ChildComponent message="Hello World"/>
</template>

在子组件中,使用defineProps宏定义组件内变量名称。

<script setup>
const props = defineProps({
  message: String
})
</script>
<template>
{{ message }}
</template>

若要传递变量,即响应式数据,需要在引用组件时使用v-bind,其他都和原来的一致。

<!--父组件-->
<script setup>
import ChildComponent from "./ChildComponent.vue";
import {ref} from "vue";

const number = ref(0)
setTimeout(() => {
  number.value ++
}, 3000)
</script>
<template>
  <ChildComponent :number="number"/>
</template>
<!--子组件-->
<script setup>
const props = defineProps({
  number: String
})
</script>
<template>
{{ number }}
</template>

组件数据传递 - 子传父

即子组件调用父组件中的函数。

在选项式API中,子传父是通过以下方法实现的。

父组件:

  • 导入并引入子组件,在子组件用v-on绑定一个自定义事件,事件名假如为customEvent,值为本组件内使用methods定义的countPlus。
  • 在data()中定义一个count,初始值为0,在本组件内输出count的值。
  • countPlus定义一个参数,调用时将这个组件内的count加上参数的值。

子组件

  • 在emits中声明上方声明的自定义事件名,在本例中为customEvent。
  • 在子组件中定义一个按钮,按钮点击后触发f函数,f函数使用this.$emit("customEvent", number),customEvent为自定义事件,number为在本组件中使用data()定义的一个变量,它与一个input使用v-model绑定。

在组合式API中,子传父是通过以下方式实现的:

父组件(使用setup语法糖)

  • 直接导入子组件,可直接使用;
  • 定义一个函数,这个函数要被子组件调用。
  • 在引用组件时使用v-on传递一个自定义事件,事件名称这里假如为customEvent,值为上方定义的函数。

子组件(使用setup语法糖)

  • 使用defineProps宏定义上方传入的自定义事件名,该方法返回一个此组件内可用的emit对象,和组合式API的this.$emit很像;
  • 使用emit(customEvent, args)调用即可。
<!--父组件-->
<script setup>
import ChildComponent from "@/ChildComponent.vue";
import {ref} from "vue";
const count = ref(0)
function func(number) {
  count.value += number
}
</script>
<template>
  count = {{ count }}
  <ChildComponent @custom-event="func" />
</template>
<!--子组件-->
<script setup>
import {ref} from "vue";

const number = ref(0)
const emit = defineEmits([
    "customEvent"
])
function clickFunc() {
  emit("customEvent", number.value)
}
</script>
<template>
  <h2>子组件</h2>
  <input v-model="number" type="number">
  <button @click="clickFunc">点击</button>
</template>

模板引用

组合式API中,在初始声明响应式变量时,将初始值设为null,然后在被引用的组件上使用ref引用这个变量即可。

<script setup>
import {ref, onMounted} from "vue";
const span = ref(null);
onMounted(function(){
  console.log(span)  //组件渲染完成才能获取
})
</script>
<template>
  <span  ref="span">span元素</span>
  <button @click="console.log(span)">输出span元素</button>
</template>

若在组件上使用ref属性,变量引用的将是组件本身。

默认情况下,setup语法糖内部的属性和方法是不开放给父组件使用的,可以通过defineExpose宏指定哪些属性和方法能被访问。

import {ref} from "vue";
const count = ref(0);
const f = function() {
  console.log('f函数');
}

defineExpose([
    count, f
])

跨层传递数据

在多个具有嵌套关系的组件中,更顶层的组件可以用provide函数定义数据,它接受两个参数,第一个参数为键类型,第二个参数为一个普通数据或一个响应式对象。底层的组件使用inject接受组件,它接受一个参数,为键名,返回一个对象,可用const承接。

<!--父组件-->
<script setup>
import {provide, ref} from "vue";
import ChildComponent from "./SonComponent.vue";

const constant = '这是一个常量';
const count = ref(0);
const countPlus = () => {
  count.value++
}
provide('constant-key', constant);
provide('count-key', count);
provide('countPlus-key', countPlus)
</script>
<template>
  <h2>父组件</h2>
  <ChildComponent />
</template>
<!--子组件-->
<script setup>
import GrandComponent from './GrandComponent.vue';
</script>
<template>
  <h2>子组件</h2>
  <GrandComponent/>
</template>
<!--孙子组件-->
<script setup>
import {inject, ref} from "vue";
const constant = inject('constant-key');
const count = inject('count-key');
const countPlus = inject('countPlus-key');
</script>
<template>
  <span>{{ constant }}</span>
  <span>{{ count }}</span>
  <button @click="countPlus"> + 1 </button>
</template>
上次编辑于:
贡献者: 棋.