写给Vue2使用者的Vue3学习笔记

🙋‍请注意,由于本人项目中引入了unplugin-auto-import的依赖,所以所有的代码示例中均未手动引入各种依赖库(ref、reactive、useRouter等等)

初始环境搭建

npm init vue@latest

模板语法

插值

同 Vue2

<span>Message: {{ msg }}</span>

HTML

同 Vue2

<p>HTML Text: {{ rawHtml }}</p>
<p>v-html: <span v-html="rawHtml"></span></p>

Attribute

同 Vue2

<div :txt="text"></div>

绑定多个值

同 Vue2

<script setup>
  import exampleCom from "./exampleCom.vue"
  import { ref } from "vue"
  const a = ref("21")
  const name = ref("Jae")
</script>

<template>
  <example-com v-bind="{age, name}"></example-com>
</template>

JavaScript 表达式

同 Vue2

<template>
  {{ num + 1 }}
  {{ izCorrect ? 'YES' : 'NO' }}
  {{ string.split('').reverse().join('') }}

  <div :id="`div-${id}`"></div>
</template>

函数

同 Vue2

<template>
  <span :title="formatTitle(date)">
    {{ formatDate(date) }}
  </span>
</template>

指令

同 Vue2

<template>
  <p v-if="izShow">Hello</p>
</template>

参数

同 Vue2

<template>
  <a :href="url">点我</a>
</template>

事件

同 Vue2

<template>
  <button @click="handleClickBtn">点我</button>
</template>

动态参数

同 Vue2

<template>
  <a :[attributeName]="url">点我</a>
</template>

动态的事件名称

同 Vue2

<template>
  <button @[eventName]="handleClickBtn">点我</button>
</template>

修饰符

同 Vue2

<template>
  <form @submit.prevent="onSubmit">
    ...
  </form>
</template>

响应式基础

开始学习,基本上是Vue3的船新版本🤣

声明状态

<template>
   {{ state.count }}
</template>

<script setup>
const state = reactive({
  count: 0
})
</script>

<style scoped></style>

声明方法

<template>
  <button @click="add">
    {{ state.count }}
  </button>
</template>

<script setup>
const state = reactive({
  count: 0
})
function add() {
  state.count++
}
</script>

<style scoped></style>

ref

reactive只能用于对象、数组和 Map、Set 这样的集合类型,对 string、number 和 boolean 这样的原始类型则需要使用ref

<template>
  <!--在 html 模板中不需要写 .value 就可以使用-->
  {{ count }}
</template>

<script setup>
const count = ref(0);
console.log(count); // { value: 0 }
console.log(count.value); // 0
</script>

<style scoped></style>

响应式样式

<template>
  <button @click="open = !open">切换</button>
  <div>Hello</div>  
</template>

<script setup>
const open = ref(false);
</script>

<style scope>
  div{
    overflow: hidden;
    height: v-bind("open ? '30px' : '0px'");
  }
</style>

Watch 和 Computed

监听状态

<template>
  <button @click="add">
    {{ count }}
  </button>
  <p> 是否为偶数: {{ isEvent ? '是' : '否' }} </p>
</template>

<script setup>
const count = ref(0)
const isEvent = ref(false)

function add() {
  state.count++
}

watch(count, () => {
  isEvent.value = count.value % 2 === 0
})

// 立即监听
watch(count, function() {
  isEvent.value = count.value % 2 === 0
}, {
  //立即执行一次
  immediate: true
})
</script>

WatchEffect

会立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数(类似计算属性)。而且是非惰性,会默认调用一次

<script setup lang="ts">
let num = ref(0)

setTimeout(() => {
  num.value++
}, 3000)

watchEffect(() => {
  // 第一次进入页面时,打印出num 值改变:0;三秒后,再次打印num 值改变:1
  console.log('num 值改变:', num.value)
})

</script>

计算状态

<script setup>
const text = ref('')
// 用watch也能实现,但是computed更好,有缓存,可以根据已有并用到的状态计算出新的状态
const str = computed(() => {
  return text.value.toUpperCase(); // 转换成大写
})
</script>

<template>
  <input v-model="text" />
  <p>STR: {{ str }}</p>
</template>

组件通信

到重点了,要注意咯🙋‍

defineProps

<!--子组件-->
<script setup>
defineProps({
  name: String
})
</script>

<template>
  <p>username: {{ name }}</p>
