Vue3.0

1 简介

2020年9月18日,Vue.js发布3.0版本Vue3官网 Vue v3.0.0 One Piece。Vue3向下兼容Vue2.x版本,并且比Vue2的性能显著提升(打包大小减少41%,初次渲染速度提高55%,更新渲染提高133%,内存使用率减少54%)。Vue3使用Proxy代替defineProperty实现响应式,重写虚拟DOM的实现和Tree-Shaking,更友好兼容TypeScript。

2 常用Composition API

2.1 setup函数

组件中所用到的数据、方法、计算属性、监视属性、生命周期钩子等等都配置在setup中。
setup函数有两种返回值:

  • 若返回一个对象,则对象中的属性方法在template模板中都可以直接使用(常用)
  • 若返回一个渲染函数,则可以自定义渲染内容
<template>
  <!--   setup函数返回对象的属性方法在模板中可以直接使用 -->
  <h1>姓名:{{name}}</h1>
  <button @click="sayHello"></button>
</template>
<script>
  // import {h} from 'vue'
  export default {
    setup(){
      // 数据
      let name = '张三'
      let age = 18
      // 方法
      function sayHello(){
        alert(`我叫${name},我今年${age}岁`)
      }
      // 返回一个对象
      return {
        name,
        age,
        sayHello
      }
      // 返回一个渲染函数
      // return ()=>{ return h('h1','你好') }
    }
  }
</script>

注意:

  • 当Vue3与Vue2.x混用时(不建议),Vue2.x配置(data、methods、computed…)中可以访问到setup中的属性、方法,但在setup中不能访问到Vue2.x配置(data、methods、computed…)。如果有重名,setup优先。
  • setup不能是一个async函数,因为setup被async修饰后,返回值不再是简单的对象,而是被promise包裹的对象,模板中看不到对象中的属性。(Suspense和异步组件配合时可以返回一个Promise实例)
  • setup在beforeCreate(){}配置项之前执行(组合式API形式的beforeCreate在setup里面),this是undefined。
  • setup(props, context){}接收到两个参数:
    第一个是对象,包含组件外部传递进来且组件内部props: []配置中声明接收了的属性;
    第二个是context上下文对象,包括:
    • attrs:组件外部传递进来,但没有在props配置中声明的属性
    • slots:收到的插槽内容
    • emit:自定义事件的函数

2.2 ref函数

ref可以定义一个响应式数据,通过语法const xxx = ref(initValue)创建一个引用对象RefImpl { …, value: initValue },对象中包含响应式数据,在JS中通过xxx.value操作数据,在模板中读取数据时不需要.value,直接<div>{{xxx}}</div>
ref接收的数据可以是基本类型,也可以是对象类型。如果是基本类型的数据,响应式实现原理与Vue2一样,通过Object.defineProperty()getset实现;如果是对象类型的数据,对象内部通过Vue3中的一个新函数reactive实现(本质上还是通过ES6的Proxy实现,Vue3把Proxy操作封装在reactive函数中)。

<template>
  <h1>姓名:{{name}}</h1>
  <h1>年龄:{{age}}</h1>
  <h1>工作种类:{{job.type}}</h1>
  <h1>工作薪水:{{job.salary}}</h1>
  <button @click="changeInfo"></button>
</template>
<script>
  import {ref} from 'vue'
  export default {
    setup(){
      // name不再是字符串,是一个引用对象RefImpl{...,value:"张三"},对象中有一个属性值为张三
      let name = ref('张三')
      // age是普通数据,不是响应式数据
      let age = 18
      // ref函数处理对象类型
      // job是ref引用对象RefImpl{...,value:Proxy{...}}
      // job.value是Proxy{type:'前端工程师', salary:'30K'}
      let job = ref({
        type:'前端工程师',
        salary:'30K'
      })
      function changeInfo(){
        name.value = '李四' // 响应式数据,页面会更新
        age = 20 // 不是响应式数据,数据被修改了但页面不会更新
        job.value.type = 'UI设计师' // 响应式数据,页面会更新
      }
      return {
        name,
        age,
        job,
        changeInfo
      }
    }
  }
</script>

2.3 reactive函数

reactive可以定义一个对象类型的响应式数据(基本类型要用ref函数),通过语法const 代理对象 = reactive(源对象)接收一个对象或数组,返回一个代理对象,代理对象本质上是Proxy的实例对象,因为reactive内部是基于ES6的Proxy实现的,通过代理对象操作源对象内部数据。
reactive定义的响应式数据是深层次的。

