vue3入门学习

297 阅读11分钟

vue@3

官方文档

vue 的两个核心功能:

  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

Vue 的组件可以按两种不同的风格书写:选项式 API组合式 API

Vue3 中,我们依旧可以使用 OptionAPI当然不建议 和 Vue3 混用

选项式 API (Options API)

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethodsmounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例

组合式 API (Composition API)

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与setup 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

优点:

  1. 增加了代码的可维护性 Vue2 使用的是 options 的API ,代码逻辑比较分散,可读性差,可维护性差。Vue3 使用的是 compositionAPI 逻辑分明,可维护性高,更友好的支持TS。在 template 模板中支持多个根节点,支持jsx语法。
  2. 提升了页面渲染性能 Vue3 在更新DOM算法上,做了优化。在 Vue2 中,每次更新diff,都是全量对比,Vue3则只对比带有标记的,这样大大减少了非动态内容的对比消耗。
  3. 加强了 MVVM 双向数据绑定的效率 Vue2 的双向数据绑定是利用 ES5 的 Object.definePropert() 对对象属性进行劫持,结合 发布订阅模式的方式来实现的。Vue3 中使用了 es6 的 ProxyAPI 对数据代理。 相比于vue2.x,使用proxy的优势如下:

defineProperty只能监听某个属性,不能对全对象监听 可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可) 可以监听数组,不用再去单独的对数组做特异性操作 vue3.x 可以检测到数组内部数据的变化

  1. 项目可持续发展 Vue 2官方还会再维护两年,但两年后的问题和需求,官方就不承诺修复和提供解答了,Vue 3 则不会。

Composition Api

setup

它是 Vue3 的一个新语法糖,在 setup 函数中。所有 ES 模块导出都被认为是暴露给上下文的值,并包含在 setup() 返回对象中。相对于之前的写法,使用后,语法也变得更简单。

setup相当于vue2的created,主要是在里面写,每次都会自动执行

使用方式极其简单,仅需要在 script 标签加上 setup 关键字即可。示例:

vue3.0

<template>
<BasicForm></BasicForm>
  <div>
  {{aa}}
  </div>
</template>
<script lang="ts">
import { defineComponent,ref } from 'vue';
import { BasicForm } from '/@/components/Form/index';
export default defineComponent({
   components: { BasicForm },
    setup(){
       const aa = ref('123') 
       return {
           aa
       }
    }
   }
 });
</script>

vue3.2 script setup 语法糖

<template setup>
<BasicForm></BasicForm>
  <div>
  {{aa}}
  </div>
</template>
<script lang="ts">
import { ref } from 'vue';
import { BasicForm } from '/@/components/Form/index';
const aa = ref('123') 
</script>

使用 script setup 语法糖,组件只需引入不用注册,属性和方法也不用返回,也不用写setup函数

reactive

作用:创建原始对象的响应式副本,即将「引用类型」数据转换为「响应式」数据

参数: reactive参数必须是对象或数组

<template>
  <button @click="change">
    {{ state.count }} <!-- 当点击button时,显示为 1 } -->
  </button>
</template><script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
​
const change = () =>{
    reactive.count = 1
}
​
</script>

这个响应式对象其实就是一个 Proxy, Vue 会在这个 Proxy 的属性被访问时收集副作用,属性被修改时触发副作用。

reactive() 的局限性

reactive() 虽然强大,但也有以下几条限制:

  1. 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 stringnumberboolean 这样的原始类型无效。

    <template>
      <button @click="change">
        {{ state }}
      </button>
    </template><script setup>
    import { reactive } from "vue";
    ​
    let state = reactive(0);
    const change = () => {
      state++;
    };
    </script>
    
  2. 因为 Vue 的响应式系统是通过属性访问进行追踪的,如果我们直接“替换”一个响应式对象,这会导致对初始引用的响应性连接丢失:

    <template>
      <button @click="change">
        {{ state }}
        <!-- 当点击button时,始终显示为 { "count": 0 } -->
      </button>
    </template>
    <script setup>
    import { reactive } from "vue";
    ​
    let state = reactive({ count: 0 });
    const change = () => {
      state = reactive({ count: 1 });  // 非响应式替换
    };
    </script>
    

