Vue3发布快两年半,我才用它完成了第一个项目——vue3+setup🔥

4,656 阅读11分钟

前言

距离 vue3 2020年09月18日正式发布已经过去快两年半了, 以前还能说是对新技术、新特性的观望,新技术用到生产会产生什么什么样不可预知的问题,但是经过两年半的沉淀该有的都有了,刚好也是有个新起的项目技术栈使用的是vue3,经过一个多月vue3的体验,对这个学习过程记录下来做个总结,也便于日后的梳理。

版本

既然要开始使用vue3,那就直接从最新的版本开始"vue": "^3.2.0"。3.2之后只需在script中添加setup,然后就可以在标签中直接使用vue3的组合式API,也不用写 setup 函数,响应式的属性和方法也不用 return 出来才能使用,像 refreactive这些方法通过 unplugin-auto-import 这个插件也不用每次都 import 导入进来,总之比最开始的时候在书写方面简洁了许多,要不怎么说是经过了两年半的沉淀呢。

准备工作

  1. 初始化 npm init vue@latest
  2. 安装 vscode 插件 Volar,注意 vue2项目中使用 Vetur,vue3中使用Volar
  3. 使用unplugin-auto-import 插件给每个.vue文件导入 vue组合式API
  4. 将浏览器的启用自定义格式设置工具勾选上 企业微信截图_16734207098350.png

企业微信截图_16734209963835.png

可以直观的看到响应式变量的值,调试的时候不用多点一次。

接下来让我们愉快的体验下vue3 setup,先用一张图感受一下组合式API的直观感受

v2-8f40f129a8f05f75f5238839f1714c5d_b (1).webp

认识 ref 系列

ref

ref: 接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value,所以访问的时候要通过 .value 的形式访问,在 <tamplate> 会自动解析,不需要 .value

const state = ref(1)
console.log('state---', state)
console.log('state.value---', state.value)

shallowRef

shallowRef: ref() 的浅层作用形式,只有对 .value 的访问是响应式的,和 ref() 不同的是不会被深层递归地转为响应式。

<template>
  <div>state:{{ state }}</div>
</template>
<script setup>
const state = shallowRef({ count: 1 })
// 不会触发视图更改
setTimeout(() => {
 state.value.count = 2
}, 2000)
// 会触发视图更改
setTimeout(() => {
  state.value = { count: 2 }
}, 2000)
</script>    

ref()shallowRef()不能同时使用,ref会调用triggerRefValue会更新视图,会导致shallowRef的数据一并被更新

triggerRef

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

const state1 = shallowRef({ count: 1 })
setTimeout(() => {
  // 不会触发更改
  state.value.count = 2 
  // 调用triggerRef会触发视图更改
  triggerRef(state)
}, 2000)

customRef

customRef: 创建一个自定义的 ref,提供的track, trigger这两个依赖的收集和触发方法,也是方便我们在任何地方调用,让 ref 根据自己的需求更加灵活,比如在 set 的时候加个防抖,一般 track 收集在 get 中调用,trigger 触发在 set 中调用,除非你有自己的想法。

<template>
  <div>name:{{ name }}</div>
  <button @click="change">修改 customRef</button>
</template>  

<script setup>
const myRef = value => {
  let timer
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newVal) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          console.log('触发了set')
          value = newVal
          trigger()
        }, 500)
      },
    }
  })
}
const name = myRef('测试')
const change = () => {
   name.value = '测试变了'
}
</script>

认识Reactive系列

reactive

reactive: 返回一个对象的响应式代理,用来绑定复杂的数据类型,不可以绑定普通的数据类型,相比 ref ,不用通过 .value 的方式来访问

let obj = reactive({ count: 0 })
obj.count = 2

关于数组异步赋值问题及解决方案,在开发中经常碰到异步赋值的问题,在vue3中不能通过像vue2那样直接用= 赋值的操作,这样会破坏数据原有的响应式,只能通过数据原有的方法,或者在它的外层再包裹一层结构来解决

