vue3学习笔记(总)——ts+组合式API(setup语法糖)
创始人
2024-05-14 01:04:57
0

文章目录

    • 1. ref全家桶
      • 1.1 ref()
      • 1.2 isRef()以及isProxy()
      • 1.3 shallowRef()
      • 1.4 triggerRef()
      • 1.5 customRef()
      • 1.6 unref()
    • 2. reactive全家桶
      • 2.1 reactive()
      • 2.2 readonly()
      • 2.3 shallowReactive() 和 shallowReadonly()
    • 3. to系列全家桶
      • 3.1 toRef()
      • 3.2 toRefs()
      • 3.3 toRaw()
    • 4. computed计算属性
        • 案例:购物车总价
    • 5. watch监听属性
      • 5.1 watch()
      • 5.2 watchEffect()
      • 5.3 总结
    • 6. 组件
      • 6.1 组件的生命周期
      • 6.2 全局组件的注册以及批量注册
      • 6.3 defineProps(父给子传值)
          • 响应性语法糖
      • 6.4 defineEmits(子给父传值)
      • 6.5 defineExpose
      • 6.6 递归组件
      • 6.7 动态组件
        • markRaw
    • 7. 插槽
      • 7.1 匿名插槽
      • 7.2 具名插槽
      • 7.3 动态插槽
      • 7.4 作用域插槽
    • 8. 内置组件
      • 8.1 异步组件&代码分包&suspense
        • 顶层 await
        • 异步组件以及defineAsyncComponent()方法
        • Suspense
        • 案例可见:vue3笔记案例——Suspense使用之骨架屏
      • 8.2 Transition&TransitionGroup动画组件
      • 8.3 Teleport传送组件
        • 基本使用
        • 案例可见:vue3笔记案例——Teleport使用之模态框
        • 禁用 Teleport
      • 8.4 KeepAlive缓存组件
        • 包含/排除(include/exclude)
        • 最大缓存实例数(max)
        • 缓存实例的生命周期
        • 案例:
    • 9. 依赖注入(Provide/Inject)
      • 9.1 Provide(提供)
      • 9.2 Inject (注入)
        • 注入默认值
        • 和响应式数据配合使用
          • 案例
      • 9.3 使用 Symbol 作注入名
    • 10. 兄弟组件传参以及Mitt
      • 10.1 event-bus
      • 10.2 Mitt
    • 11. TSX
    • 12. v-model
      • 12.1 组件中的v-model
        • 案例:v-model的实现
      • 12.2 内置修饰符
        • .lazy
        • .number
        • .trim
      • 12.3 自定义修饰符Modifiers
        • 基本使用
    • 13. 全局API
      • 13.1 app.config.globalProperties
      • 13.2 nextTick()
        • EventLoop
    • 14. 自定义指令
      • 14.1 指令钩子
        • 钩子参数
      • 14.2 简写形式
        • 案例:简单实现权限指令dome
      • 案例:自定义指令实现拖拽效果
    • 15. 组合式函数——“vue的hooks”
      • 15.1 基本使用
        • 命名
        • 输入参数
        • 返回值
    • 16. 插件
    • 17. 样式穿透及CSS 新特性
    • 18. h函数
      • 案例
    • 19. 环境变量及proxy代理
      • 19.1 环境变量
      • 19.2 proxy代理

1. ref全家桶

1.1 ref()

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

type M = {name: string,
}const msg1: Ref = ref('字符串')
// const msg1 = ref('字符串')
const msg2 = ref({name: '多多'})const changeMsg = () =>{msg1.value = '已修改'msg2.value.name = '小多改变了'
}
  • ref也可以获取dom属性
dom内容
// 名字要与ref绑定的名字一样
const dom = ref(null)const changeMsg = () => {console.log('dom.value?.innerText :>> ', dom.value?.innerText);console.log('dom :>> ', dom)
}

在这里插入图片描述

1.2 isRef()以及isProxy()

  • isRef:检查某个值是否为 ref
  • isProxy:检查一个对象是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理。

1.3 shallowRef()

ref() 的浅层作用形式。

type M = {name: string,
}const msg2 = shallowRef({name: '多多'})const changeMsg = () =>{// msg2.value.name = '小多改变了' // 视图不会改变msg2.value = {name: '改变了'}
}