ref

作用:把基本类型的数据变为响应式数据。

参数:

1.基本数据类型

2.引用类型

3.DOM的ref属性值

<template>
  <button @click="change" ref="buttonRef">
    {{ state }}
    <!-- 当点击button时,显示为 1 } -->
  </button>
</template><script setup>
import { ref } from "vue";
​
const state = ref(0);
const change = () => {
  state.value++;
};
const num = ref({ num: (Number = 1) }); //console.log(num.value.num) --- 1
num.value.num = 2;                      //console.log(num.value.num) --- 2const buttonRef = ref(null); //获取到button的ref
</script>

isRef()

作用:判断某个值是否为 Ref 对象,如果是 Ref 对象的话,它就是响应式的,且需要通过 .value 取值或赋值。

<script setup>
import {ref, isRef } from "vue";
let a = ref('1')
let b = 1
//console.log(isRef(a)) true
//console.log(isRef(b)) false
</script>

unref()

作用:如果参数是 ref,则返回内部值,否则返回参数本身。

const obj = unref(info)
​
isRef(info) ? info.value : info

toRef()

作用:基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

const state = reactive({
    foo:1
})
const fooRef = toRef(state,'foo')
​
fooRef.value++
​
//console.log(state.foo) 2
//console.log(fooRef.value) 2

toRefs()

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

<script setup>
import { reactive, toRefs } from "vue";
export default {
    const state = reactive({
      age: 18,
      name: "zs"
    });
    const stateToRefs = toRefs(state)
    
    console.log(stateToRefs.age.value) // 18
    
    
    stateToRefs.age.value++
    
    console.log(stateToRefs.age.value) // 19
    console.log(state.age) // 19
};
</script>

computed

作用:接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

示例

创建一个只读的计算属性 ref:

<script setup>
import { ref, computed } from "vue";
const count = ref(1);
const plusOne = computed(() => count.value + 1);
​
console.log(plusOne.value); // 2// plusOne.value++; // 错误
count.value++
​
console.log(plusOne.value); // 2
</script>

创建一个可写的计算属性 ref:

<script setup>
import { ref, computed } from "vue";
const count = ref(1);
​
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1;
  },
});plusOne.value = 1;
console.log(count.value); // 0
</script>

watchEffect

作用:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

返回值是一个用来停止该副作用的函数。

<script setup>
import { watchEffect, ref } from "vue";
const count = ref(0);
​
watchEffect(() => console.log(count.value));
// -> 输出 0
count.value++;
// -> 输出 1
</script><script setup>
import { watchEffect, ref } from "vue";
const count = ref(0);
​
const stop = watchEffect(() => console.log(count.value));
​
let time = setInterval(() => {
  count.value++;
  if (count.value == 3) {
    stop();
    clearInterval(time);
  }
}, 1000);
</script>

watch

作用:侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

<script>
 import {watch} form 'vue'
watch(person,(newValue,oldValue)=>{
// person为你要监听的数据
})
// newValue为新数据 ,oldValue为旧数据
 //每次person发生变化,都会执行里面的函数
</script>

vue-router

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

  • 嵌套路由映射
  • 动态路由选择
  • 模块化、基于组件的路由配置
  • 路由参数、查询、通配符
  • 展示由 Vue.js 的过渡系统提供的过渡效果
  • 细致的导航控制
  • 自动激活 CSS 类的链接
  • HTML5 history 模式或 hash 模式
  • 可定制的滚动行为
  • URL 的正确编码

简单用法

<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const { currentRoute } = useRouter();
//路由跳转
router.push({path:'/',query:{data}})
   //path路径 ,query参数
    
