Skip to content

vue3

优点

  • 打包体积更小
  • 内存占用减少
  • 更好的支持TS
  • 使用Proxy代替defineProperty实现数据响应式
  • 重写虚拟DOM的实现和Tree-Shaking
  • 组合API (composition api) ,能够更好的组织逻辑,封装逻辑,复用逻辑

新增特性

  • Composition (组合) API
  • setup
    • ref 和 reactive
    • computed 和 watch
    • 新的生命周期函数
    • provide与inject
    • ...
  • 新组件
    • Fragment - 文档碎片
    • Teleport - 瞬移组件的位置
    • Suspense - 异步加载组件的loading界面
  • 其它API更新
    • 全局API的修改
    • 将原来的全局API转移到应用对象
    • 模板语法变化

使用vite快速创建项目

开发环境下基于浏览器 原生 ES 模块, 有兼容性问题: chrome > 61, firefox >60, safair > 10.1 。在生产环境下基于 Rollup 打包。

使用 npm:

bash
# npm 6.x
$ npm init vite@latest <project-name> --template vue

# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> -- --template vue

$ cd <project-name>
$ npm install
$ npm run dev

pnpm:

bash
$ pnpm create vite <project-name> -- --template vue
$ cd <project-name>
$ pnpm install
$ pnpm dev

Composition API(组合式api)

2.x的叫option API (配置 api)

官方文档

生命周期和钩子函数

创建->挂载->更新->销毁, 每个阶段都有两个钩子函数

  • setup()-> <script setup>
  • beforeCreate 创建实例前 -> 使用 setup()
  • created 创建实例后 -> 使用 setup()
  • beforeMount 挂载DOM前 -> onBeforeMount
  • mounted 挂载DOM后 -> onMounted
  • beforeUpdate 更新组件前 -> onBeforeUpdate
  • updated 更新组件后 -> onUpdated
  • beforeDestroy 卸载销毁前 -> onBeforeUnmount
  • destroyed 卸载销毁后 s-> onUnmounted
  • errorCaptured -> onErrorCaptured

可以多次使用同一个钩子,执行顺序和书写顺序相同。

|586x938

setup 函数

  • setup执行的时机
    • setup() → beforeCreate → created → ...
    • ==this是undefined==, 不能通过this来访问data/computed/methods/props
    • 其实所有的composition API相关回调函数中也都不可以
  • setup的返回值
    • 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
    • 若返回一个渲染函数,则可以自定义渲染内容
    • 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性, 返回对象中的方法会与methods中的方法合并成组件对象的方法, 如果有重名, setup优先
    • 一般不要混用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods
    • setup不能是一个async函数,因为使用async后返回值不再是return的对象,而是promise,模板看不到return对象中的属性(后期也可以使用suspense和异步组件的配合得到promise)
  • setup的参数
    • setup(props, context) / setup(props, {attrs, slots, emit})
    • props: 包含props配置声明且传入了的所有属性的对象
    • attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
    • slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
    • emit: 用来分发自定义事件的函数, 相当于 this.$emit

ps: ===官方文档里带$的api, 可以在模板里使用, 但不能在setup里使用===

setup 语法糖

它向模板公开了所有的顶层绑定

<script setup>是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。

define 开头的 api 都为编译器宏(compiler macros )api,只能在 <script setup> 中使用。它们不需要被导入,并且在处理 <script setup> 时被编译掉。

注意:define 类 api 必须直接在 setup 中外层进行使用,你无法将其放在方法中。

defineProps

js
// js 基础用法
intetface UserInfo {
  name: string,
  age: number
}

defineProps({
 name: {
   type: String,
   required: false,
   default: 'Petter',
 },
 userInfo: Object as PropsType<UserInfo>, // 注解 Props https://v3.cn.vuejs.org/guide/typescript-support.html#%E6%B3%A8%E8%A7%A3-props
 tags: Array,
})
ts
// 使用编译器宏
const props = defineProps<{ 
    foo: string 
    bar?: number
}>()

// 为了解决在TS类型声明下无法进行设置默认值, 使用了新的api withDefaults, 这个方法并非属于编译器宏api,但是这个 api 由 defineProps 衍生而出
withDefaults(defineProps<{
  size?: number
  labels?: string[]
}>(), {
  size: 3,
  labels: () => ['default label']
})

两种方法不能同时使用

3.5+ 版本 defineProps 可以在解构时设置默认值

ts
interface Props {
  msg?: string
  labels?: string[]
}

const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()

defindEmits

js
// 基础用法
// 声明
const emits = defineEmits(['change', 'delete'])
// 使用
emits('change')
ts
// ts声明
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 3.3+:另一种更简洁的语法
const emit = defineEmits<{
  change: [id: number] // 具名元组语法
  update: [value: string]
}>()

// 使用
emits('change',1)

两种方法不能同时使用

defineExpose

在vue2中, 所有暴露在模板上的东西都隐含地暴露在组件实例上,也就是父组件可以通过ref 或者子链可以全量获取到子组件所有的属性、方法。大多数时候,这种全量暴露是过度的,而 vue3 setup 中必须进行手动暴露。

ts
const a = 1
const b = ref(2)
defineExpose({ a, b, })

useSlots, useAttrs

useSlots: 获取插槽数据