</template>
<!--父组件-->
<script setup>
const sonName = 'Jack'
</script>

<template>
  <children :name="sonName" />
</template>

defineEmits

<!--子组件-->
<template>
  <input v-model="keyword" />
  <button @click="onSearch">search</button>
</template>

<script setup>
const emit = defineEmits(['search'])
const keyword = ref('')
const onSearch = function() {
  emit('search', keyword.value)
}
</script>
<!--父组件-->
<template>
  <children @search="onSearch" />
</template>

<script setup>
const onSearch = (keyword) => {
  console.log(keyword)
}
</script>

defineProps + defineEmits + computed 组件初尝试

来写一个经典的弹窗组件吧😀

<template>
  <div v-if="dialogVisible" class="child-class">
    <span @click="handleClickClose">×</span>
    <div>我是一个模拟弹窗</div>
  </div>
</template>

<script setup>
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits(['update:visible'])

let dialogVisible = computed({
  get() {
    return props.visible
  },
  set(val) {
    emit('update:visible', val)
  }
})
const handleClickClose = () => {
  dialogVisible.value = false
}
</script>

<style scoped>
.child-class {
  width: 300px;
  height: 200px;
  margin-top: 20px;
  border: 2px solid #000;
  span {
    float: right;
    clear: both;
    font-size: 20px;
    color: red;
    cursor: pointer;
  }
}
</style>

<!--父组件-->
<script setup lang="ts">
import ChildrenCom from './components/icons/ChildrenCom.vue';

const open = ref(false)
const handleClick = () => {
  open.value = !open.value
}
</script>

<template>
  <button @click="handleClick">打开</button>
  <children-com v-model:visible="open" />
</template>

<style scoped></style>

注意:父组件中的 v-model:visible="open" 等同于 Vue2 中我们常写的 :visible.sync="open"

看下效果吧👇
请添加图片描述

defineExpose

<!--子组件-->
<template>
  {{ sonTxt }}
</template>

<script setup>
const props = defineProps({
  sonTxt: {
    type: String,
    default: '1'
  }
})

const test = '777'

defineExpose({
  test
})
</script>

<style scoped></style>
<!--父组件-->
<template>
   <children ref="compRef" :son-txt="txt" />
</template>

<script setup>
const txt = ref('Hello')

const compRef = ref(null)
onMounted(() => {
  console.log(compRef.value.test); // 777
})
</script>

<style scoped></style>

使用 TypeScript

更难了,做好准备🏇

为 props 标注类型

<script setup lang="ts">
// 当使用 <script setup> 时,defineProps支持从它的参数中推导类型
const props = defineProps({
  param1: { type: String, required: true },
  param2: Number
})
props.param1 // string
props.param2 // number | undefined

// 或者使用接口定义
interface IProps {
  param1: string,
  param2: number
}
const props = defineProps<IProps>()
</script>

Props 默认值

<script setup lang="ts">
interface IProps {
  msg?: string,
  strArray?: (string | number)[]
}
const props = withDefaults(defineProps<IProps>(), {
  msg: 'hello',
  strArray: () => [1, '2', '3']
})

console.log(props.strArray); // [1, '2', '3']
</script>

<template>
  {{ msg }}
</template>

<style scoped></style>

Props 解构

Vue3 中为了保持响应性,始终需要以 props.x 的方式访问这些 prop

所以不能够解构 defineProps 的返回值,因为得到的变量将不是响应式的、也不会更新😶

<!--子组件-->
<template>
  <!-- count 始终为 0 -->
  <div>{{ count }}</div>
  <!-- info 始终为 { age: 21 } -->
  <div>{{ info }}</div>
</template>

<script setup lang="ts">

const props = defineProps<{
  count: number;
  info: object;
}>()

const { count, info } = props
</script>
<!--父组件-->
<template>
   <comp :count="count" :info="info" />
</template>

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

const count = ref(0);
const info = ref({
	age: 21
})
// 模拟数据变化
setTimeout(() => {
	count.value++
	info.value = { age: 99 }
}, 1000)
</script>

<style scoped></style>

有两种方式解决😀

方式一:直接解构。但是请注意:vue@3.5 才支持

<template>
  <!-- count 会在1秒后变为1 -->
  <div>{{ count }}</div>
  <!-- info 会在1秒后变为 { age: 99 } -->
  <div>{{ info }}</div>
</template>

<script setup lang="ts">
const { count, info } = defineProps<{
	count: number;
	info: object;
}>()
</script>