1.4 triggerRef()

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。强制更新

注意: ref()和shallowRef()不能一块写,不然会影响shallowRef 造成视图更新

const msg1 = ref('字符串')
const msg2 = shallowRef({name: '多多'})const changeMsg = () =>{msg1.value = '改变了'msg2.value.name = '小多改变了,被影响' // 视图也会改变
}

由于 ref底层调用了triggerRef(),所以会造成视图的强制更新

const msg2 = shallowRef({name: '多多'})const changeMsg = () =>{msg2.value.name = '小多改变了'triggerRef(msg2)	// 视图强制更新了
}

1.5 customRef()

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

主要应用是:防抖

import { customRef } from 'vue'
function MyRef(value: T, delay = 500) {let timer: anyreturn customRef((track, trigger) => {return {get() {track() /* 收集依赖 */return value},set(newVal) {clearTimeout(timer)timer = setTimeout(() => {console.log('触发了');value = newValtimer = nulltrigger() /* 触发依赖,视图更新 */}, delay)},}})
}
const msg1 = MyRef('字符串')
// const msg1 = ref('字符串')
const msg2 = MyRef({ name: '多多' })const changeMsg = () => {// msg1.value = '小多改变了'msg2.value = {name: '改变'}
}

1.6 unref()

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

类型

function unref(ref: T | Ref): T

示例

function useFoo(x: number | Ref) {const unwrapped = unref(x)// unwrapped 现在保证为 number 类型
}

2. reactive全家桶

2.1 reactive()

返回一个对象的响应式代理。

interface Msg = {name: string
}
// ref 支持所有类型,reactive 只支持引用类型 Array Object Map Set...
// ref 取值赋值都需要添加.value	reactive 不需要添加.value
const msg1 = ref({name: 'ref---多多'})
const msg2:Msg = reactive({ name: 'reactive---多多' })
// 不推荐
// const msg2 = reactive({ name: 'reactive---多多' })const changeMsg = () => {msg1.value.name = 'ref---小多'msg2.name = 'reactive---小多'
}
  • reactive proxy 不能直接赋值,否则会破坏响应式对象
  • 不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

解决方案:1. 数组可以使用push加解构


解决方案: 2. 变成一个对象,把数组作为一个属性去解决


2.2 readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

// readonly 无法更改只读, 但会受原始数据的影响,原始数据改变则相应改变
let msg1 = reactive({ name: '改变' })const change = () => {let copy = readonly(msg1)msg1.name = '1111'// copy.name = '2222' // 无法更改console.log('msg1,copy :>> ', msg1, copy)
}

2.3 shallowReactive() 和 shallowReadonly()

  • shallowReactivereactive() 的浅层作用形式
  • shallowReadonlyreadonly() 的浅层作用形式

3. to系列全家桶

只对响应式对象有效果,对普通对象无效

3.1 toRef()

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

let msg1 = reactive({ name: '多多', age: 18 })
let age = toRef(msg1, 'age')const edit = () => {age.value++
}

应用场景: useDemo(value) 需要一个属性,但定义的是对象,则可以单独把属性取出来使用,而不破坏属性的响应性

3.2 toRefs()

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

let msg1 = reactive({ name: '多多', age: 18 })// toRefs源码类似
const myTORefs = (object: T) => {const map: any = {}for (const key in object) {map[key] = toRef(object, key)}return map
}// let { name, age } = msg1 /* 直接解构 不具备响应性,更改不会造成视图更新 */
let { name, age } = toRefs(msg1)  /* 使其解构的属性具备响应性 */const edit = () => {name.value = '小多'age.value++
}

3.3 toRaw()

根据一个 Vue 创建的代理返回其原始对象。

console.log('msg1, toRaw(msg1) :>> ', msg1, toRaw(msg1));

在这里插入图片描述

4. computed计算属性

计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。

  1. 函数形式
let price ref(0)
let m = computed(()=>{return `$` + price.value
})
  1. 对象形式
let price = ref(1)//$0
let mul = computed({get: () => {return price.value},set: (value) => {price.value = 'set' + value}
})

案例:购物车总价

5. watch监听属性

详情可了解:Vue3:watch 的使用场景及常见问题