useAttrs: 用来获取 attrs 数据,但和 vue2 不同,里面包含了 class属性方法

ts
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

限制

  1. 修改选项配置需要单开一个 script
ts
// Vue 3 SFC 一般会自动从组件的文件名推断出组件的 name。在大多数情况下,不需要明确的 name 声明。
// 唯一需要的情况是当你需要 <keep-alive> 包含或排除或直接检查组件的选项时,你需要这个名字。
<script>
	export default {
			name: 'YourName',
			inheritAttrs: false,
			customOptions: {},
	} 
</script>

<script setup>
 // your code
</script>
  1. ts兼容
ts
// 通过解构的方式去导入类型,setup sugar 会进行自动导出, 此时ts会报错
//解决方法:
import type { test } from "./test"

==vue 2 和 vue 3 的响应式区别==

vue 2

  • 核心:
    • 对象: 通过 Object.defineProperty() 对象的已有属性值的读取和修改进行劫持 (监视/拦截)
    • 数组: 通过 重写数组 更新数组一系列更新元素的 方法 来实现元素修改的劫持
js
Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})
  • 问题
    • 数组直接通过下标替换元素或更新 length, 界面不会自动更新
      • push() pop() shift() unshift() splice() sort() reverse() 这类会改变原数据的方法, 都进行了包裹
      • this.$set(this.array, index, newValue)
    • 对象直接新添加的属性或删除已有属性, 界面不会自动更新
      • this.$set(this.obj, key , newValue)
      • this.$delete(this.obj, key)
      • this.obj=Object.assign({}, this.obj, {a:1, b:2})

vue 3

  • 核心:
    • ref:创建的是一个包含 .value 属性的对象。对这个 .value 属性的访问和修改会触发 get 和 set 拦截,从而实现响应式(数据劫持)。
    • reactive:通过 Proxy 来实现响应式(数据劫持),并通过 Reflect 操作原对象内部的数据
  • 问题
    • 数组用下标更新元素或者更新 length, 同样会丢失响应式
      • push() pop() shift() unshift() splice() sort() reverse()
    • 对象使用 Proxy 实现响应式, 可以直接添加属性和删除属性
    • 对象提取的 基本类型 没有响应式
      • 用 computed 或者 toRef
    • 对象 解构展开运算符 会丢失响应式
      • toRefs
      • reactive 创建新的响应式对象
      • Object.assign(someReactive, newObj)
    • 对象 整个替换 会丢失响应式
  • 文档:
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Proxy 与 Reflect</title>
</head>
<body>
  <script>
    
    const user = {
      name: "John",
      age: 12
    };

    /* 
    proxyUser是代理对象, user是被代理对象
    后面所有的操作都是通过代理对象来操作被代理对象内部属性
    */
    const proxyUser = new Proxy(user, {

      get(target, prop) {
        console.log('劫持get()', prop)
        return Reflect.get(target, prop)
      },

      set(target, prop, val) {
        console.log('劫持set()', prop, val)
        return Reflect.set(target, prop, val); // (2)
      },

      deleteProperty (target, prop) {
        console.log('劫持delete属性', prop)
        return Reflect.deleteProperty(target, prop)
      }
    });
    // 读取属性值
    console.log(proxyUser===user)
    console.log(proxyUser.name, proxyUser.age)
    // 设置属性值
    proxyUser.name = 'bob'
    proxyUser.age = 13
    console.log(user)
    // 添加属性
    proxyUser.sex = '男'
    console.log(user)
    // 删除属性
    delete proxyUser.sex
    console.log(user)
  </script>
</body>
</html>

ref 函数

常用于简单数据类型定义为响应式数据

  • 在 script 中修改值,获取值的时候,需要.value
  • 在模板 template 中使用ref声明的响应式数据,不需要.value
  • 其实也可以定义复杂数据类型的响应式数据, 如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象
  • 基本数据类型:创建的是一个包含 .value 属性的对象。对这个 .value 属性的访问和修改会触发 getset 拦截,从而实现响应式追踪和更新。
vue
<template>
  <h2>{{ count }}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'

/* 使用vue3的composition API */

// 定义响应式数据 ref对象
const count = ref<number>(1)
console.log(count)

// 更新响应式数据的函数
function update() {
  // alert('update')
  count.value = count.value + 1
}
</script>
  • 减少心智负担, 优先 ref

toRef 函数

转换响应式对象中某个属性为单独响应式数据,并且值是关联的。

  • 语法:const name = toRef(person,"name")

注意: reactive 对象取出的所有属性值都是非响应式的。

使用场景: 有一个响应式对象数据,但是模版中只需要使用其中一项数据。

vue
<template>
  <div class="container">
    {{ name }} <button @click="updateName">修改数据</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRef } from 'vue'

// 1. 响应式数据对象
const obj = reactive({
  name: 'ls',
  age: 10
})
console.log(obj)

// 2. 模板中只需要使用name数据
// 注意:从响应式数据对象中解构出的属性数据,不再是响应式数据
// let { name } = obj 不能直接解构,出来的是一个普通数据
const name = toRef(obj, 'name')
// console.log(name)

const updateName = () => {
  console.log('updateName')
  // toRef转换响应式数据包装成对象,value存放值的位置
  name.value += '+'
}
</script>

<style scoped lang="less"></style>

