# Vue3 分享

# 自我介绍

6年多的工作经验,上一份工作在万科物业工作了四年半,主要从事前端开发工作,擅长小程序的开发爬坑和前端性能优化。

喜欢阅读和健身,喜欢挑战自己并不断学习新知识,今年的小目标是完成一次半程马拉松。

# 基础

# vue3 简介

Vue3 是一款渐进式的JavaScript框架,用于构建用户界面。Vue3 是 Vue2 的升级版本,具有更好的性能、更小的体积、更好的 TypeScript 支持等优点。

# 响应式

# vue2.x的响应式

  • 实现原理:

    • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。

      Object.defineProperty(data, 'count', {
        get () {}, 
        set () {}
      })
      
    • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

  • 存在问题:

    • 新增属性、删除属性, 界面不会更新。
    • 直接通过下标修改数组, 界面不会自动更新。

9B386C0A-5C60-47D9-B9FE-E51531A15F46.png

# Vue3.0的响应式

  • 实现原理:
    • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
    • 通过Reflect(反射): 对源对象的属性进行操作。
    • MDN文档中描述的Proxy与Reflect:
      • Proxy (opens new window)

      • Reflect: (opens new window)

        new Proxy(data, {
         // 拦截读取属性值
            get (target, prop) {
             return Reflect.get(target, prop)
            },
            // 拦截设置属性值或添加新属性
            set (target, prop, value) {
             return Reflect.set(target, prop, value)
            },
            // 拦截删除属性
            deleteProperty (target, prop) {
             return Reflect.deleteProperty(target, prop)
            }
        })
        
        proxy.name = 'tom'   
        

3DC041E0-2DF3-47B7-8F19-A2FF77C944C9.png

# ref

  • 作用: 定义一个响应式的数据
  • 语法: const xxx = ref(initValue)
    • 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)。
    • JS中操作数据: xxx.value
    • 模板中读取数据: 不需要.value,直接:<div>{xxx}</div>
function ref<T>(value: T): Ref<UnwrapRef<T>>

interface Ref<T> {
  value: T
}
//定义
const name = ref('')
const arr = ref([])
const obj = ref({})
// 使用
name.value

# reactive

  • 作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用ref函数)
  • 语法:const 代理对象= reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象
  • reactive定义的响应式数据是“深层次的”。
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

# reactive对比ref

  • 从定义数据角度对比:
    • ref用来定义:基本类型数据。
    • reactive用来定义:对象(或数组)类型数据。
    • 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象。
  • 从使用角度对比:
    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据:均不需要.value

# watch

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

Vue SFC Playground (opens new window)


<script setup>
import { ref, watch, reactive } from 'vue'
const log = ref('')
const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log('单个 ref:', `x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log('// getter 函数:', `sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log('多个来源组成的数组:', `x is ${newX} and y is ${newY}`)
})

const obj = reactive({ count: 0 })
// 响应式对象
watch(obj, (newValue, oldValue) => {
  console.log('响应式对象:', newValue, oldValue)
})

const objRef = ref({ count: 0 })
console.log(objRef.value)
// 问题:打印的结果是什么
watch(objRef, (newValue, oldValue) => {
  console.log('响应式ref对象:', newValue, oldValue)
})
const refCount = () => {
  objRef.value.count++
}
</script>

<template>
  <button @click="() => x++">x++</button>
  <button @click="() => y++">y++</button>
  <button @click="() => obj.count++">count++</button>
  <button @click="refCount">ref count++</button>
</template>


# watchEffect

  • watch的套路是:既要指明监视的属性,也要指明监视的回调。

  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。

  • watchEffect有点像computed:

    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
const todoId = ref(1)
const data = ref(null)

watch(todoId, async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
}, { immediate: true })

const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

# 深入组件

# 注册

  • 全局注册 app.component(name,component)

interface App {
  component(name: string): Component | undefined
  component(name: string, component: Component): this
}
  • 局部注册

    • 在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册
    • 如果没有使用 <script setup>,则需要使用 components 选项来显式注册
  • 在element-plus中的应用

  1. 组件目录下有一个index.ts 负责组件导出

import { withInstall } from '@element-plus/utils'
import table from './src/table.vue'

export const Table = withInstall(table)
export default Table

export * from './src/table'

// 为组件添加intall函数
export const withInstall = <T, E extends Record<string, any>>(
  main: T,
  extra?: E
) => {
  ;(main as SFCWithInstall<T>).install = (app): void => {
    for (const comp of [main, ...Object.values(extra ?? {})]) {
      app.component(comp.name, comp)
    }
  }

  if (extra) {
    for (const [key, comp] of Object.entries(extra)) {
      ;(main as any)[key] = comp
    }
  }
  return main as SFCWithInstall<T> & E
}

在整体组件库安装到框架的时候,遍历所有组件,注册全局组件。

// 在哪调用install呢
components.forEach((c) => app.use(c))

# props

  • 单项数据流

    所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

    • 导致你想要更改一个 prop 的需求通常来源于以下两种场景

      1. prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性
      
        const props = defineProps(['initialCounter'])
      
        // 计数器只是将 props.initialCounter 作为初始值
        // 像下面这样做就使 prop 和后续更新无关了
        const counter = ref(props.initialCounter)
      
      
      1. 需要对传入的 prop 值做进一步的转换
        const props = defineProps(['size'])
      
        // 该 prop 变更时计算属性也会自动更新
        const normalizedSize = computed(() => props.size.trim().toLowerCase())
      
      
    • Prop 校验

      defineProps({
        // 基础类型检查
        // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
        propA: Number,
        // 多种可能的类型
        propB: [String, Number],
        // 必传,且为 String 类型
        propC: {
          type: String,
          required: true
        },
        // Number 类型的默认值
        propD: {
          type: Number,
          default: 100
        },
        // 对象类型的默认值
        propE: {
          type: Object,
          // 对象或数组的默认值
          // 必须从一个工厂函数返回。
          // 该函数接收组件所接收到的原始 prop 作为参数。
          default(rawProps) {
            return { message: 'hello' }
          }
        },
        // 自定义类型校验函数
        propF: {
          validator(value) {
            // The value must match one of these strings
            return ['success', 'warning', 'danger'].includes(value)
          }
        },
        // 函数类型的默认值
        propG: {
          type: Function,
          // 不像对象或数组的默认,这不是一个
          // 工厂函数。这会是一个用来作为默认值的函数
          default() {
            return 'Default function'
          }
        }
      })
      
      1. 基于运行时声明 复杂props类型
      
      import type { PropType } from 'vue'
      
      const props = defineProps({
        book: Object as PropType<Book>
      })
      
      
      1. 基于类型的声明:泛型参数来定义 props 的类型
          <script setup lang="ts">
            interface Book {
              title: string
              author: string
              year: number
            }
      
            const props = defineProps<{
              book: Book
            }>()
          </script>
      

# v-model

需要实现这样一个组件需要怎么写? <CustomInput v-model="searchText" />

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

🤔思考: 如果我在外层在嵌入一套model怎么实现

答案 (opens new window)

<ParentInput v-model="searchText" />

// ParentInput
<template>
  <CustomInput v-model="searchText" />
</template>

如果都是用 v-model (opens new window)

# 透传

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id。

  • 当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上:<table-column>
  • 多根节点的 Attributes 继承:和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。<table>

# 插槽

# 作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

试一试 (opens new window)

# 依赖注入

Prop 逐级透传问题

虽然这里的 <Footer> 组件可能根本不关心这些 props,但为了使 <DeepChild> 能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况

带有响应性的 provide + inject 完整示例 (opens new window)