<template>
  <h1>姓名:{{person.name}}</h1>
  <h1>年龄:{{person.age}}</h1>
  <h1>工作种类:{{person.job.type}}</h1>
  <h1>工作薪水:{{person.job.salary.max}}</h1>
  <h1>爱好:{{person.hobby}}</h1>
  <button @click="changeInfo"></button>
</template>
<script>
  import {reactive} from 'vue'
  export default {
    setup(){
      // reactive接收一个数组
      // let hobby = reactive(['吃饭', '睡觉', '打豆豆'])
      // reactive接收一个对象,person是Proxy{name: '张三', ...}
      let person = reactive({
        name: '张三',
        age: 18,
        job: {
          type: '前端工程师',
          salary: {
            max: '15K',
            min: '30K'
          }
        },
        hobby: ['吃饭', '睡觉', '打豆豆'],
      })
      function changeInfo(){
        person.name = '李四'
        person.age = 20
        person.job.type = 'UI设计师'
      }
      return {
        person,
        changeInfo
      }
    }
  }
</script>

2.4 Vue3的响应式原理

2.4.1 Vue2.x的响应式

Vue2.x实现响应式,如果是对象类型,通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持);如果是数组类型,通过重写更新数组的一系列方法来实现拦截,比如在vue中调用数组的push方法,其实调用的是vue经过二次封装的push,不是Array原型对象上的push,vue帮你先调用Array原型对象上的push方法,再更新界面。

// 对象类型通过Object.defineProperty()实现响应式
Object.defineProperty(对象, 属性名, {
  get(){
    return 对象.属性名
  },
  set(value){
    对象.属性名 = value
  }
})

Vue2.x实现响应式存在一些问题,新增属性、删除属性和直接通过下标修改数组,界面都不会自动更新,但这还是有解决的办法:

  • 增加属性:this.$set(对象, 属性名, 属性值)Vue.set(对象, 属性名, 属性值)
  • 删除属性:this.$delete(对象, 属性名)Vue.delete(对象, 属性名)
  • 更新数组:this.$set(数组, 下标, 值)数组.splice()

2.4.2 Vue3的响应式

在介绍Vue3响应式原理前先来介绍Reflect反射对象——window身上的一个内置对象。
Reflect.get(对象, 属性名)Reflect.set(对象, 属性名, value)Reflect.deleteProperty(对象, 属性名)可以实现同对象.属性名对象.属性名 = valuedelete 对象.属性名一样的效果,读取、修改和删除对象上的某一个属性。但是Object.defineProperty()出错时,后续代码会停止运行,需要try catch抛出异常后才能跑下去,而Reflect即使出错了后续代码也能执行下去,并且会返回一个执行成功或失败的布尔值,可以通过判断布尔值真假指定操作。

let obj = {a:1, b:2}
Object.defineProperty(obj, 'c', {
  get(){
    return 3
  },
  set(value){}
})
Object.defineProperty(obj, 'c', {
  get(){
    return 4
  },
  set(value){}
})
// 此时会报错Cannot redefine property: c,不能重复定义属性c,需要try{}catch(error){}抛出异常,后面代码才能执行

const x1 = Reflect.defineProperty(obj, 'c', { // x1为true
  get(){
    return 3
  },
  set(value){}
})
const x2 = Reflect.defineProperty(obj, 'c', { // x2为false
  get(){
    return 4
  },
  set(value){}
})
if(x2){
  // 可以指定x2为真时的操作
}
// 此时x2虽然执行失败,但后续代码还能跑下去,并且可以指定x1、x2执行成功或失败的操作

Vue3实现响应式首先通过Proxy代理拦截对象中任意属性的变化,包括属性的读写、添加和删除,再通过Reflect对源对象的属性进行操作。

const p = new Proxy(person,{ // person是原对象,p是代理对象
  // 拦截,读取某个属性时调用
  get(target,propName){
    // target是new Proxy时传入的源对象,也就是person
    // propName是某个属性
    return Reflect.get(target, propName) // Reflect对源对象的属性进行读取
  },
  // 拦截,修改或增加某个属性时调用
  set(target,propName){
    Reflect.set(target, propName, value) // Reflect对源对象的属性进行修改或添加
  },
  // 拦截,删除某个属性时调用
  deleteProperty(target,propName){
    return Reflect.deleteProperty(target, propName) // Reflect对源对象的属性进行删除
  },
})

2.4.3 ref与reactive对比