//接收路由传参 
 console.log(currentRoute.value.query) //data
 console.log(currentRoute.value.path) // '/'
</script>

pinia

Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。Pinia 已经实现了 Vuex 5 中想要的大部分内容,并决定实现它 取而代之的是新的建议。

与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。

VuexStateGettesMutations(同步)、Actions(异步)

PiniaStateGettesActions(同步异步都支持)

Vuex 当前最新版是 4.x

  • Vuex4 用于 Vue3
  • Vuex3 用于 Vue2

Pinia 当前最新版是 2.x

  • 即支持 Vue2 也支持 Vue3

在 store 目录下创建一个 user.ts 为例,我们先定义并导出一个名为 user 的模块

import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
    state: () => {
        return { 
            count: 1,
            arr: []
        }
    },
    getters: { ... },
    actions: { ... }
})

defineStore 接收两个参数

第一个参数就是模块的名称,必须是唯一的,多个模块不能重名,Pinia 会把所有的模块都挂载到根容器上 第二个参数是一个对象,里面的选项和 Vuex 差不多

  • 其中 state 用来存储全局状态,它必须是箭头函数,为了在服务端渲染的时候避免交叉请求导致的数据状态污染所以只能是函数,而必须用箭头函数则为了更好的 TS 类型推导
  • getters 就是用来封装计算属性,它有缓存的功能
  • actions 就是用来封装业务逻辑,修改 state

访问 state

比如我们要在页面中访问 state 里的属性 count

由于 defineStore 会返回一个函数,所以要先调用拿到数据对象,然后就可以在模板中直接使用了

<template>
    <div>{{ user_store.count }}</div>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
// 解构
// const { count } = userStore()
</script>

比如像注释中的解构出来使用,是完全没有问题的,只是注意了,这样拿到的数据不是响应式的,如果要解构还保持响应式就要用到一个方法 storeToRefs(),示例如下

<template>
    <div>{{ count }}</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { userStore } from '../store'
const { count } = storeToRefs(userStore())
</script>

原因就是 Pinia 其实是把 state 数据都做了 reactive 处理,和 Vue3 的 reactive 同理,解构出来的也不是响应式,所以需要再做 ref 响应式代理

getters

Getter 完全等同于 Store 状态的 计算值。 它们可以用 defineStore() 中的 getters 属性定义。

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
  },
})
<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template><script setup>
import { userStore } from '../store'
const store = useStore()
​
    store.counter = 3
    store.doubleCount // 6</script>

更新和 actions

更新 state 里的数据有四种方法,我们先看三种简单的更新,说明都写在注释里了

<template>
    <div>{{ user_store.count }}</div>
    <button @click="handleClick">按钮</button>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
const handleClick = () => {
    // 方法一
    user_store.count++
    
    // 方法二,需要修改多个数据,建议用 $patch 批量更新,传入一个对象
    user_store.$patch({
        count: user_store.count1++,
        // arr: user_store.arr.push(1) // 错误
        arr: [ ...user_store.arr, 1 ] // 可以,但是还得把整个数组都拿出来解构,就没必要
    })
    
    // 使用 $patch 性能更优,因为多个数据更新只会更新一次视图
    
    // 方法三,还是$patch,传入函数,第一个参数就是 state
    user_store.$patch( state => {
        state.count++
        state.arr.push(1)
    })
}
</script>

第四种方法就是当逻辑比较多或者请求的时候,我们就可以封装到示例中 store/user.ts 里的 actions 里

可以传参数,也可以通过 this.xx 可以直接获取到 state 里的数据,需要注意的是不能用箭头函数定义 actions,不然就会绑定外部的 this 了

actions: {
    changeState(num: number){ // 不能用箭头函数
        this.count += num
    }
}

调用

user_store.changeState(1)

总得来说,Pinia 就是 Vuex 的替代版,可以更好的兼容 Vue2,Vue3以及TypeScript。在Vuex的基础上去掉了 Mutation,只保留了 state, getter和action。Pinia拥有更简洁的语法, 扁平化的代码编排,符合Vue3 的 Composition api

