vue3快速上手

907 阅读13分钟

1 组合式


<template>
<div>
    <div>{{ state.count }}</div>
    <div @click="changeMsg">{{msg}}</div>
</div>
</template>

import { reactive, ref, defineComponent  } from 'vue'

export default {
  // 要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回
  setup() {

    // reactive: 用于定义引用类型的响应式对象
    const state = reactive({ count: 0 })

    // ref:可以定义基本烈性响应式变量(也可以定义引用类型的)
    const msg = ref('hello world')


    const changeMsg = ()=>{
      //ref定义的变量操作时,需要使用.value访问;但是在模板中会自动解包,所以在模板中不需要.value
      msg.value = 'hello juejin'
    }

    function increment() {
      state.count++
    }

    // 不要忘记同时暴露变量和函数到模板(如果嫌弃手动返回麻烦,可以采用setup语法糖)
    return {
      state,
      increment,
      msg,
      changeMsg
    }
  }
}

2. setup

2.1 setup语法糖

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。我们可以使用下面方式来大幅度地简化代码

<script setup>

<!-- 留意这里setup写法 -->
<script setup>
import { reactive, nextTick } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
  // 需要更新后访问使用nextTick,注意需要导入
  nextTick(() => {
    // 访问更新后的 DOM
  })
}

// setup语法糖这里无需返回变量和方法
</script>

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

2.2 setUp参数

setup中没有绑定this,所以setup接受2个参数,props和context

<template>
<div>
    <div>{{ state.count }}</div>
    <div @click="changeMsg">{{msg}}</div>
</div>
</template>

<script>
import { reactive, ref, defineComponent  } from 'vue'

export default {
  setup(props, context) {
    const state = reactive({ count: 0 })
    const msg = ref('hello world')
    const changeMsg = ()=>{
      msg.value = 'hello juejin'
    }
    const increment = () => state.count++
    return {
      state,
      increment,
      msg,
      changeMsg
    }
  }
}
</script>

3. 响应式代理vs原始对象

3.0 响应式案例

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

const msg = ref('Hello World!')
msg.value = {name: 'hello'}
let test = reactive({testname: 'world'})
test = {testage: 30}
msg.value = []

// 测试1:
setTimeout(() => {
msg.value = {'age': 10} // 依然触发
msg.value.push(1)  //依然触发
},0)

// 测试2
setTimeout(() => {
test = {testage: 60} // 不触发;但是数据已经在内存,只是没有刷到页面;
},0)

// 测试2
setTimeout(() => {
msg.value = {'age': 10} // 依然触发
test = {testage: 60} // 触发,由于ref会触发,所以reactive也会被刷到页面
},0)



// 测试3
let testReactive = reactive({data: testname: 'world'})
setTimeout(() => {
testReactive.data = {testage: 60} // 可以触发(所以reactive如果想要触发响应式,必须在内部再定义一个属性,然后修改这个属性就可以触发响应式;又或者和ref属性连带着一起修改就会被强制刷到页面)
},0)
</script>

<template>
  <h1>{{ msg }}</h1>
  <h1>{{ test }}</h1>
  <input v-model="msg" />
</template>

3.1 reactive

reactive基本使用:

import {reactive} from 'vue'

const book = reactive({title: 'VUE3'})

reactive集合ts泛型(不支持<>泛型,只支持类型注解方式):

import {reactive} from 'vue'

interface IBook {
    title: string
}

const book: IBook = reactive({title: 'VUE3'})

官方提示:不支持reactive<IBook>

image.png

主要原因是:reactive带有深层次的 ref时,我们如果通过泛型来约束类型,类型是会对应不上的!

interface IForm{
    name: string
}

const name = ref('名字')

const from = reactive<IForm>({
    name
})

此时会报错:Type 'Ref<string>' is not assignable to type 'string'

我们可以修改IForm中name的类型为Ref; 但IForm自动推导出的name的类型确实string,和Ref不匹配。



reactive的响应式:

reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的

const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本.


为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

依靠深层响应性,响应式对象内的嵌套对象依然是代理:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

局限性:

  1. 仅对引用类型有效
  2. 解构或者赋值或者传值都将失去响应式
// 示例1:
let state = reactive({ count: 0 })

// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })



// 示例2:
const state = reactive({ count: 0 })

// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++

// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)

3.2 ref

ref() 让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用

ref基本使用:

import { ref } from 'vue'

// 推导出的类型:Ref<number>
const year = ref(2020) 