refreactive
定义数据类型基本类型
(也可以定义对象或数组,内部通过reactive实现)对象或数组
实现原理Object.defineProperty()的get和setProxy实现响应式,Reflect操作源对象的数据
使用方式JS中通过xxx.value操作,模板中读取时不需要.value都不需要.value

2.5 computed

import { computed } from 'vue'
export default {
  setup() {
    let person = {
      firstName: '张',
      lastName: '三',
    }
    // 计算属性简写(只读)
    let name = computed(() => {
      return person.firstName + '-' + person.lastName
    })
    // 计算属性完整写法(可修改)
    let name = computed({
      get(){
        return person.firstName + '-' + person.lastName
      },
      set(value){
        const nameArr = value.split('-')
        person.firstName = nameArr[0]
        person.lastName = nameArr[1]
      },
    })
    return {
      person
    }
  }
}

2.6 watch

import {ref, watch} from 'vue'
export default {
  // Vue2写法
  watch: {
    // 简写
    sum(newValue, oldValue) {
      ...
    },
    // 完整写法
    sum: {
      immediate: true, // 立即监听
      deep: true, // 深度监视
      // 监视的回调
      handler(newValue, oldValue){
        ...
      }
    }
  },
  setup() {
    let sum = ref(0)
    let msg = ref('hello')
    let person = reactive({
      firstName: '张',
      lastName: '三',
      a:{
        b:{
          c: 1,
        }
      }
    })
    // Vue3写法
    // 监视ref定义的一个响应式数据
    watch(sum, (newValue, oldValue)=>{
      ...
    }, {immediate: true})
    // 监视ref定义的多个响应式数据,Vue2中只能写一个watch配置项,Vue3中可以调用多次watch函数
    watch(sum, (newValue, oldValue)=>{
      ...
    })
    watch(msg, (newValue, oldValue)=>{
      ...
    })
    // 监视ref定义的多个响应式数据,简便写法
    watch([sum, msg], (newValue, oldValue)=>{
      // newValue、oldValue是数组,相当于[sum的newValue, msg的newValue]、[sum的oldValue, msg的oldValue]
      ...
    })
    // 监视reactive定义的对象
    watch(person, (newValue, oldValue)=>{
      // 无法获取正确的oldValue
      // 监视的是reactive定义的对象,deep配置无效,强制开启深度监视
    })
    // 监视reactive定义的对象中的某个属性
    watch(()=>person.firstName, (newValue, oldValue)=>{
      // 可以获取正确的oldValue
    })
    // 监视reactive定义的对象中的某些属性
    watch([()=>person.firstName, ()=>person.lastName], (newValue, oldValue)=>{
      ...
    })
    // 监视reactive定义的对象中的某个属性,这个属性也是对象
    watch(()=>person.a, (newValue, oldValue)=>{
      // 监视的是reactive定义的对象中的某个属性,所以deep配置有效
      // 无法获取oldValue
    }, {deep: true})
    return {
      sum,
      msg,
      person
    }
  }
}

监视ref定义的基本数据类型时,不需要.value.value后相当于监视0这个数字本身。
监视ref定义的对象类型时,需要.value,此时相当于监视reactive定义的对象。或者不加.value,加deep: true配置,此时可以监视到RefImpl对象中的属性。

2.7 watchEffect函数

watch函数既要指明监视的是哪个属性,也要指明监视的回调。watchEffect函数不用指明监视的是哪个属性,监视的回调中用到哪个属性就监视哪个属性。

// watchEffect所指定的回调中,用到的数据只要发生变化,就直接重新执行回调
watchEffect(()=>{
  const x1 = sum.value
  const x2 = person.age
})

2.8 Vue3的生命周期

Vue3中可以继续使用Vue2.x中的生命周期钩子,但有两个被更名:beforeDestroy改为beforeUnmountdestroyed改为unmounted

// 通过配置项的形式使用生命周期钩子
export default {
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {},
}

Vue3也提供了Composition API形式的生命周期钩子,与Vue2.x中钩子对应关系如下:

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

如果代码中同时写了配置项形式和Composition API形式的生命周期钩子,Composition API形式的优先,执行顺序为:setup–>beforeCreate–>created–>onBeforeMount–>beforeMount–>onMounted–>mounted。

// 通过组合式API的形式使用生命周期钩子
import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'
export default {
  setup() {
    onBeforeMount(()=>{})
    onMounted(()=>{})
    onBeforeUpdate(()=>{})
    onUpdated(()=>{})
    onBeforeUnmount(()=>{})
    onUnmounted(()=>{})
  }
}