方式二:toRef 与 toRefs

toRef,基于响应式对象上的一个属性,创建一个对应的 ref,这个 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值

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

<template>
  <!-- count 会在1秒后变为1 -->
  <div>{{ count }}</div>
  <!-- info 会在1秒后变为 { age: 99 } -->
  <div>{{ info }}</div>
</template>

<script setup lang="ts">
const props = defineProps<{
	count: number
	info: object
}>()

// const count = toRef(props, "count")
// const info  = toRef(props, "info")
const { count, info } = toRefs(props)
</script>

为 emits 标注类型

<!--子组件-->
<template>
  <button @click="handleClickBtn">click me</button>
</template>

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', value?: string): void
}>()
function handleClickBtn() {
  emit('change', '我是从子组件来的')
}
</script>
<!--父组件-->
<template>
   <comp @change="emitChange" />
</template>

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

function emitChange(val) {
  console.log(val);
}
</script>

<style scoped></style>

为 ref 标注类型

<template>
   <comp :msg="msg" />
</template>

<script setup lang="ts">
import comp from './components/comp.vue'
import type { Ref } from 'vue'

// 不能写msg: string,因为ref函数返回的是一个 Ref 类型的对象,而这么写实际是将其类型定义为了 string
// 因此,TypeScript 会报告类型不匹配的错误(不能将类型“Ref<string>”分配给类型“string”)
const msg: Ref<string> = ref('hello2')
</script>

<style scoped></style>

为reactive标注类型

<script setup lang="ts">
import { onMounted, reactive } from 'vue';
import ChildrenCom from './components/icons/ChildrenCom.vue';
interface IBook {
  title: string,
  person?: string
}

const book: IBook = reactive({
  title: '为reactive标注'
})
onMounted(() => {
  console.log(book.title); // 为reactive标注
})
</script>

<template>
  <children-com  />
</template>

<style scoped></style>

为 computed() 标注类型

<script setup lang="ts">
const count = ref(0)
const double = computed<number>(() => {
  // 若返回值不是 number 类型则会报错
  return count.value * 2
})
</script>

<template>
  <button @click="handleClick">Click</button>
</template>

<script lang="ts" setup>
const emit = defineEmits(['click'])
const handleClick = (e: MouseEvent) => {
  emit('click', e)
}
</script>

<style scoped></style>

当你启用了 ts 后,这么写会报错 参数“e”隐式具有“any”类型,这是因为TypeScript 检测到函数 handleClick 的参数 e 没有明确的类型声明,默认情况下被隐式地视为 any 类型,这不符合 TypeScript 的严格类型检查规则。

<template>
  <button @click="handleClick">Click</button>
</template>

<script lang="ts" setup>
const emit = defineEmits(['click'])
const handleClick = (e: MouseEvent) => {
  emit('click', e)
}
</script>

<style scoped></style>

为事件处理函数标注类型

<!--子组件-->
<template>
  <input @change="handleChange" />
</template>

<script lang="ts" setup>
const emit = defineEmits(['change'])
const handleChange = (e: Event) => { // 或者e: MouseEvent也可以
  emit('change', e)
}
</script>

<style scoped></style>
<!--父组件-->
<script setup lang="ts">
import ChildrenCom from './components/icons/ChildrenCom.vue';

const handleChildChange = (e: Event) => {
  console.log(e.target.value);
}
</script>

<template>
  <children-com @change="handleChildChange" />
</template>

<style scoped></style>

父组件这样子直接输出e.target.value是会报错的哦,会报错e.target 可能为 null类型 EventTarget 上不存在属性 value。这是因为Event 类型是一个通用类型,而 value 属性通常存在于特定类型的事件目标(如 HTMLInputElement)上

使用类型断言改进👇:

const handleChildChange = (e: Event) => {
  console.log((e.target as HTMLInputElement).value);
}

为模板引用标注类型

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

<script lang="ts" setup>
const el = ref(null)
onMounted(() => {
  el.value.focus()
})
</script>

<style scoped></style>

这么写的话 ts 报错 el.value 可能为 null ,这是因为在 onMounted 钩子执行时,el 可能还没有被正确引用到 DOM 元素,所以我们可以使用可选链操作符 ?. 来避免直接调用 focus 方法时出现的 null 错误。

const el = ref(null)
onMounted(() => {
  el.value?.focus()
})

