Vue3 学习指南

认识 Vue3

相关信息

  • 2020年9月18日,Vue.js发布3.0版本,代号:One Piece(海贼王)
  • github上的tags地址:https://github.com/vuejs/vue-next/releases/tag/v3.0.0
  • 性能的提升:
    • 打包大小减少41%
    • 初次渲染快55%, 更新渲染快133%
    • 内存减少54%
  • 底层源码的改动
  • 支持 TypeScript
  • 新的特性:【Compostition API】
  • 新的内置组件
  • 一些其它的改变

创建 Vue3.x 的工程

  • 使用 vue-cli 创建
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue_test
## 启动
cd vue_test
npm run serve
  • 使用 vite 创建
## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev

Composition API(常用部分)

setup

  • 它是 Vue3.x 中一个新的配置项,值为一个函数

  • setup 是所有 Composition API【组合API】‘表演舞台’

  • 组件中所用到的: 数据、方法...等,均要配置在 setup 中

  • setup 的执行时机:

    • 在beforeCreate 之前执行(一次),此时组件对象还没有创建
    • this 是 undefined,不能通过 this 进行访问 data、computed、methods、props
  • setup 返回值:

    • 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
    • 返回对象中的属性会与 data 函数返回对象的属性合并成为组件对象的属性
    • 返回对象中的方法会与 methods 中的方法合并成功组件对象的方法
    • 如果有重名, setup 优先
  • 注意点:

    • 尽量不要与 Vue2.x 配置混用
      • (1) Vue2.x 配置【data,methods,computed...】中可以访问到 setup 中的属性,方法
      • (1) 但在 setup 中 不能访问到 Vue2.x 配置【data,methods,computed...】
      • (2) 如果有重名,则 setup 优先
    • setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 promise,模板看不到 return 对象中的属性
  • setup 的参数

    • props: 值为对象, 包含: 组件外部传递过来,且组件内部声明接收了的属性
    • context: 上下文对象
      • attrs: 值为对象,包含: 组件外部传递过来,但是没有在props配置中声明的属性,相当于 this.$attrs
      • slots: 接收到的插槽内容,相当于 this.$slots
      • emit: 分发自定义事件的函数,相当于 this.$emit 【注意:此处有警告,可以加一句: emits:['接收的自定义事件']】

ref

  • 作用: 定义一个数据的响应式
  • 语法: const xxx = ref(initValue)
    • 创建一个包含响应式数据的引用对象【reference 对象】
    • JS 中操作数据: xxx.value
    • 模板中读取数据: 不需要.value,直接: <h1>{{xxx}}</h1>
  • 接收的数据可以是: 基本类型、也可以是对象类型
  • 基本类型的数据: 响应式依然是 Object.defineProperty() 的 get 和 set 完成的
  • 对应类型的数据: 内部借助了 Vue3.x 中的一个函数【reactive 函数】