2.9 自定义hook函数

hook本质是一个函数,把setup函数中使用的Composition API进行封装,起到复用代码的作用。

2.10 toRef

要将响应式对象a中的某个属性b单独提供给外部使用时,可以通过toRef(a, 'b')创建一个ref对象,其value值指向a中的属性b。toRefs与toRef功能一致,但可以批量创建多个ref对象。

import { reactive } from 'vue'
export default {
  setup() {
    let person = reactive({
      name: '张三',
      age: 18,
      a: {
        b: {
          c: 1,
        }
      }
    })
    // 相当于name: '张三',返回出去的name、age不是响应式的
    // return {
    //   name: person.name,
    //   age: person.age,
    // }

    // name是响应式,但是重新复制一份,修改name,person中的name属性不会改变
    // return {
    //   name: ref(person.name),
    //   age: ref(person.age)
    // }

    // name是ref对象ObjectRefImpl{..., value: '张三', ...},属性value指向person中的name
    return {
      name: toRef(person, 'name'),
      age: toRef(person, 'age'),
      c: toRef(person.a.b, 'c')
    }

    // toRefs(person)是一个对象
    // {name: ObjectRefImpl{..., value: '张三', ...}, age: ObjectRefImpl{}, job: {}}
    return {
      ...toRefs(person)
    }
  }
}

3 其他Composition API

3.1 shallowReactive与shallowRef

shallowReactive的响应式是浅层次,只处理对象第一层属性的响应式。

let person = shallowReactive({
  name: '张三', // 响应式
  age: 18, // 响应式
  a: {
    b: {
      c: 1, // 不是响应式
    }
  }
})

ref可以定义对象类型数据,内部通过调用reactive来处理,而shallowRef不能处理对象类型的响应式,只能定义基本类型的响应式数据。

// x是RefImpl{...,value:Proxy{y: 0}}
let x = ref({
  y: 0
})

// x是RefImpl{...,value:{y: 0}}
let x = ref({
  y: 0
})

3.2 readonly与shallowReadonly

person = readonly(person)让一个响应式数据变为只读的,shallowReadonly浅只读。

3.3 toRaw与markRaw

toRaw将一个由reactive生成的响应式对象转为普通对象
markRaw标记一个对象,使其永远不会再成为响应式对象。

3.4 customRef

3.5 provide与inject

作用:实现祖孙组件间通信
父组件有一个provide选项来提供数据,孙组件有一个inject选项来使用这些数据。(子组件也可以,但父子组件传递数据一般用props)

setup(){
  ...
  let person = reactive({
    name: '张三',
    age: 18
  })
  provide('person', person)
  ...
}
setup(){
  ...
  const person = inject('person')
  return { person } 
	...
}

3.6 响应式数据的判断

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

4 新的组件

4.1 Fragment

在Vue2.x中组件必须有一个根标签,在Vue3中组件可以没有根标签,内部会将多个标签包含在一个Fragment组件中。

4.2 Teleport

Teleport能够将组件结构移动到指定位置。

<teleport to="body">
  <!-- 把dialog组件移动到body下 -->
  <!-- <body>
  ...
  <div class="dialog"></div>
  ...
</body> -->
  <div class="dialog"></div>
</teleport>

4.3 Suspense

使用import xxx from './xxx/xxx'引入子组件的方式是静态引入,这种引入方式子组件与父组件同时出现,如果子组件引入失败,那么父组件也不会进行渲染。
使用import {defineAsyncComponent} from 'vue'``const Child = defineAsyncComponent(()=>import('./xxx/xxx'))引入子组件的方式是异步引入,父组件先出现,子组件后出现。如果等待子组件渲染的时间过长,或子组件突然出现,用户体验感差。
Suspense可以在等待异步组件时先渲染一些额外内容,如提示“加载中。。。”,让用户有更好的体验。
使用步骤:

  • 异步引入组件
  • 使用Suspense包裹组件,并配置好default和fallback
<template>
  <div>
    <h3>我是App父组件</h3>
    <Suspense>
      <template v-slot:default>
        <Child/>
      </template>
      <template v-slot:fallback>
        <h3>加载中。。。</h3>
      </template>
    </Suspense>
  </div>
</template>
<script>
  import {defineAsyncComponent} from 'vue'
  const Child = defineAsyncComponent(()=>import('./xxx/xxx')) // 异步引入
  export default {
    name: 'App',
    components: {
      Child,
    },
  }
</script>

版权声明:本文为Dani05原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。