生命周期方法

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

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

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

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated
<script setup lang="ts"> 
import { onMounted } from 'vue';
​
onMounted(() => { console.log('mounted!'); });
​
</script>
  • setup() :开始创建组件之前,在beforeCreate和created之前执行。创建的是data和method
  • onBeforeMount() : 组件挂载到节点上之前执行的函数。
  • onMounted() : 组件挂载完成后执行的函数。
  • onBeforeUpdate(): 组件更新之前执行的函数。
  • onUpdated(): 组件更新完成之后执行的函数。
  • onBeforeUnmount(): 组件卸载之前执行的函数。
  • onUnmounted(): 组件卸载完成后执行的函数

定义组件的 props

通过defineProps指定当前 props 类型,获得上下文的props对象。示例:

<script setup>
  import { defineProps } from 'vue'
​
  const props = defineProps({
    title: String,
  })
</script>
<!-- 或者 -->
<script setup lang="ts"> 
    import { ref,defineProps } from 'vue';
    
    type Props={ 
        msg:string 
    }
    defineProps<Props>(); 
</script>

定义 emit

使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行 emit。示例:

<script setup>
  import { defineEmits } from 'vue'
​
  const emit = defineEmits(['change', 'delete'])
</script>

父子组件通信

defineProps 用来接收父组件传来的 props ; defineEmits 用来声明触发的事件。

//父组件
<template>
  <my-son foo="abc" @childClick="childClick" />
</template><script lang="ts" setup>
import MySon from "./MySon.vue";
​
const childClick = (e: any):void => {
  console.log('from son:',e); 
};
</script>
​
​
//子组件
<template>
  <span @click="sonToFather">信息:{{ props.foo }}</span>
</template><script lang="ts" setup>
import { defineEmits, defineProps} from "vue";
​
const emit = defineEmits(["childClick"]);     // 声明触发事件 
    
const props = defineProps({ foo: String });   // 获取props const sonToFather = () =>{
    emit('childClick' , props.foo) 
    // 第一个是调用的事件,第二个是传递的参数
}
</script>

子组件通过 defineProps 接收父组件传过来的数据,子组件通过 defineEmits 定义事件发送信息给父组件

插槽

如果想要子组件渲染父组件指定的数据,可以使用props,将父组件的数据传入进去。但如果我们要子组件渲染一段父组件指定的内容呢?

Vue提供了一个特别的组件 < slot>(插槽),它的作用就是在子组件中标记一个位置,然后将父组件中传入的内容渲染到它所以的位置中。

1. 基本使用

子组件:通过 < slot>< /slot>标签来定义插槽

//hello-world组件中
<div>
    <!--定义插槽-->
    <slot></slot>
</div>

父组件:

<hello-world>
      <!--向插槽插入内容-->
      <button>按钮</button>
</hello-world>

在子组件中通过 <slot>定义插槽,父组件中插入的内容将被放到<slot>定义的地方进行渲染。

最终子组件的渲染结果:

<div>
    <button>按钮</button>
</div>
​

2. 插槽的作用域

插槽的内容可以访问到父组件的数据,但是不能访问到子组件的数据。 这是因为插槽的内容是在父组件总进行编译,然后将编译好的内容传给子组件进行渲染。子组件并不会解析和编译,只是负责渲染父组件编译好的插槽内容。

插槽的内容是由它的定义者进行解析编译的,然后将编译好的内容传入到插槽中,由插槽进行渲染。

interface Person {
   name: string,
   age: number
}
const person = reactive<Person>({
   age: 18,
   name: "tom"
})
​
<hello-world>
   <!--向插槽插入内容-->
   <button>{{person.name}}</button> //可以使用person。
</hello-world>

引用Vue官网文档的一段话:

插槽内容无法访问子组件的数据,请牢记一条规则:

任何父组件模板中的东西都只被编译到父组件的作用域中;而任何子组件模板中的东西都只被编译到子组件的作用域中。

3.插槽的默认内容

可以为插槽定义默认内容,就和给props定义默认值一样,如果在父组件中没有传入,则使用默认内容。反之如果父组件有传入内容,则使用父组件传入的内容。

子组件中:

<div>
   <slot>
      <!--默认内容-->
      <button>按钮1</button>
   </slot>
</div>

父组件中没有传入值

<hello-world></hello-world>

渲染结果:渲染默认内容

<div>
   <button>按钮1</button>
</div>

父组件传入值

<hello-world>
   <button>按钮2</button>
</hello-world>

渲染结果:渲染父组件传入的内容

<div>
   <button>按钮2</button>
</div>

4. 具名插槽

具名插槽:就是给插槽定义一个标识,因为有时候一个组件中可能会定义多个插槽,为了能够区分他们,就需要给他们定义一个标识。

子组件:通过name属性给插槽定义标识:

<div>
   <slot name="header">
   </slot>
   <slot name="main">
   </slot>
</div>

父组件:通过 < template v-slot:标识>来将内容插入到指定的插槽中。

<hello-world>
   <template v-slot:header>
      <button>按钮01</button>
   </template>
   <template v-slot:main>
      <button>按钮02</button>
   </template>
</hello-world>

注意:如果一个插槽没有指定name属性,则会使用它的默认值:default

子组件:

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

父组件使用:

<hello-world>
   <template v-slot:default>
      <button>按钮</button>
   </template>
</hello-world>

上面的写法等价于:

<hello-world>
   <button>按钮2</button>
</hello-world>

直接使用就是传入给默认插槽

v-slot 有对应的简写方式 #,因此 <template v-slot:header> 可以简写为 <template #header>

<hello-world>
   <template v-slot:header>
      <button>按钮01</button>
   </template>
</hello-world>

等价于:

<hello-world>
   <template #header>
      <button>按钮01</button>
   </template>
</hello-world>

5.作用域插槽

在上面我们有提到过,插槽的内容是无法获取到子组件中的数据的。如果我们想要获取子组件中的数据呢?

可以在给插槽组件定义属性,在插槽上定义的属性,会因为属性的透传的特性,流入到组件内的标签元素中。 当然,如果只是这样我们还是没有办法去获取和使用的。

我们需要使用Vue给我们提供了一个指令 v-slot,这个指令不止能用来指定具名插槽,还可以用来获取 < slot>组件的透传对象。

slot组件上定义透传属性:

interface Person {
   name: string,
   age: number
}
​
const person = reactive<Person>({
   age: 18,
   name: "tom"
})
​
<div>
   <slot :brand="phone.brand" class="item" :price="phone.price"></slot>
</div>

通过 v-slot指令获取透传对象

<hello-world v-slot="row">
   <div>{{ row.class }}</div>
   <div>{{ row.price }}</div>
   <div>{{ row.brand }}</div>
</hello-world>

上面的例子是获取默认插槽的透传对象。如果是具名插槽该怎么获取?还是使用 v-slot指令来获取,不过使用方式有点变化。

定义具名插槽:

<div>
   <slot name="slot1" class="slot1" title="标题1"></slot>
   <slot name="slot2" class="slot2" title="标题2"></slot>
</div>

获取具名插槽的透传对象

<hello-world>
   <template v-slot:slot1="row1">
      <div>{{ row1.title }}</div> //标题1
   </template>
   <template v-slot:slot2="row2">
      <div>{{ row2.title }}</div> //标题2
   </template>
</hello-world>

通过 v-slot:插槽标识="透传对象"的方法来获取具名插槽的透传对象

6 简写

Vue 中,很多指令都有简写形式,v-slot:name 指令也有简写形式,比如看我们下面的示例。

原写法:

<template v-slot:footer>
</template>

简写法:

<template #footer>
</template>