let arr = reactive([])
setTimeout(() => {
  arr = [1, 2, 3] // 这样arr赋值会脱离响应式
  console.log(arr);
},1000)

// 解决办法:使用数组的push方法
setTimeout(() => {
  arr.push(...[1, 2, 3])
  console.log(arr)
}, 1000)

// 解决办法:再外层包裹一个对象
let arr = reactive({
  list: [],
})
setTimeout(() => {
  arr.list = [1, 2, 3]
  console.log(arr)
}, 1000)

shallowReactive

shallowReactive: reactive() 的浅层作用形式。和 reactive() 不同的是这里没有深层级的转换,一个浅层响应式对象里只有根级别的属性是响应式的,如果是深层的数据只会改变值,不会改变视图

<template>
  <div>shallowReactive:state:{{ state }}</div>
  <button @click="change1">test1</button>
  <button @click="change2">test2</button>
</template>  

<script setup>
let obj = {
  a: 1,
  first: {
    b: 2,
    second: {
      c: 3,
    },
  },
}
const state = shallowReactive(obj)
const change1 = () => {
  state.a = 7
}
const change2 = () => {
  state.first.b = 8
  state.first.second.c = 9
  console.log(state)
}
</script>

如上图所示,点击 test2 的时候虽然数据变了,但是视图并未更新,也不能像 shallowRef 那样通过 triggerRef 来解决视图更新,而且也不能和 reactive一起使用,一般只会在数据层级深且数据量大只使用顶级属性的时候才会用 shallowReactive 解决性能问题

认识to系列

toRef: 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。重点是基于响应式对象,用它包裹普通对象没有什么意义,虽然值会改变,但是是非响应式的。

const obj = reactive({
  foo: 2,
  bar: 1,
})
const barRef = toRef(obj, 'bar')
// obj响应式对象的bar属性 转化为响应式对象
const change = () => {
  barRef.value++
  console.log('obj', obj)
  console.log('barRef', barRef)
}
const change2 = () => {
  obj.bar++
  console.log('obj', obj)
  console.log('barRef', barRef)
}

toRefs

toRefs: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref,每个单独的 ref 都是使用 toRef() 创建的,可以帮我们批量创建ref来方便我们解构使用。

const state = reactive({
  foo: 1,
  bar: 2,
})
const stateAsRefs = toRefs(state)
const { foo, bar } = toRefs(state)
foo.value = 999
console.log('state:', state)
console.log('stateAsRefs:', stateAsRefs)
console.log('foo:', foo)
console.log('bar:', bar)

toRaw

toRaw: 根据一个 Vue 创建的代理返回其原始对象,将响应式对象转化为普通对象,这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,需谨慎使用。

const foo = {}
const reactiveFoo = reactive(foo)
reactiveFoo.a = 1
const toRawFoo = toRaw(reactiveFoo)
console.log('foo:', foo)
console.log('reactiveFoo:', reactiveFoo)
console.log('toRawFoo:', toRawFoo)
console.log('toRawFoo === foo:', toRawFoo === foo)

readonly和shallowReadonly

readonly

readonly: 接受一个对象 (不论是响应式还是普通的) 或是一个ref,返回一个原值的只读代理,强制更改会有警告。

const state = reactive({ count: 1 })
const copy = readonly(state)

copy.count++

shallowReadonly

shallowReadonly: readonly() 的浅层作用形式,只有根层级的属性变为了只读,深层次的属性修改不会有警告。

computed计算属性

computed: 就是当依赖的属性的值发生变化的时候,才会触发更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。它接受一个getter函数,返回一个只读的响应式ref对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

computed总的来说vue2和vue3的差别不是很大,就是写法有些区别。

let price = ref(1)
// 只读
let cPrice = computed(() => {
  return '¥' + price.value
})
console.log('cPrice:', cPrice.value)
// 可写
let cWritePrice = computed({
  get() {
    return '¥' + price.value
  },
  set(value) {
    price.value = 'set' + value
  },
})
cWritePrice.value = 2
console.log('price:', price.value)
console.log('cWritePrice:', cWritePrice.value)