```vue
<template>
  <h2>{{ count }}</h2>
  <hr />
  <button @click="update">更新</button>
</template>

<script>
import { ref } from 'vue'
export default {
  /* 在Vue3中依然可以使用data和methods配置, 但建议使用其新语法实现 */
  // data () {
  //   return {
  //     count: 0
  //   }
  // },
  // methods: {
  //   update () {
  //     this.count++
  //   }
  // }

  /* 使用vue3的composition API */
  setup() {
    // 定义响应式数据 ref对象
    const count = ref(1)
    console.log(count)

    // 更新响应式数据的函数
    function update() {
      // alert('update')
      count.value = count.value + 1
    }

    return {
      count,
      update
    }
  }
}
</script>

reactive 函数

  • 作用: 定义多个数据的响应式
  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
<template>
  <h2>name: {{ state.name }}</h2>
  <h2>age: {{ state.age }}</h2>
  <h2>wife: {{ state.wife }}</h2>
  <hr />
  <button @click="update">更新</button>
</template>

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

    return {
      state,
      update
    }
  }
}
</script>

比较 Vue2.x 与 Vue3.x 的响应式

  • Vue2.x 【Object.defineProperty()】

    • 对象: 通过 defineProperty 对对象的已有属性值的读取和修改进行劫持(监视/拦截)
    • 对象: 通过 defineProperty 对对象的已有属性值的读取和修改进行劫持(监视/拦截)
  • 存在的问题:

    • 对象直接新添加的属性或删除已有属性, 界面不会自动更新
    • 直接通过下标替换元素或更新 length, 界面不会自动更新 arr[1] = {}
  • Vue3.x

    • 通过 Proxy(代理): 拦截对 data 任意属性的任意(13 种)操作, 包括属性值的读写, 属性的添加, 属性的删除等...
    • 通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
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'
<!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>

reactive 对比 ref

  • 它们都是 Vue3 的 composition API 中 2 个最重要的响应式 API
  • ref 用来处理基本类型数据, reactive 用来处理对象(递归深度响应式)
  • 如果用 ref 对象/数组, 内部会自动将对象/数组转换为 reactive 的代理对象
  • ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持
  • reactive 内部: 通过使用 Proxy 来实现对对象内部所有数据的劫持, 并通过 Reflect 操作对象内部数据
  • ref 的数据操作: 在 js 中要.value, 在模板中不需要(内部解析模板时会自动添加.value)

计算属性与监视【侦听器】

  • computed 函数
    • 它的写法与在 Vue2.x 中的写法几乎是一致的
<template>
  <div>
    姓: <input type="text" v-model="person.FirstName">
    名: <input type="text" v-model="person.LastName">
    <br />
    全名: <span>{{ person.FullName }}</span>
 </div>
</template>

<script>
import { reactive, computed } from 'vue'
export default {
  name: 'Demo',
  setup() {
    let person = reactive({
       FirstName: '李',
       LastName: '四'
    })

    // 计算属性--简写的方法是没有考虑属性被修改的情况
    person.FullName = computed(() => {
      return person.FirstName + '-' + person.LastName
    })
    
    // 完整写法
    let FullName = computed(() => {
      get() {
        return  person.FirstName + '-' + person.LastName
      },
      set(value) {
        const nameArr = value.split('-')
        person.FistName = nameArr[0]
        person.LastName = newArr[1]
      }
    })

    return {
       person
    }
  }
}
</script>

<style>

</style>
  • watch 函数
    • 它的写法与 Vue2.x 的写法几乎一致
    • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
    • 默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次
    • 通过配置 deep 为 true, 来指定深度监视
    • 在 Vue3.x 中有几个小坑
      • 监视 reactive 定义的响应式数据时, oldValue 无法正确获取、强制开启了深度监视【deep配置无效】
      • 监视 reactive 定义响应式数据中某个属性时【对象属性】, deep的配置是有效的
<template>
 <div>
    <h2>当前求和为: {{sum}}</h2>
  <button @click="sum++">点击+1</button>
  <hr>
  <h2>当前的信息为: {{msg}}</h2>
  <button @click="msg+= '!'">修改信息</button>
  <hr>
  <h2>姓名: {{Person.name}}</h2>
  <h2>年龄: {{Person.age}}</h2>
  <button @click="Person.name += '~'">修改姓名</button>
  <button @click="Person.age++">增长年龄</button>
 </div>
</template>

<script>
import {ref, watch, reactive} from 'vue'
export default {
  name: "Demo",
  setup() {
  
    let sum = ref(0)
    let msg = ref('你好啊')
    let Person = reactive({
      name: '张三',
      age: 19,
    })

    // 情况一: 监视 ref 所定义的一个响应式数据
    /*  watch(sum, function(newValue,oldValue) {
       console.log(newValue,oldValue)
     }, {immediate:true,deep:true}) */


    // 情况二: 监视 ref 所定义的多个响应式数据
     /* watch([sum, msg], function(newValue,oldValue) {
       console.log('sum的值改变了',newValue,oldValue)
     },{immediate:true,deep:true}) */
     

     /*
      情况三: 监视 reactive 所定义的一个响应式数据的全部属性
        【注意: 有 Bug,无法获取旧的值】
        【注意: 强制开启了深度监视(deep 配置无效)】
     */
    /* watch(Person, (newValue, oldValue) => {
       console.log('Person变化了', newValue, oldValue)
     },{deep:false}) */


    // 情况四: 监视 reactive 所定义的一个响应式数据中某个属性【需要是回调函数形式】
    /* watch(() => Person.age, (newValue, oldValue) => {
      console.log('Person变化了', newValue, oldValue)
    })  */


    // 情况五: 监视 reactive 所定义的一个响应式数据中的某些属性
   /*  watch([() => Person.name, () => Person.age], (newValue, oldValue) => {
      console.log('Person变化了', newValue, oldValue)
    })  */
    
    return {
      sum,
      msg,
      Person
    }
  }
}
</script>

<style>

</style>
watchEffect 函数
  • watchEffect 的套路是: 不需要指明监视哪个属性,监视的回调中用到哪个属性,就监视哪个属性
  • watchEffect 有点像 computed
  • 但 computed 注重的计算出来的值,【回调函数的返回值】所以必须要有返回值
  • 而 watchEffect 更注重的是过程【回调函数的函数体】所以不需要写返回值
<template>
  <div>
    <h2>当前求和为: {{ sum }}</h2>
    <button @click="sum++">点击+1</button>
    <hr />
    <h2>当前的信息为: {{ msg }}</h2>
    <button @click="msg += '!'">修改信息</button>
    <hr />
    <h2>姓名: {{ Person.name }}</h2>
    <h2>年龄: {{ Person.age }}</h2>
    <button @click="Person.name += '~'">修改姓名</button>
    <button @click="Person.age++">增长年龄</button>
  </div>
</template>

<script>
import { ref, reactive, watch, watchEffect } from "vue"
export default {
  name: "Demo",
  setup() {
    let sum = ref(0);
    let msg = ref("你好啊");
    let Person = reactive({
      name: "张三",
      age: 19,
    })

    watchEffect(() => {
      const x1 = sum.value
      const x2 = person.age
      console.log('watchEffect所指定的回调执行了')
    })

    return {
      sum,
      msg,
      Person
    }
  }
}
</script>

<style>
</style>

Vue3.x 的生命周期

  • Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:

    • beforeDestroy 改名为 beforeUnmount
    • destroyed 改名为 unmounted
  • Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:

    • beforeCreate===>setup()
    • created=======>setup()
    • beforeMount ===>onBeforeMount
    • mounted=======>onMounted
    • beforeUpdate===>onBeforeUpdate
    • updated =======>onUpdated
    • beforeUnmount ==>onBeforeUnmount
    • unmounted =====>onUnmounted

自定义 hook 函数

  • 使用 Vue3 的组合 API 封装的可复用的功能函数
  • 自定义 hook 的作用类似于 vue2 中的 mixin 技术
  • 自定义 Hook 的优势: 很清楚复用功能代码的来源, 更清楚易懂

toRefs

  • 把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref
  • 应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
  • 问题: reactive 对象取出的所有属性值都是非响应式的
  • 解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性
<template>
  <h2>App</h2>
  <h3>foo: {{ foo }}</h3>
  <h3>bar: {{ bar }}</h3>
  <h3>foo2: {{ foo2 }}</h3>
  <h3>bar2: {{ bar2 }}</h3>
</template>

<script lang="ts">
import { reactive, toRefs } from 'vue'
/*
toRefs:
  将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象
  应用: 当从合成函数返回响应式对象时,toRefs 非常有用,
        这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
*/
export default {
  setup() {
    const state = reactive({
      foo: 'a',
      bar: 'b'
    })

    const stateAsRefs = toRefs(state)

    setTimeout(() => {
      state.foo += '++'
      state.bar += '++'
    }, 2000)

    const { foo2, bar2 } = useReatureX()

    return {
      // ...state,
      ...stateAsRefs,
      foo2,
      bar2
    }
  }
}

function useReatureX() {
  const state = reactive({
    foo2: 'a',
    bar2: 'b'
  })

  setTimeout(() => {
    state.foo2 += '++'
    state.bar2 += '++'
  }, 2000)

  return toRefs(state)
}
</script>

shallowReactive 与 shallowRef

  • shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
  • shallowRef: 只处理了 value 的响应式, 不进行对象的 reactive 处理
  • 什么时候用浅响应式呢?
    • 一般情况下使用 ref 和 reactive 即可
    • 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
    • 如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
<template>
  <h2>App</h2>

  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>
  <h3>m3: {{ m3 }}</h3>
  <h3>m4: {{ m4 }}</h3>

  <button @click="update">更新</button>
</template>

<script lang="ts">
import { reactive, ref, shallowReactive, shallowRef } from 'vue'
export default {
  setup() {
    const m1 = reactive({ a: 1, b: { c: 2 } })
    const m2 = shallowReactive({ a: 1, b: { c: 2 } })

    const m3 = ref({ a: 1, b: { c: 2 } })
    const m4 = shallowRef({ a: 1, b: { c: 2 } })

    const update = () => {
      // m1.b.c += 1
      // m2.b.c += 1

      // m3.value.a += 1
      m4.value.a += 1
    }

    return {
      m1,
      m2,
      m3,
      m4,
      update
    }
  }
}
</script>

readonly 与 shallowReadonly

  • readonly: 深度只读数据
    • 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
    • 只读代理是深层的:访问的任何嵌套 property 也是只读的。
  • shallowReadonly: 浅只读数据
  • 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
  • 应用场景:
  • 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<template>
  <h2>App</h2>
  <h3>{{ state }}</h3>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import { reactive, readonly, shallowReadonly } from 'vue'
export default {
  setup() {
    const state = reactive({
      a: 1,
      b: {
        c: 2
      }
    })

    // const rState1 = readonly(state)
    const rState2 = shallowReadonly(state)

    const update = () => {
      // rState1.a++ // error
      // rState1.b.c++ // error

      // rState2.a++ // error
      rState2.b.c++
    }

    return {
      state,
      update
    }
  }
}
</script>

toRaw 与 markRaw

  • toRaw: 将一个由 reactive 生成的 响应式数据对象转为普通对象

    • 使用场景: 用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新
  • markRaw: 标记一个对象,使其永远不再成为响应式对象

    • 应用场景:
      • 有些值不应该被设置为为响应式的,例如【赋值的第三方类库等】
      • 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能
<template>
  <h2>{{ state }}</h2>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
import { markRaw, reactive, toRaw } from 'vue'
export default {
  setup() {
    const state = reactive<any>({
      name: 'tom',
      age: 25
    })

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

    const testMarkRaw = () => {
      const likes = ['a', 'b']
      // state.likes = likes
      state.likes = markRaw(likes) // likes数组就不再是响应式的了
      setTimeout(() => {
        state.likes[0] += '--'
      }, 1000)
    }

    return {
      state,
      testToRaw,
      testMarkRaw
    }
  }
}
</script>

customRef

  • 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
<template>
  <input type="text" v-model="keyWord">
  <h3>{{keyWord}}</h3>
</template>

<script>
import { ref, customRef } from 'vue'
export default {
  name: 'Demo',
  setup() {

    // 自定义 ref
    function myRef(value) {
      let timer
      return customRef((track, trigger) => {
        return {
          get() {
            track()
            return value
          },
          set(newValue) {
            timer = setTimeout(() => {
              clearTimeout(timer)
              value = newValue
              trigger() // 通知 Vue 重新解析模板
            }, 500)
          }
        }
      })
    }

    let keyWord = myRef('hello')

    return {
      keyWord
    }
  }
}
</script>

<style>

</style>

provide 与 inject

  • 实现跨层级组件(祖孙)间通信
<template>
  <h1>父组件</h1>
  <p>当前颜色: {{ color }}</p>
  <button @click="color = 'red'">红</button>
  <button @click="color = 'yellow'">黄</button>
  <button @click="color = 'blue'">蓝</button>

  <hr />
  <Son />
</template>

<script lang="ts">
import { provide, ref } from 'vue'
/*
- provide` 和 `inject` 提供依赖注入,功能类似 2.x 的 `provide/inject
- 实现跨层级组件(祖孙)间通信
*/

import Son from './Son.vue'
export default {
  name: 'ProvideInject',
  components: {
    Son
  },
  setup() {
    const color = ref('red')

    provide('color', color)

    return {
      color
    }
  }
}
</script>
<template>
  <div>
    <h2>子组件</h2>
    <hr />
    <GrandSon />
  </div>
</template>

<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
  components: {
    GrandSon
  }
}
</script>
<template>
  <h3 :style="{ color }">孙子组件: {{ color }}</h3>
</template>

<script lang="ts">
import { inject } from 'vue'
export default {
  setup() {
    const color = inject('color')

    return {
      color
    }
  }
}
</script>

响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否由 reactive 或 readonly 方法创建的代理

Composition API VS Option API

  • 在传统的 Vue OptionsAPI 中,新增或者修改一个需求,就需要分别在 data,methods,computed 里修改 ,滚动条反复上下移动
  • 使用 组合式API,我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起

Fragment(片断)

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

Teleport(瞬移)

  • Teleport 提供了一种干净的方法, 让组件的 html 在父组件界面外的特定标签(很可能是 body)下插入显示
  • ModalButton.vue
<template>
  <button @click="modalOpen = true">
    Open full screen modal! (With teleport!)
  </button>

  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      <div>
        I'm a teleported modal! (My parent is "body")
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  </teleport>
</template>

<script>
import { ref } from 'vue'
export default {
  name: 'modal-button',
  setup() {
    const modalOpen = ref(false)
    return {
      modalOpen
    }
  }
}
</script>

<style>
.modal {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.modal div {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 300px;
  height: 300px;
  padding: 5px;
}
</style>
  • App.vue
<template>
  <h2>App</h2>
  <modal-button></modal-button>
</template>

<script lang="ts">
import ModalButton from './ModalButton.vue'

export default {
  setup() {
    return {}
  },

  components: {
    ModalButton
  }
}
</script>

Suspense(不确定的)

  • 它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
posted @ 2022-08-10 01:59  小学生学Web前端  阅读(116)  评论(0)    收藏  举报