toRefs 函数

转换一个响应式对象, 返回一个普通对象, 普通对象的每个属性都是响应式的

  • 语法:const person = toRefs(person)

使用场景:剥离响应式对象(解构|展开),想使用响应式对象中的多个或者所有属性做为响应式数据。

vue
<template>
  <div class="container">
    <div>{{ name }}</div>
    <div>{{ age }}</div>
    <button @click="updateName">修改数据</button>
  </div>
</template>

<script lang="ts" setup>
import { reactive, toRefs } from 'vue'

// 1. 响应式数据对象
const obj = reactive({
  name: 'ls',
  age: 10
})
console.log(obj)

// 2. 解构或者展开响应式数据对象
// const { name, age } = obj
// console.log(name, age)
// const obj2 = { ...obj }
// console.log(obj2)
// 以上方式导致数据就不是响应式数据了

const obj3 = toRefs(obj)
console.log(obj3)

const updateName = () => {
  // obj3.name.value = 'zs'
  obj.name = 'zs'
}

const { name, age } = obj3 // 这里用了解构
</script>

<style scoped lang="less">
</style>

isRef

检查值是否为一个 ref 对象。

unref

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

shallowRef函数

shallow: 浅层的

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的

简单理解: 只处理了value的响应式, 不进行对象的reactive处理

使用场景: 有一个对象数据, 后面会产生新的对象来替换(比如: 动态组件)

ts
let sum = shallowRef(0)
const update1 = ()=>{
  sum.value++ // 基础数据类型是响应式的
}

let sum2 = shallowRef({count: 0})
const update2 = ()=> {
  sum2.value.count++ // 对象不是响应式的, 视图不更新
}
const update3 = ()=>{
  sum2.value = {cont: 3} // value是响应式的, 视图会更新
}

triggerRef函数

手动使 shallowRef 的对象响应一次

ts
let sum2 = shallowRef({count: 0})
const update1 = ()=>{
  sum2.value.count++ // 对象不是响应式的, 视图不更新
}
const trigger => {
  triggerRef(sum2) // 视图触发更新一次
}

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 getset 的对象。

使用 customRef 实现 防抖 的示例

ts
import {customRef} from 'vue'

/* 
实现函数防抖的自定义ref
*/
function useDebouncedRef<T>(value: T, delay = 200) {
  let timeout: number
  return customRef((track, trigger) => {
    return {
      get() {
        // 告诉Vue追踪数据
        track()
        return value
      },
      set(newValue: T) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 告诉Vue去触发界面更新
          trigger()
        }, delay)
      }
    }
  })
}

const keyword = useDebouncedRef('', 500)

reactive 函数

一般用来定义复杂数据类型成为响应数据

  • reactive() 会对对象进行深层响应式转换,但是这种深层转换是**惰性的:只有当访问嵌套属性(触发 getter)时,Vue 才会递归将该属性也转为响应式。
  • 注意: 用解构取出的单独属性, 不再是响应式的(用toRef)
  • 给reactive赋值 {} , 不再是响应式(用ref). 举例: 从接口获取数据, 直接把data赋值给reactive, 会失去相应式. 改为用ref定义, 给ref.value赋值
  • 内部基于es6的proxy实现,通过代理对象内部数据进行操作
vue
<template>
  <h2>name: {{ state.name }}</h2>
  <h2>age: {{ state.age }}</h2>
  <h2>wife: {{ state.wife }}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script lang="ts" setup>
/* 
reactive: 
    作用: 定义多个数据的响应式
    const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
    响应式转换是“深层的”:会影响对象内部所有嵌套的属性
    内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
*/
import { reactive } from 'vue'

/* 
定义响应式数据对象
*/
const state = reactive({
  name: 'tom',
  age: 25,
  wife: {
    name: 'marry',
    age: 22
  },
})

console.log(state, state.wife)

const update = () => {
  state.name += '--'
  state.age += 1
  state.wife.name += '++'
  state.wife.age += 2
}
</script>
  • 用proxy实现数据相应式后, 就不再需要v2的this.$set了(proxy响应是深层次的)
vue
<template>
	ReactiveTest: <br>
	<div v-for="(user, index) in userInfos" :key="index">
		名字: {{user.name}} 年龄: {{user.age}} <button @click="ageAddOne(index)">年龄+1</button>
	</div>
	<button style="width:100px; height:50px;" @click="addUser">增加成员</button>
</template>

<script  setup lang="ts">
import { reactive } from 'vue'

let userInfos = reactive(
	[
		{
			name: '张三',
			age: 12
		},
		{
			name: '李四',
			age: 13
		}
	]
)

const addUser = ()=>{
	userInfos.push({
		name: 'test'+ ( userInfos.length + 1 ),
		age: 22  // proxy响应是深层次的
	})
}

const ageAddOne = (index)=>{
	userInfos[index].age += 1
}
	
</script>
  • 当将 ref分配给 reactive property 时,ref 将被自动解包。
ts
const count = ref(1)
const obj = reactive({})

obj.count = count

console.log(obj.count) // 1
console.log(obj.count === count.value) // true

shallowReactive

proxy代理有性能问题, https://www.zhihu.com/question/460330154, https://www.cnblogs.com/zmj97/p/10954968.html