watch侦听器

watch: 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

  1. 第一个参数是侦听器的源。这个来源可以是以下几种:
    • 一个函数,返回一个值

      const state = reactive({ count: 0 })
      watch(
        () => state.count,
        (newVal, oldVal) => {
          console.log(newVal, oldVal)
        }
      )
      
    • 一个 ref

      const count = ref(0)
      watch(count, (newVal, oldVal) => {
        console.log(newVal, oldVal)
      })
      
    • 一个响应式对象,侦听器会自动启用深层模式,但是无法正确获得oldValue!!会自动启用深层模式

      const state = reactive({
        foo: {
          a: 1,
        },
        bar: {
          b: 2,
        },
      })
      watch(state, (newVal, oldVal) => {
        console.log(newVal, oldVal) // 下图可以看出oldVal和newVal是一样的
      }, {
        // deep: true  // 自动启用深层模式
      })
      

    • 或是由以上类型的值组成的数组

      watch(
        [() => state.foo, () => state.bar],
        (newValue, oldValue) => {
          console.log(newValue, oldValue)
        },
        {
          deep: true,
        }
      )
      
  2. 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
  3. 第三个可选的参数是一个对象,支持以下这些选项:
    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined

    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调,使用reactive监听深层对象开启和不开启deep 效果一样

    • flush:调整回调函数的刷新时机。

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

watchEffect高级侦听器

watchEffect: 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(() => {
  const bar = state.bar
  const foo = state.foo
  console.log('watchEffect配置的回调执行了')
})

watchEffect,watch,computed的区别

  • watch:既要指明监听的属性,也要指明监听的回调。

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

  • watchEffect有点像computed,但computed注重的计算出来的值(回调函数的返回值),必须要写返回值,而watchEffect更注重的是过程(回调函数的函数体),不用写返回值。

自定义hook函数

什么是hook

hook 本质是一个函数,把setup函数中使用的组合式API进行了封装。

和vue2.x中的mixin比较

  1. mixin的缺点:变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护;多个mixin的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突,所以在 vue3 中已弃用。
  2. 自定义 hook 的优势:复用代码, 很清楚复用功能代码的来源。

实现一个记录当前鼠标点击位置的hook

创建一个hooks文件夹,里面创建文件usePoint.js

import { reactive, onMounted, onBeforeUnmount, toRefs } from 'vue'
export default function () {
  let point = reactive({
    x: 0,
    y: 0,
  })
  const savePoint = event => {
    point.x = event.pageX
    point.y = event.pageY
    console.log(event.pageX, event.pageY)
  }
  onMounted(() => {
    window.addEventListener('click', savePoint)
  })
  onBeforeUnmount(() => {
    window.removeEventListener('click', savePoint)
  })
  return toRefs(point)
}

在组件中使用

<template>
  <h2>当前点击时鼠标的坐标为:x:{{pointX}},y:{{pointY}},point:{{ point }}</h2>
</template>
<script setup>
import usePoint from '@/hooks/usePoint'
const point = usePoint()
const { x: pointX, y: pointY } = usePoint()
</script>

CSS相关

新增的选择器

  1. 深度选择器:deep():处于 scoped 样式中的选择器如果想要做更“深度”的选择,可以使用 :deep() 这个伪类,可以不用在使用 ::v-deep
  2. 插槽选择器:slotted:默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以明确地将插槽内容作为选择器的目标
  3. 全局选择器:global :如果想让其中一个样式规则应用到全局,可以使用 :global 伪类来实现,不用再另外创建一个 <style>

CSS Modules

CSS Modules: <style module> 标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style 对象暴露给组件,也可以通过 module <style module="classes">自定义注入名称