这时候 ts 又报错了 类型“never”上不存在属性 focus,看来直接声明 el 为 ref(null) 是不行的,那怎么办呢?🧐

const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
  el.value?.focus()
})

介绍一种 Vue3.5 以上可使用的更直接的方法,使用 useTemplateRef

const element = useTemplateRef<HTMLInputElement>('el') // 甚至变量名都不需要跟template中ref的名字一样
onMounted(() => {
  element.value?.focus()
})

路由跳转,获取路由参数

直接上示例🤣
路由文件:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/home',
      name: 'home',
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('@/views/About.vue')
    }
  ]
})

export default router

关于页:

<template>
  <div>我是关于页</div>
</template>

<script setup></script>

<style scoped></style>

首页:

<template>
  <div>我是首页</div>
</template>

<script setup></script>

<style scoped></style>

App.vue

<script setup>
const router = useRouter()
const route = useRoute()
let title = ref('')
let url = ref('')

watch(route, val => {
  const routeName = val.name
  title.value = routeName === 'home' ? '前往关于页' : '前往首页'
  url.value = routeName === 'home' ? '/about' : '/home'
})
const goToPage = () => {
  router.push(url.value)
}
</script>

<template>
  <router-view></router-view>
  <button @click="goToPage">{{ title }}</button>
</template>

<style scoped></style>

在这里插入图片描述
在这里插入图片描述

获取上下文对象

Vue3 中无法使用 this 获取上下文对象了,虽然 Vue3 的开发中不使用这个问题也不大,但是对于刚从 Vue2 转到 Vue3 的同学可能依然还是想获取到类似 Vue2 中this 中的数据,有办法吗?🧐

有的👇

// 如果你没有安装自动导入依赖的依赖库,需要手动引入哦,import { getCurrentInstance } from 'vue'
const { ctx } = getCurrentInstance()
console.log(ctx); // 和 this 的属性基本一样,只不过这里每个都是Proxy

缓存路由组件

缓存一般的动态组件,Vue3 和 Vue2 的用法是一样的,都是使用 KeepAlive 包裹 Component。但缓存路由组件,由于Vue 3 由于引入了组合式 API 和 v-slot 功能,有更简洁的方式来实现动态缓存:

Vue2👇

<KeepAlive>
  <router-view />
</KeepAlive>

Vue3👇

<router-view v-slot="{ Component }">
  <keep-alive v-if="Component.meta && Component.meta.keepAlive">
      <component :is="Component" />
  </keep-alive>
  <component v-else :is="Component" />
</router-view>

逻辑复用

Vue2 中逻辑复用主要是采用 mixin,但 mixin 会有数据来源不明、引起命名冲突、可重用性有限等问题。所以 Vue3 推荐使用 hooks🥸

来个最经典的鼠标位置跟踪功能吧🤣

// useMouse.ts
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e: MouseEvent) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.addEventListener('mousemove', update)
  })

  return { x, y }
}
<script setup lang="ts">
import { useMouse } from './hooks/useMouse'

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

<template>
  <div>鼠标位置为:{{ x }}, {{ y }}</div>
</template>

<style scoped></style>

生命周期

这部分看文档吧,比较简单就不多说了,注意一下如果需要在组件创建前注入逻辑,直接在 <script setup> 中编写同步代码就可以了

全局 API

还记得Vue2 怎么添加全局属性和全局方法吗?是这样的:Vue.prototype.$http = () => {},在构造函数Vue的原型对象上添加。但是在Vue3中,需要在 app 实例上添加:

// main.ts
const app = createApp({})
app.config.globalProperties.$http = () => {}

CSS 样式穿透

<style lang="scss" scoped>
/* Vue2 */
/deep/ .el-form {
    .el-form-item { ... }
}

/* Vue3 */
:deep(.el-form) {
    .el-form-item { ... }
}
</style>

异步组件

通过 defineAsyncComponent 异步加载

<template>
  <Children></Children>
</template>

<script setup lang="ts">
// import Children from './Children.vue'
const Children = defineAsyncComponent(() => import('./Children.vue'))
</script>

Teleport

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响

<script setup lang="ts">
import ChildrenCom from './components/icons/ChildrenCom.vue';
</script>

<template>
  <!-- 插入至body -->
  <Teleport to='body'>
    <children-com />
  </Teleport>
  <!-- #app下 -->
  <children-com />
</template>

<style scoped></style>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大杯美式不加糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值