创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值) 简单理解: 只有第一层是响应式的 应用: 有一个对象数据, 结构比较深, 但变化时只是外层属性变化

ts
const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

readonly函数

接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。

ts
const original = reactive({ count: 0 })
const copy = readonly(original)
original.count++
copy.count++ // 警告!

shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)

ts
const data = shallowReadonly({
  a: 1,
  b: {
    c: 2
  }
})
const update = ()=>{
  data.a ++ // error
  data.b.c++ // 成功
}

isReactive

检查对象是否是由 reactive 创建的响应式代理。

如果该代理是 readonly 创建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true

ts
const state = reactive({
  name: 'John'
})
// 从普通对象创建的只读 proxy
const plain = readonly({
  name: 'Mary'
})
console.log(isReactive(plain)) // -> false

// 从响应式 proxy 创建的只读 proxy
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true

isProxy

检查对象是否是由 reactivereadonly 创建的 proxy。

toRaw

  • 返回由 reactivereadonly 方法转换成响应式代理的普通对象。
  • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
ts
const state = reactive<any>({
  name: 'tom',
  age: 25,
})

const testToRaw = () => {
  const user = toRaw(state)
  user.age++  // 界面不会更新
}

markRaw

  • 标记一个对象,使其永远不会转换为代理。返回对象本身
  • 应用场景:
    • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
    • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
ts
const state = reactive<any>({
  name: 'tom',
  age: 25,
})
const testMarkRaw = ()=>{
  const likes = ['a', 'b']
	state.likes = markRaw(likes) // likes数组就不再是响应式的了
}

同一性风险: 因为原始选择退出仅在根级别,因此,如果将嵌套在内的、未标记的原始对象添加进响应式对象,然后再次访问该响应式对象,就会得到原始对象被代理后的版本

ts
const foo = markRaw({
  nested: {}
})

const bar = reactive({
  // 虽然 `foo` 被标记为原始,但 foo.nested 不是。
  nested: foo.nested
})

console.log(foo.nested === bar.nested) // false

computed 函数

计算属性 computed函数:

  • 与computed配置功能一致
  • 普通用法: 只有getter
  • 高级用法: 有getter和setter
  • 在script中获取computed的值需要.value, 在模板中不用
vue
<template>
  <h2>cpmputed</h2>
  fistName: <input v-model="user.firstName" /><br>
  lastName: <input v-model="user.lastName" /><br>
  fullName1(只有getter的计算属性): <input v-model="fullName1" /><br>
  fullName2(有getter与setter的计算属性): <input v-model="fullName2" /><br>
</template>

<script lang="ts" setup>
import { reactive, computed } from 'vue'

const user = reactive({
  firstName: 'A',
  lastName: 'B'
})

// 只有 getter 的计算属性
const fullName1 = computed(() => {
  console.log('fullName1')
  return user.firstName + '-' + user.lastName
})

// 有 getter 与 setter 的计算属性
const fullName2 = computed({
  get() {
    console.log('fullName2 get')
    return user.firstName + '-' + user.lastName
  },
  set(value: string) {
    console.log('fullName2 set')
    const names = value.split('-')
    user.firstName = names[0]
    user.lastName = names[1]
  }
})

// 在 script 中获取 computed 的值, 要用 .value
console.log(fullName2.value)
</script>

watch 函数

监听 watch 函数:

  • 与watch配置功能一致
  • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
  • 默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次
  • 通过配置deep为true, 来指定深度监视
  • ===监听reactive对象中的属性, 必须通过函数返回该属性来指定===
  • 回调函数可以接收到两个参数, newValue和oldValue
vue
<template>
  <h2>watch</h2>
  fistName: <input v-model="user.firstName"/><br>
  lastName: <input v-model="user.lastName"/><br>
  fullName1: <input v-model="fullName1"/><br>
</template>

<script setup lang="ts">
import { reactive, ref, watch } from 'vue'

interface User {
  firstName: string
  lastName: string
}

const user = reactive<User>({
  firstName: 'A',
  lastName: 'B'
})

const fullName1 = ref<string>('')

/* 
使用watch的2个特性:
  深度监视
  初始化立即执行
*/
watch(
  user,
  (newVal, oldVal) => {
    console.log('深度监听, 立即执行')
    fullName1.value = `${user.firstName}-${user.lastName}`
  },
  {
    immediate: true,  // 是否初始化立即执行一次, 默认是false
    deep: true,       // 是否是深度监视, 默认是false
  }
)

/* 
watch一个数据
  默认在数据发生改变时执行回调
*/
watch(fullName1, (newVal, oldVal) => {
  console.log('监听一个数据')
  const names = newVal.split('-')
  if (names.length === 2) {
    user.firstName = names[0]
    user.lastName = names[1]
  }
})

/* 
watch多个数据: 
  使用数组来指定
*/
watch(
  [() => user.firstName, () => user.lastName, fullName1],
  (values) => {
    console.log('监视多个数据', values)
  }
)
</script>

监听 prop

第一个参数需要通过箭头函数返回 props 的值 props 理解成 reactive 对象

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

// 定义 props
const props = defineProps({
  message: {
    type: String,
    required: true
  }
})

// 用于记录更新次数的响应式变量
const updateCount = ref(0)