5.1 watch()

  • 第一个参数是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

  • 第二个参数是cb回调函数:(newVal,oldVal,onCleanup)

  • 第三个参数是options配置项(一个对象):

    deep: true // 是否开启深层监听
    immediate: true // 是否立即调用一次
    flush: 'pre ’ | ‘sync’ | ‘post’ // 更新时机
    onTrack:函数,具备 event 参数,调试用。将在响应式 property 或 ref 作为依赖项被追踪时被调用
    onTrigger:函数,具备 event 参数,调试用。将在依赖项变更导致副作用被触发时被调用。

ref监听深层属性需要开启深层监听,深层监听引用类型旧值与新值一样

reactive,隐性开启深层监听

监听属性单一值,需将其变为getter 函数

注意: 深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

import { watch, reactive, ref } from 'vue'let msg1 = reactive({one: {two: {three: '内容',},},
})
let msg2 = ref('测试')
let msg3 = ref('多多')
let msg4 = ref(1)
let msg5 = ref(2)watch(()=> msg1.one.two.three,(newVal, oldVal) => {console.log('newVal, oldVal :>> ', newVal, oldVal)}
)watch([msg2, msg3],(newVal, oldVal) => {console.log('newVal, oldVal :>> ', newVal, oldVal)}
)
watch([msg3, ()=> msg4.value + msg5.value],(newVal, oldVal) => {console.log('newVal, oldVal :>> ', newVal, oldVal)}
)

在这里插入图片描述

onCleanup: onCleanup 接受一个回调函数,这个回调函数,在触发下一次 watch 之前会执行,因此,可以在这里,取消上一次的网络请求,亦或做一些内存清理及数据变更等任何操作。
作用场景: 监听数据变化发起网络请求时