// => TS Error: Type 'string' is not assignable to type 'number'. 
year.value = '2020'

ref结合ts泛型使用:

import { ref } from 'vue'
import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')
year.value = 2020 // 成功!

// 或者

const year = ref<string | number>('2022')
year.value = 2022 // 成功

如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型

// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()

3.3 readonly

传给子组件的是readonly的响应式对象,而不是readonly的普通对象

4. computed

computed() 方法期望接收一个 getter函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value

<script setup>
// 注意computed的导入
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

computed泛型:

const double = computed<number>(() => {
    // 若返回值不是 number 类型则会报错
})    

5. watch

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

5.1 ref监听

注意点:watch的ref的new和old是字面值而不是ref对象,打印直接是值本身;因为源码中判断是ref直接返回的.value

// 示例1:
export default defineComponent({
  setup() {
    const a = ref(1)
    watch(a, (newValue, oldValue) => {
     console.log(`a从${oldValue}变成了${newValue}`) // 值是1
    })
  },
});


// 示例2:
const x = ref(0)
const y = ref(0)

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

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

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

5.2 reactive监听

直接对reactive对象监听;reactive的监听对象的new和old值是相等的对象;因为源码中如果是reactive监听则直接返回原source,并且开启deep深层次监听

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})
obj.count++

注意,你不能直接侦听响应式对象的属性值:

const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})


// 这里需要用一个返回该属性的 getter 函数:提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)


多输入源传入:

// 数组解构
const info = reactive({ name: "why", age: 18 })
const name = ref("john")

watch([info, name], (newValue, oldValue) => {
    // 此时newValue是个数组[proxy, "john"]
    // oldValue是个数组[proxy, "john"]
    console.log(newValue, oldValue) 
})


// 数组解构
const info = reactive({ name: "why", age: 18, friend: {
    name: "haha"
}})
const name = ref("john")

// 解构的info对象不具有响应式;加了deep之后有响应式了
watch(() => ({...info}), (newInfo, oldInfo) => {
    console.log(newInfo, oldInfo)
    
},{
    deep: true, // 如果这里加了true后, friend就变成proxy,如果需要friend,则继续解构info
    immediate: true
})

chanegInfo = () => {
    info.freind.name ="wwww"
} 

5.3 深层监听器

一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

// 可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

5.4 watchEffect()

watchEffect会自动收集自身参数回调中的可响应式的依赖(watch需要手动指定监听依赖);并且开始时就会执行一次,后面随着收集的依赖的变化继续执行

5.4.1 基本使用

computed和watch所依赖的数据必须是响应式的。Vue3引入了watchEffect,watchEffect 相当于将 watch 的依赖源和回调函数合并,当任何你有用到的响应式依赖更新时,该回调函数便会重新执行。不同于 watch的是watchEffect的回调函数会被立即执行,即({ immediate: true })



首先刚进入页面就会执行watchEffect中的函数打印出:0,随着定时器的运行,watchEffect监听到依赖数据的变化回调函数每隔一秒就会执行一次

<template>
  <div>{{ watchTarget }}</div>
</template>
<script setup>
    import { watchEffect,ref } from "vue";
    const watchTarget = ref(0)
    watchEffect(()=>{
    console.log(watchTarget.value)
    })
    setInterval(()=>{
    watchTarget.value++
    },1000)
</script>

5.4.2 停止监听

停止监听:比如达到某一种边界后就不需要再执行监听回调。

watchEffect的返回值是一个停止监听函数,达到边界之后,可以调用stop函数停止监听回调。

<template>
  <div @click="addChange">{{ watchTarget }}</div>
</template>
<script setup>
    import { watchEffect,ref } from "vue";
    const watchTarget = ref(0)
    // watchEffect的返回值stop是一个停止监听的函数
    const stop = watchEffect(()=>{
        console.log(watchTarget.value)
    })
    const addChange = () => {
        if(watchTarget.value > 20){
            // 大于20停止监听回调的执行
            stop()
        }
    }
</script>

5.4.3 清除副作用

场景:比如watch中发送网络请求,但是watch中的响应式变量发生了变化,那么上一次的基于响应式旧值得网络请求就是副作用了,目前要基于新的响应式变量发送网络请求

watchEffect回调接受的一个参数onInvalidate,onInvalidate也是一个函数,并且onInvalidate的参数是一个回调。

<template>
  <div @click="addChange">{{ watchTarget }}</div>