// 监听 message prop 的变化
watch(
  () => props.message,
  (newValue, oldValue) => {
    console.log('消息已更新:', newValue)
    console.log('之前的消息:', oldValue)
    updateCount.value++
  }
)
</script>

watchEffect 函数

  • vue3特性
  • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
  • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
  • 监视数据发生变化时回调
vue
<template>
  <h2>watchEffect</h2>
  fistName: <input v-model="user.firstName"/><br>
  lastName: <input v-model="user.lastName"/><br>
  fullName1: <input v-model="fullName1"/><br>
</template>

<script lang="ts">
import {
  reactive,
  ref,
  watchEffect
} from 'vue'

export default {
  setup () {
    const user = reactive({
      firstName: 'A',
      lastName: 'B'
    })

    const fullName1 = ref('')

    /* 
    watchEffect: 监视所有回调中使用的数据
    */
    watchEffect(() => {
      console.log('watchEffect')
      fullName1.value = user.firstName + '-' + user.lastName
    }) 

    return {
      user,
      fullName1
    }
  }
}
</script>

<style scoped>
/* 样式部分可以根据需要添加 */
</style>
特性watchEffectwatch
依赖收集方式自动收集所有回调中的响应式依赖显式指定要监听的源
立即执行立即执行回调默认不立即执行(可配置 immediate)
返回值处理无法直接访问变化前后的值可以获取新旧值
调试信息调试信息较少提供更丰富的调试信息
性能开销略高(自动依赖收集)略低(明确依赖关系)

动态依赖

每次运行时都会重新收集依赖 watch 是静态依赖

  • 条件依赖
ts
watchEffect(() => {
  if (condition.value) {
    console.log(activeData.value) // 条件为真时才依赖 activeData
  } else {
    console.log(backupData.value) // 条件为假时才依赖 backupData
  }
})

/**
当 condition 变化时,会重新执行并收集依赖
每次执行可能建立不同的依赖关系
*/
  • 动态依赖
ts
const activeTab = ref('users')
const users = ref([])
const products = ref([])
watchEffect(async () => {
  if (activeTab.value === 'users') {
    users.value = await fetchUsers()
  } else {
    products.value = await fetchProducts()
  }
})

/**
当 activeTab 从 'users' 变为 'products' 时:
移除对 users 的依赖
新增对 products 的依赖
*/

ref标签属性

绑定DOM或者组件

单个元素:先申明ref响应式数据,返回给模版使用,通过ref绑定数据

注意: 写法和vue2不同, 把一个使用ref声明的响应式数据绑定到标签的ref属性上, 使用时要用.value

vue
<template>
  <h2>App</h2>
  <input type="text">---
  <input type="text" ref="inputRef">
</template>

<script lang="ts" setup>
import { onMounted, ref } from 'vue'

/* 
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
*/

const inputRef = ref<HTMLElement | null>(null)

onMounted(() => {
  inputRef.value && inputRef.value.focus()
})
</script>

v-for中的ref

  • 遍历的元素:先定义一个空数组,定一个函数获取元素,返回给模版使用,通过 ref 绑定这个函数
  • 视图更新时, 要清空数组(在onBeforeUpdate )

https://v3.cn.vuejs.org/guide/migration/array-refs.html#frontmatter-title

js
<template>
  <h1>{{ number }}</h1>
  <button @click="number++">加油~</button>
  <!-- 获取多个DOM节点ref属性需要绑定一个函数 -->
  <div v-for="i in 4" :key="i" :ref="setItemRef">{{ i }}</div>
</template>

<script lang="ts" setup>
import { onBeforeUpdate, onUpdated, ref } from 'vue'

const number = ref(0)
// 用于接收 dom 节点的数组
let itemRefs: HTMLElement[] = []

// 在函数形参中获取到真实 DOM 节点
const setItemRef = (el: HTMLElement | null) => {
  if (el) {
    itemRefs.push(el)
  }
}

// 💥注意:视图更新的时候,ref 属性绑定的函数会被多次触发,所以需要在更新时重置数组
onBeforeUpdate(() => {
  console.log('视图更新了')
  itemRefs = []
})

onUpdated(() => {
  console.log(itemRefs)
})
</script>

自定义hook函数

自定义hook的作用类似于vue2中的mixin技术 使用Vue3的组合API封装的可复用的功能函数 文件命名时最好在前面加上use 在setup中能使用的函数,在hook函数中也都可以使用

和mixin的区别

  • mixin中的变量和方法是隐式引入(在mixinsp配置中引入), 而hook是要主动调用
  • 在一个组件中使用多个mixin可能会出现,函数和变量重名现象,就会导致冲突或覆盖现象。而使用Hook函数时,因为变量和函数是显示引用,我们就可以通过解构赋值,来避免函数和变量重名现象。
  • mixins不能传入参数改变它的逻辑

例子1: 收集用户鼠标点击的页面坐标

hooks/useMousePosition.ts

ts
import { ref, onMounted, onUnmounted } from 'vue'
/* 
收集用户鼠标点击的页面坐标
*/
export default function useMousePosition () {
  // 初始化坐标数据
  const x = ref(-1)
  const y = ref(-1)

  // 用于收集点击事件坐标的函数
  const updatePosition = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 挂载后绑定点击监听
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })

  // 卸载前解绑点击监听
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })

  return {x, y}
}

使用:

vue
<template>
<div>
  <h2>x: {{x}}, y: {{y}}</h2>
</div>
</template>

<script setup lang="ts">
/* 
在组件中引入并使用自定义hook
自定义hook的作用类似于vue2中的mixin技术
自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
*/
import useMousePosition from './hooks/useMousePosition'
const {x, y} = useMousePosition()
</script>

v-model

官方文档

变化的总体概述:

  • 非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
    • prop:value -> modelValue
    • 事件:input -> update:modelValue
  • 非兼容v-bind.sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替;
  • 新增:现在可以在同一个组件上使用多个 v-model 绑定;
  • 新增:现在可以自定义 v-model 修饰符。

ps: vue2.2 引入 model 组件选项, 但只允许在组件上使用一个 v-model

prop 和事件名发生变化

父组件

vue
<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->

<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

子组件

vue
<script setup lang="ts">
// 使用defineProps和defineEmits来声明props和emits
const props = defineProps<{
  // 使用TypeScript类型注解确保类型安全
  modelValue: boolean
}>()

// 定义emits类型,确保事件类型安全
const emit = defineEmits<{
  // 使用精确的事件类型定义
  (e: 'update:modelValue', value: boolean): void
}>()

// 示例方法:处理值变化
const handleChange = (val: boolean) => {
  // 严格类型检查的emit调用
  emit('update:modelValue', val)
}
</script>

自定义model名(可以用来替代原来的.sync语法糖)

父组件

vue
<InputComponent v-model:input="text" />

子组件

js
<template>
  输入: <input type="text" :value="input" @change='textChange'/>
</template>

<script  setup lang="ts">
import { ref, onMounted } from 'vue'
const props = defineProps<{
  input: string, // 1.定义props
}>()

const emit = defineEmits<{
  (e:'update:input', values: string): void  // 2. 定义emit
}>()

const textChange = (e)=>{
  let value = e.target.value
  emit('update:input', value) // 3.发送emit
}
</script>

自定义修饰符

官方文档

ps: 内置的有.trim, number, .lazy

添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件 ( 如果v-model 自定义model名, prop 名为自定义名 + Modifiers )

  • 例子: 把v-model的输入首字母变大写

    父组件

vue
<ChildComponent v-model.capitalize="myText"></ChildComponent>

子组件

vue
<script lang="ts" setup>
import { defineProps, defineEmits, onBeforeMount } from 'vue'

/*
	name: 'ChildComponent',
*/

// 1. 声明 props(包括修饰符)
const props = defineProps<{
	modelValue: boolean,
	modelModifiers: Record<string, boolean> // 接收修饰符号,如 { capitalize: true }
}>()

// 声明 emits
const emit = defineEmits<{
	(e: 'update:modelValue', value: any): void
}>()

/*
	1. 当组件的 created 生命周期钩子触发时,modelModifiers prop 会包含 capitalize,且其值为 true
*/
onBeforeMount(() => {
	console.log(props.modelModifiers) // { capitalize: true }
})

/*
	2. 用法就是先判断修饰符的值是不是为 true,是的话就处理数据
*/
let val = 'hello'
if (props.modelModifiers.capitalize) {
	val = val.charAt(0).toUpperCase() + val.slice(1)
}

emit('update:modelValue', val)
</script>
  • 例子: 自定义model名 + 自定义修饰符号

    父组件

vue
<InputComponent v-model:input.addPlus="text" />

子组件

vue
<template>
	输入: <input type="text" :value="input" @change='textChange'/>
</template>

<script  setup lang="ts">
import { ref, onMounted } from 'vue'
const props = defineProps<{
	input: string,
	inputModifiers: {  // '自定义model名' + 'Modifiers'
		default: ()=>{}
	}
}>()

const emit = defineEmits<{
	(e:'update:input', value: string): void
}>()

console.log()

const textChange = (e)=>{
	let value = e.target.value
	if(props.inputModifiers.addPlus){ // 判断是否存在自定义修饰符
		 if(value.charAt(0) !== '+'){
			 value = '+' + value
		 }
	}
	emit('update:input', value)
}

onMounted(()=>{
	console.log('modelModifiers',props.modelModifiers)
})
</script>

<style scoped>
</style>

依赖注入

无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。 这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。 ==在 <script setup> 顶级作用域使用==

可以将依赖注入看作是"长距离的 prop",除了:

  • 父组件不需要知道哪些子组件使用了它 provide 的 property
  • 子组件不需要知道 inject 的 property 来自哪里

语法

ts
// 祖父组件
import { provide } from 'vue'
// provide 有两个参数: 第一个name (<String> 类型), 第二个value
// 如果要传递多个, 包装成对象放到第二个参数里, 不建议多次provide
provide('data', {
  name: '张三',
  age: 12
})
ts
// 孙组件
import { inject } from 'vue'
// inject有两个参数: 第一个name, 第2个为默认值(可选)
const data = inject('data')

添加响应性

在 provide 值时使用 refreactive

修改响应式 property

  • 尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部
  • 如果需要在孙组件更新数据, 建议在祖父组件provide一个更新的方法(==该方法的作用域在祖父组件==)
  • 如果要保证provide的数据不被修改, 使用readonly

祖父组件

ts
import { provide, reactive, readonly } from 'vue'
const userInfo = reactive({
	name: '张三',
	age: 12
})