<template>
  css modules
  <!-- 默认注入名称 -->
  <p :class="$style.red">red</p>
  <!-- 自定义注入名称 -->
  <p :class="classes.green">green</p>
</template>

/* 默认注入的名称 $style */
<style module>
.red {
  color: red;
}
</style>
/* 自定义注入的名称 classes */
<style module="classes">
.green {
  color: green;
}
</style>

在css中使用js变量

CSS 中的 v-bind():单文件组件的 <style> 标签支持使用 v-bind CSS 函数将 CSS 的值链接到动态的组件状态,且支持 JavaScript 表达式 (需要用引号包裹起来)

<template>
  <p>hello</p>
</template>

<script setup>
const theme = {
  color: 'red'
}
</script>

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

新增的内置组件

Fragment

  1. 在Vue2中: 组件 <template> 必须有一个根标签
  2. 在Vue3中: 组件 <template> 可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  3. 好处: 减少标签层级, 减小内存占用

Teleport

  1. Teleport 可以将其插槽内容渲染到指定 DOM 节点,不受父级stylev-show等属性影响,但dataprop数据依旧能够共用的技术;

  2. Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。

  3. 使用: 通过to 属性插入到指定元素位置,如 body,html,自定义className等等,不能是组件中的某些位置否则会报错。

    <teleport to="移动位置">我是一个弹窗<</teleport>
    

Suspense

  1. <Suspense> 接受两个插槽:#default#fallback。它将在内存中渲染默认插槽的同时展示后备插槽内容。
  2. 如果在渲染时遇到异步依赖项 (异步组件和具有 async setup() 的组件),它将等到所有异步依赖项解析完成时再显示默认插槽。
  3. <script setup> 中可以使用顶层 await。结果代码会被编译成 async setup()
  4. 使用:实际开发中没有使用,个人觉得列表数据加载中配合骨架屏使用应该不错

示例

异步组件

<template>
  <div>异步组件</div>
</template>

<script setup>
const res = async () => {
  return await new Promise(resolve => {
    setTimeout(() => {
      resolve()
    }, 2000)
  })
}
await res()
</script>

异步引入组件

<template>
  <Suspense>
    <AsyncView />
    <template #fallback>
      <div>loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
const AsyncView = defineAsyncComponent(() => import('./AsyncView.vue'))
</script>

v-model

v-model 其实是一个语法糖 通过props和emit组合而成的,在Vue3中v-model 是破坏性更新的

vue2和vue3中两者的区别

  1. 默认值的改变
    • prop:value -> modelValue
    • 事件:input -> update:modelValue
  2. 新增 支持多个v-model
  3. 新增 支持自定义 修饰符 Modifiers
  4. v-bind 的 .sync 修饰符和组件的 model 选项已移除

示例

<!-- 父组件 -->
<UserName v-model:first-name="first" v-model:last-name="last" />
<!-- 子组件 -->
<template>
  <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
  <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>

<script setup>
defineProps({
  firstName: String,
  lastName: String,
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

也可以看看我之前写的vue2和vue3组件封装父子组件之间的通信之v-model

指令directive

指令钩子函数

相比vue2中的指令,vue3中的生命周期更好理解,它的生命周期和vue2的生命周期触发时机是一样的,参数和vue2基本一样。

const vMyDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

// 简写 对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为
// 除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示
const vMyDirective = (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  /* ... */
}

钩子参数

  1. el:指令绑定到的元素。这可以用于直接操作 DOM。

  2. binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  3. vnode:代表绑定元素的底层 VNode。

  4. prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

指令注册

全局注册

// 全局注册
const app = createApp({})
app.directive('myDirective', {
  /* ... */
})

局部注册:在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。

const vMyDirective = () => {
  /* ... */
}

实现一个简单的指令v-color

<template>
  <div style="width: 200px; height: 200px" v-color="'red'"></div>
</template>
<script setup>
const vColor = (el, binding) => {
  el.style.background = binding.value
}
</script>

最后

文中有问题或者有异议也欢迎大家指出。