</template>
<script setup>
    import { watchEffect,ref } from "vue";
    const watchTarget = ref(0)
    // stop是停止监听的函数
    const stop = watchEffect((onInvalidate) => {
        // 因为watchTarget每次发生变化,watchEffect都会执行,所以onInvalidate也会执行。
        onInvalidate(() => {
            // 在这个函数中清楚额外的副作用
            // request.cancel()
            console.log("onInvalidate")
        })
        console.log(watchTarget.value)
    })
    const addChange = () => watchTarget.value++
</script>

5.4.4 watchEffect的执行时机

watchEffect的第二个参数是watchEffect的执行时机对象,属性是flush取值如下:

  • pre(默认值,就是在dom挂载之前就会执行一次,如果dom挂载完成了,那么对应的依赖值从没有到有则watchEffect还会调用一次)
  • post(执行时机是dom挂载完成后执行watchEffect)
  • sync

场景:比如想通过ref获取dom,但不使用onMounted钩子。

<template>
  <div ref="divRef" @click="addChange">{{ watchTarget }}</div>
</template>
<script setup>
    import { watchEffect,ref } from "vue";
    const divRef = ref(null)
    const watchTarget = ref(0)
    const stop = watchEffect(() => {
        console.log(watchTarget.value)
        console.log(divRef.value) // 打印出div的dom节点实例
    }, {
        flush: 'post' // 表明会在dom挂载完成后执行watchEffect回调
    })
    const addChange = () => watchTarget.value++
</script>

6. ref模板引用

6.1 基本模板引用

为了通过组合式 API 获得该模板引用,我们需要声明一个同名的 ref:

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

    // 声明一个 ref 来存放该元素的引用
    // 必须和模板里的 ref 同名
    const input = ref<HTMLInputElement | null>(null)
    onMounted(() => {
        // input.value就是input元素的dom实例; 留意可选连,因为有可能ref没有值
        input.value?.focus() 
    })
    </script>

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

6.2 v-for的ref模板引用

v-for绑定模板引用(需要 v3.2.25 及以上版本):当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素

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

const list = ref([
  /* ... */
])

const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">
      {{ item }}
    </li>
  </ul>
</template>

6.3 ref组件模板引用

有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 MyModal 子组件,它有一个打开模态框的方法

<!-- MyModal.vue --> 
<script setup lang="ts">
    import { ref } from 'vue'
    const isContentShown = ref(false)
    const open = () => (isContentShown.value = true)
    // 如果外部使用ref方式调用open方法,则在MyModal中必须使用defineExpose,将open方法暴露出去
    defineExpose({ open })
</script>    

为了获取 MyModal 的类型,我们首先需要通过 typeof 得到其类型,再使用 TypeScript 内置的 InstanceType 工具类型来获取其实例类型:

    const modal = ref<InstanceType<typeof MyModal> | null>(null)
    const openModal = () => { modal.value?.open() }

7. 生命周期

8. 动态组件

<el-tabs v-model="activeTabName" class="tabs" @tab-click="handleTabClick">
  <el-tab-pane
    v-for="tabItem of tabInfoConfig"
    :key="tabItem.name"
    :label="tabItem.label"
    :name="tabItem.name"
  >
    <component :is="tabItem.component"></component>
  </el-tab-pane>
</el-tabs>
 

export const tabInfoConfig: ITabDataType[] = [
  {
    name: "baseInfo",
    label: "基本信息",
    component: defineAsyncComponent(
      () => import("@/views/resource/manage/cpns/BaseInfoDetail.vue")
    )
  },
  {
    name: "followUpRecord",
    label: "记录",
    component: defineAsyncComponent(
      () => import("@/views/resource/manage/cpns/FollowUpRecord.vue")
    )
  },
  {
    name: "memorabilia",
    label: "节点",
    component: defineAsyncComponent(
      () => import("@/views/resource/manage/cpns/Memorabilia.vue")
    )
  }
];

9. 组件通信(setup语法糖模式)

9.1 props && emit

props基本使用接受传值:

const props = defineProps({
  businessRowId: {
    type: Boolean,
    default: false
  },
  currentBusinessInfoRow: {
    type: Object,
    default: () => {}
  },
  foo: { type: String, required: true },
  bar: Number
});

props接受泛型类型:

const props = defineProps<{ foo: string bar?: number }>()

// 或者将props 的类型移入一个单独的接口中
interface Props {
    foo: string
    bar?: number
}
const props = defineProps<Props>()

接口或对象字面类型可以包含从其他文件导入的类型引用,但是,传递给 defineProps 的泛型参数本身不能是一个导入的类型