const env = '祖父组件'

const updateUserInfo = ({name, age})=>{
	console.log('provide方法的作用域', env) // provide的方法的作用域在组父组件, 这里打印的env是'组父组件'
	if(name){
		userInfo.name = name
	}
	if(age !== undefined){
		userInfo.age = age
	}
}

provide('userInfo',readonly(userInfo))
provide('updateUserInfo', updateUserInfo)

孙组件

vue
<template>
	<div>孙组件</div>{{userInfo2.name}}{{userInfo2.age}}岁
	<br/>
	<button style="width:100px;height:50px;" @click="ageAddOne">年龄+1</button>
</template>

<script  setup lang="ts">
	import { inject } from 'vue'
	const userInfo2 = inject('userInfo')
	const updateUserInfo = inject('updateUserInfo')
	const env = '孙组件'
	const ageAddOne = ()=>{
		// reactive设置了readonly, 直接修改不成功报错
		// userInfo2.age += 1

		// 需要通过父组件provide传来的方法修改
		updateUserInfo({
			age:userInfo2.age + 1
		})

	}
</script>

新组件

Fragment(片段)

  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级, 减小内存占用
vue
<template>
    <h2>aaaa</h2>
    <h2>aaaa</h2>
</template>

vue 2 必须添加一个根标签把这两个 h2 包住

Teleport 传送门

一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。

最常见的例子就是全屏的模态框

<Teleport> 接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue"把以下模板片段传送到 body 标签下"。

js
<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

Suspense(不确定的)

试验性特性

新指令

v-memo

记录指令下的模板树。该指令期望一个数组,如果数组内的值,都没有发生更新,那么指令下的模板树也不会发生更新。

官网提到v-memo仅用于性能关键场景中的微优化,一般使用到的地方应该是渲染大型v-for列表(其中length > 1000)。

vue
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>

v-if和v-for优先级

  • v-if优先v-for解析(和vue2不同)

常见错误

  1. 引入组件时, .vue后缀不能省略
  2. @ 路径不生效, 在tsconfig.json里添加配置
json
"compilerOptions": {
	//...
	"paths": {
			"@/*": [
				"./src/*"
			],
		},
	//...
}

vite.config.ts

ts
export default defineConfig({
 base: "./",
 resolve: {
	 alias: [
		 {
			 find: "@",
			 replacement: resolve(__dirname, "./src") // __dirname报错, 则需要安装 `@types/node` npm包
		 }
	 ]
 },
 // ...
})
  1. less支持
ts
// 1. 先安装less 和 less-loader
// 在vite.config.ts
export default defineConfig({
 css: {
	 preprocessorOptions: {
		 less: {
			javascriptEnabled: true
		 }
	 }
 },
 // ...
})

h 函数直接生成虚拟dom

待补充...

render函数渲染

待补充...

动态加载组件

import() 函数和 defineAsyncComponent

vue2 vue3 是怎么自动更新页面的

数据劫持/数据代理 + 依赖收集 + 派发更新

vue 2:

  • 数据劫持: 通过 Object.definedProperry() 为数据对象添加 getter 和 setter 方法, 当读取属性的时候, 会触发 getter 方法, 当修改属性值时, 会触发 setter 方法
  • 依赖收集: 在 getter 方法中会进行依赖收集. 当 Vue 渲染时, 会访问数据对象的属性, getter 方法会将当前的渲染函数和计算属性添加到一个 依赖列表
  • 派发更新: 当数据发生变化时, setter 方法会通知所有依赖该属性的地方进行更新. Vue 通过 Wathcher 观察者模式 实现

vue 3:

  • 数据代理: 通过 Proxy 代理整个对象, 无须递归劫持
  • 依赖收集: 也是在 getter 访问属性时收集依赖, 但是用 WeakMap (key 是对象, 可以被自动回收) + Map 存储依赖关系
  • 派发更新: setter 方法会调用 trigger, 遍历 effect(类似 Vue 2 的 Watcher), 通过调度器(Scheduler)执行