let count = 2;
const loadData = (data) =>new Promise((resolve) => {count--;setTimeout(() => {resolve(`返回的数据为${data}`);}, count * 1000);});// 此时如果直接监听,两次数据变更时间太短,导致最后页面展示的data数据更新为 ’返回的数据为李四‘
// 原因:数据每次变化,都会发送网络请求,但是时间长短不确定,所以就有可能导致,后发的请求先回来了,所以会被先发的请求返回结果给覆盖掉。
setTimeout(() => {state.name = '李四';
}, 100);
setTimeout(() => {state.name = '王五';
}, 200);// 第二次更新时间在第一次网络请求结束之前
watch(() => state.name,(newValue, oldValue, onCleanup) => {let isCurrent = true;onCleanup(() => {// 在下次监听更新之前执行isCurrent = false;});// 模拟网络请求loadData(newValue).then((res) => {// 取消上次网络请求,上次网络请求还没完成就将isCurrent设置为false, 则不会变成第一次网络请求的结果,顺序执行第二次监听的结果if (isCurrent) {data.value = res;}});}
);

5.2 watchEffect()

watch() 是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

配置项
副作用刷新时机 flush 一般使用post

presyncpost
更新时机组件更新前执行强制效果始终同步触发组件更新后执行

其他配置项:onTrack函数,onTrigger函数

let msg1 = ref('多多测试')
let msg2 = ref('小多')watchEffect(() => {console.log('watchEffect监听 : 默认执行顺序等同于开启立即执行的watch');const dom1 = document.querySelector('#dom1')console.log('dom1 :>> ', dom1)console.log('msg1 :>> ', msg1)
})watchEffect(() => {console.log('watchEffect监听 : flush: "post"');const dom1 = document.querySelector('#dom1')console.log('post组件更新后执行dom1 :>> ', dom1)
}, {flush: 'post'
})watch(msg1,(newVal, oldVal) => {console.log('watch监听 : ');const dom1 = document.querySelector('#dom1')console.log('dom1 :>> ', dom1)console.log('newVal,oldVal :>> ', newVal, oldVal)},{immediate: true,}
)

在这里插入图片描述

  1. watchEffect 默认监听,也就是默认第一次就会执行;
  2. 不需要设置监听的数据,在 effect 函数中,用到了哪个数据,会自动进行依赖,因此不用担心类似 watch 中出现深层属性监听不到的问题;
  3. 只能获取到新值,由于没有提前指定监听的是哪个数据,所以不会提供旧值。

watchEffect监听可能出现的问题:
在异步任务(无论是宏任务还是微任务)中进行的响应式操作,watchEffect 无法正确的进行依赖收集。所以后面无论数据如何变更,都不会触发 effect 函数。

在这里插入图片描述

解决方法:
如果真的需要用到异步的操作,可以在外面先取值,再放到异步中去使用

在这里插入图片描述
清除副作用

watchEffect((onInvalidate) => {console.log('msg1 :>> ', msg1)onInvalidate(() => {// 第一次不执行console.log('before')})
})

在这里插入图片描述

停止监听
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:

const unwatch = watchEffect(() => {})// ...当该侦听器不再需要时
unwatch()

5.3 总结

  1. 当监听 Reactive 数据时:
    • deep 属性失效,会强制进行深度监听;
    • 新旧值指向同一个引用,导致内容是一样的。
  2. watchsourceRefImpl 类型时:
    • 直接监听 state 和 监听 () => state.value 是等效的;
    • 如果 ref 定义的是引用类型,并且想要进行深度监听,需要将 deep 设置为 true。
  3. watchsource 是函数时,可以监听到函数返回值的变更。如果想监听到函数返回值深层属性的变化,需要将 deep 设置为 true
  4. 如果想监听多个值的变化,可以将 source 设置为数组,内部可以是 Proxy 对象,可以是 RefImpl 对象,也可以是具有返回值的函数;
  5. 在监听组件 props 时,建议使用函数的方式进行 watch,并且希望该 prop 深层任何属性的变化都能触发,可以将 deep 属性设置为 true
  6. 使用 watchEffect 时,注意在异步任务中使用响应式数据的情况,可能会导致无法正确进行依赖收集。如果确实需要异步操作,可以在异步任务外先获取响应式数据,再将值放到异步任务里进行操作。

6. 组件

6.1 组件的生命周期

在这里插入图片描述

  1. beforeCreatecreated 两个生命周期在setup语法糖模式是没有的,用setup去代替
  2. onBeforeMount 时读不到dom元素,onMouted 以及之后的生命周期可以读取到dom元素。
  3. onBeforeUpdate 获取的是更新之前的dom,onUpdated 获取的是更新之后的dom
  4. onRenderTrackedonRenderTriggerd 用于调试,获取收集依赖

在这里插入图片描述

6.2 全局组件的注册以及批量注册

全局注册

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent .vue'
...const app = createApp(App)
// 全局注册
app.component('MyComponent', MyComponent)...
app.mount('#app')

批量注册:例如elmUI的icon

// main.ts// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}

6.3 defineProps(父给子传值)



 无传递值 

// ts版本
// const props = defineProps<{msg:string}>() /* 不需要定义默认值时 */
// 定义默认值需要使用 withDefaults --- ts专有的
const props = withDefaults(defineProps<{ msg: string, list: number[] }>(), {msg: '默认值',list: () => []
})// js版本
// const props = defineProps({
//   msg: {
//     type: String,
//     default: '默认值'
//   },
//   list: {
//     type: Array,
//     default: () => []
//   }
// })
// ts
// 也可以将类型声明提取出来,传递数据多时推荐
type Props = { msg: string, list: number[] }const props = withDefaults(defineProps(), {msg: '默认值',list: () => []
})// 也可以使用响应性语法糖结构默认值 ---目前为实验性的需要显式启用
const { msg = '默认值', list= []} = defineProps()
响应性语法糖

响应性语法糖

6.4 defineEmits(子给父传值)

例子:





6.5 defineExpose

可以通过 defineExpose 编译器宏来显式指定在



应用场景: 例如elm表单的方法

6.6 递归组件





使用unplugin-vue-define-options进行命名

npm i unplugin-vue-define-options -D
// tsconfig.json
{"compilerOptions": {// ..."types": ["unplugin-vue-define-options/macros-global" /* ... */]}
}
// vite
// vite.config.ts
import DefineOptions from 'unplugin-vue-define-options/vite'
import Vue from '@vitejs/plugin-vue'export default defineConfig({plugins: [Vue(), DefineOptions()],
})

6.7 动态组件

注意: reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用 shallowRef 或者 markRaw 跳过proxy 代理

markRaw

将一个对象标记为不可被转为代理。返回该对象本身。
类型

function markRaw(value: T): T

示例

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

7. 插槽

  1. 插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件
  2. v-slot 有对应的简写 #,因此