备注:目前defineProps的泛型类型不支持从外部导入(这是因为 Vue 组件是单独编译的,编译器目前不会抓取导入的文件以分析源类型。我们计划在未来的版本中解决这个限制)

import { Props } from './other-file'

defineProps<Props>() // 不支持!

当Props使用泛型类型接受传值时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults 编译器宏解决:

export interface IProps { msg?: string labels?: string[] }

const props = withDefaults(defineProps<IProps>(), {
    msg: 'hello',
    labels: () => ['one', 'two']
 })


emits发射事件之基本使用:

const emit = defineEmits(["handleSizeChange", "handleCurrentChange"]);

const sizeChange = (val: number) => emit("handleSizeChange", val);
const currentChange = (val: number) => emit("handleCurrentChange", val);

emits发射事件之ts泛型(更细粒度控制):

// 基于ts泛型类型
const emit = defineEmits<{
    (e: 'change', id: number): void
    (e: 'update', value: string): void
}>()

9.2 provide/inject

可以在祖孙之间传递属性

// 祖组件中
import { provide } from "vue";
provide("currentRowResourceInfo", resourceInfo);

// 孙组件中
import { inject } from "vue";
const currentBusinessInfoRow = inject<any>("currentBusinessInfoRow");

问题

1. vue3中深度修改样式问题

不能使用之前的::v-deep,需要使用:deep(选择器)

:deep(.el-breadcrumb__item .is-link) {
  font-family: PingFangSC-Regular;
  font-size: 14px;
  color: #86909c;
  line-height: 20px;
  font-weight: 400;
}

API

1. toRefs和toRef

一般直接对响应式对象解构之后,响应式对象解构是值赋值,则被赋值的变量不具有响应式;此时可以借助toRefs


<div @click="changeAge">{{age}}</div>

<script setup>
import { ref, reactive, toRefs } from 'vue'
// 示例1:
const state = reactive({name: 'john', age:18})
// 此时解构的name和age是不具有响应式的;而state仍然是响应式
let {name, age} = state

const changeAge = () => {
    age++; // 解构的age值会变,但是state保存的age值不变,页面age也不变,页面显示的解构的age不具有响应式
    state.age++; // state的age发生变化,但是解构age不变,页面age也不变,因为页面引用的不是state的age
}

</script>

如果想针对响应式变量解构,并且使得解构的变量也具有响应式,可以借助toRefs。

toRefs的原理是,使得解构的变量内部引用指向还是原响应式变量的ref。所以改变某一个都会变。


<div @click="changeAge">{{age}}</div>

<script setup>
import { ref, reactive, toRefs } from 'vue'
const state = reactive({name: 'john', age:18})
let {name, age} = toRefs(state) // 此时解构的name和age和state响应式中间建立的连接,将所有属性都转成ref建立连接;所以修改state,解构的name和age都会随之变化

const age = toRef(state, "age") // toRef只是针对响应式对象的某一个key属性建立ref连接;toRefs是针对所有属性。


const changeAge = () => {
    age++;
}

</script>

2. unRef

获取变量target值时,如果target是ref引用,则返回target.value,否则返回target

语法糖:

const target = unRef(target) ? target.value : target

const target = ref("why")
foo(target)
const foo = (bar) => {
    "why".value // 这种bar参数不确定是ref的响应式还是常规变量,所以不能确定是否用.value获取值

    const value = unRef(bar)
}

3. isRef

判断值是否是一个ref对象

4. shallowRef

创建一个浅层的ref对象

<div @click="changeAge">{{state}}</div>

<script setup>
import { ref, reactive, toRefs, shallowRef } from 'vue'
const state = ref({name: 'john', age:18})


const changeAge = () => {
    // state.vlaue = {name: 'bob', age:20} 一般这样改
    state.value.age++; // 这种是深层次修改;如果不希望这种深层次修改;可以使用shallowRef
}

</script>

使用shallowRef拒绝深层次修改:

<div @click="changeAge">{{state}}</div>

<script setup>
import { ref, reactive, toRefs, shallowRef } from 'vue'
const state = shallowRef({name: 'john', age:18}) // 使用shallowRef拒絕深層次修改


const changeAge = () => {
    state.value.age++; // shallowRef之后,这里不生效了,如果需要生效可手动使用triggerRef(state)触发
    // triggerRef(state)
}

</script>

5. triggerRef

手动触发和shallowRef相关联的副作用

参考

segmentfault.com/a/119000004…

segmentfault.com/a/119000004…