对比项Vue 2 (Object.defineProperty)Vue 3 (Proxy + WeakMap)
劫持方式递归使用 Object.defineProperty 劫持每个属性使用 Proxy 一次性劫持整个对象
动态属性监听❌ 新增属性无法响应(需 Vue.set✅ 动态添加/删除属性也能被监听
依赖收集getter 中手动调用 Dep 收集依赖利用 WeakMap + Map 存储依赖关系,结构更清晰
依赖存储结构每个属性有自己的 Dep,记录对应 Watcher中心化的 targetMap: WeakMap -> Map -> Set 存储结构
派发更新setter 中触发对应 Watcher.update()使用 trigger(target, key) 精准触发 effect 函数
数组监听重写数组方法(如 pushpop)手动触发更新Proxy 能直接监听数组索引和长度变化
性能优化初始化时递归全对象,成本高按需懒劫持,只在访问时创建响应式,性能更高
调度机制借助 nextTick() 实现异步更新提供内建的 scheduler 参数,可自定义任务调度
内存管理手动管理引用关系WeakMap 自动管理依赖,对象无引用时自动回收

vue 2 和 vue 3 diff 算法的区别

  • vue 2 基于标签和 key 识别节点, 需要 递归遍历 比较所有节点
  • vue 3 增加了 静态标记, 静态节点会被跳过, 动态节点会标记哪里会变
  • vue 2 使用 双端比较算法, 需要同时比较新旧虚拟节点的头部和尾部
  • vue 3 先排除首尾不变的部分, 对于变化部分, 使用基于 最长递增子序列算法 . 举例:
A-B-C-D  变成 A-C-B-D
1. 会先去掉首尾不变的部分, 即 B-C-D 变成 C-B-D
2. 基于最新的节点, 生成序列 [0, 1, 2], 对应旧节点, 那么序列为 [1, 0, 2]
3. 最长递增子序列是 [0, 2], 只需要调整 1 的位置就行
优化点Vue 2Vue 3
静态节点优化无优化,全部重新渲染编译时标记静态节点,跳过不变的部分
VNode Patch递归比对所有子节点patchFlag 标记动态部分,跳过静态内容
列表 Diff(v-for)O(n) 复杂度遍历整个 children最长递增子序列(LIS),优化 DOM 操作
Fragment 片段需要额外的 div 作为根节点允许多个根节点,减少无意义 Diff

手写api

shallowReactive 与 reactive

js
const reactiveHandler = {
  get (target, key) {

    if (key==='_is_reactive') return true

    return Reflect.get(target, key)
  },

  set (target, key, value) {
    const result = Reflect.set(target, key, value)
    console.log('数据已更新, 去更新界面')
    return result
  },

  deleteProperty (target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('数据已删除, 去更新界面')
    return result
  },
}

/* 
自定义shallowReactive
*/
function shallowReactive(obj) {
  return new Proxy(obj, reactiveHandler)
}

/* 
自定义reactive
*/
function reactive (target) {
  if (target && typeof target==='object') {
    if (target instanceof Array) { // 数组
      target.forEach((item, index) => {
        target[index] = reactive(item)
      })
    } else { // 对象
      Object.keys(target).forEach(key => {
        target[key] = reactive(target[key])
      })
    }

    const proxy = new Proxy(target, reactiveHandler)
    return proxy
  }

  return target
}

/* 测试自定义shallowReactive */
const proxy = shallowReactive({
  a: {
    b: 3
  }
})

proxy.a = {b: 4} // 劫持到了
proxy.a.b = 5 // 没有劫持到

/* 测试自定义reactive */
const obj = {
  a: 'abc',
  b: [{x: 1}],
  c: {x: [11]},
}

const proxy = reactive(obj)
console.log(proxy)
proxy.b[0].x += 1
proxy.c.x[0] += 1

shallowRef 与 ref

ts
/*
自定义shallowRef
*/
function shallowRef(target) {
  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    }
  }

  return result
}

/* 
自定义ref
*/
function ref(target) {
  if (target && typeof target==='object') {
    target = reactive(target)
  }

  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    }
  }

  return result
}

/* 测试自定义shallowRef */
const ref3 = shallowRef({
  a: 'abc',
})
ref3.value = 'xxx'
ref3.value.a = 'yyy'

/* 测试自定义ref */
const ref1 = ref(0)
const ref2 = ref({
  a: 'abc',
  b: [{x: 1}],
  c: {x: [11]},
})
ref1.value++
ref2.value.b[0].x++
console.log(ref1, ref2)

shallowReadonly 与 readonly

js
const readonlyHandler = {
  get (target, key) {
    if (key==='_is_readonly') return true

    return Reflect.get(target, key)
  },

  set () {
    console.warn('只读的, 不能修改')
    return true
  },

  deleteProperty () {
    console.warn('只读的, 不能删除')
    return true
  },
}

/* 
自定义shallowReadonly
*/
function shallowReadonly(obj) {
  return new Proxy(obj, readonlyHandler)
}

/* 
自定义readonly
*/
function readonly(target) {
  if (target && typeof target==='object') {
    if (target instanceof Array) { // 数组
      target.forEach((item, index) => {
        target[index] = readonly(item)
      })
    } else { // 对象
      Object.keys(target).forEach(key => {
        target[key] = readonly(target[key])
      })
    }
    const proxy = new Proxy(target, readonlyHandler)

    return proxy 
  }

  return target
}

/* 测试自定义readonly */
/* 测试自定义shallowReadonly */
const objReadOnly = readonly({
  a: {
    b: 1
  }
})
const objReadOnly2 = shallowReadonly({
  a: {
    b: 1
  }
})

objReadOnly.a = 1
objReadOnly.a.b = 2
objReadOnly2.a = 1
objReadOnly2.a.b = 2

isRef, isReactive 与 isReadonly

js
/* 
判断是否是ref对象
*/
function isRef(obj) {
  return obj && obj._is_ref
}

/* 
判断是否是reactive对象
*/
function isReactive(obj) {
  return obj && obj._is_reactive
}

/* 
判断是否是readonly对象
*/
function isReadonly(obj) {
  return obj && obj._is_readonly
}

/* 
是否是reactive或readonly产生的代理对象
*/
function isProxy (obj) {
  return isReactive(obj) || isReadonly(obj)
}

/* 测试判断函数 */
console.log(isReactive(reactive({})))
console.log(isRef(ref({})))
console.log(isReadonly(readonly({})))
console.log(isProxy(reactive({})))
console.log(isProxy(readonly({})))