vue3.0新特性

1,524 阅读9分钟

1. vue2.0与vue3.0响应式原理对比

1. vue2.0响应式原理

vue2.0中使用ES5中的Object.defineProperty方法实现响应式数据

缺点

  1. 无法监听到对象属性的动态添加和删除
  2. 无法监听数组的下标和length属性的变更

解决方案

  1. vue2.0提供vue.set方法用于动态给对象添加属性
  2. vue2.0提供vue.delete方法动态的删除对象的属性
  3. 重写vue中数组的方法,用于检测数组的变更

1.1 Object.defineProperty

const data = {
    name: 'ly',
    age: 18
}
for (let k in data) {
    let temp = data[k]
    Object.defineProperty(data, k, {
        get() {
            console.log(`我劫持了${k}的获取`)
            return temp
        },
        set(value) {
            console.log(`我劫持了${k}的设置,值${value}`)
            temp = value
        }
    })
}

在控制台中访问
1. data.name 
我劫持了name的获取
"ly"

2. 修改:
data.name = ‘ls’
我劫持了name的设置,值ls
"ls"
缺点举例:

无法监视到新增的属性的变更和删除

  1. 当修改data中修改一个不存在的属性:
data.gender = '男'
“男”

这个时候并没有做数据的劫持
  1. 当删除data中的属性时:
delete data.name
true

虽然删掉了但是也没有进行劫持

1.2. vue2中响应式数据

vue中需要使用$set方法动态的增加一个响应式属性

vue中需要使用$delete方法动态的删除对象的属性

//                                     price在data中没有定义
<p>{{car.brand}}----{{car.color}}-----{{car.price}}</p>

data: {
    car: {
        brand: '奔驰',
        color: 'blue'
    }
}

演示:
1. vue中需要使用$set方法动态的增加一个响应式属性
原来页面展示:奔驰----blue-----

vm.car.brand = '宝马'
修改后页面展示:宝马----blue-----
这个时候页面展示的奔驰会改成宝马

vm.car.price = '100'
修改后页面展示:宝马----blue-----
这个时候price并没有添加到页面
解决方法:
vm.$set(vm.car, 'price', 100)
修改后页面展示: 宝马----blue-----100

2. vue中需要使用$delete方法动态的删除对象的属性
原来页面展示:奔驰----blue-----
delete vm.car.brand
修改后页面展示:奔驰----blue-----
这个时候页面并没有更新删除掉brand
解决方法:
vm.$delete(vm.car, 'brand')
修改后页面展示: ----blue-----

2. vue3.0响应式原理

vue3.0中使用ES6中的proxy语法实现响应式数据

优点

  1. 可以检测到代理对象属性的动态添加和删除
  2. 可以检测到数组的下标和length属性的变更

缺点

  1. ES6的proxy语法对于低版本浏览器不支持IE11
  2. vue3.0会针对IE11出一个特殊的版本用于支持IE11

2.1. es6中proxy语法

const data = {
    name: 'ly',
    age: 18
}
const proxyData = new Proxy(data, {
    get(target, name) {
        console.log(`检测到${name}的获取`)
        return target[name]
    },
    set(target, name, value) {
        console.log(`检测到${name}的设置`,值为${value})
        target[name] = value
    },
    deleteProperty(target, key) {
        console.log(`监测到删除${key}`)
        return delete target[key]
    }
})

演示:(都会拦截到)
proxyData.name
检测到name的获取
"ly"

proxyData.name = 'ww'
检测到name的设置,值为ww
“ww”

delete proxyData.name
监测到删除name
true

2.2. vue3中响应式数据

直接新增和删除就可以了,不用setset、delete。

//                                     price在data中没有定义
<p>{{car.brand}}----{{car.color}}-----{{car.price}}</p>

data: {
    car: {
        brand: '奔驰',
        color: 'blue'
    }
}

演示:
原来页面展示:奔驰----blue-----

vm.car.price = 100
修改后页面展示:奔驰----blue-----100

delete vm.car.brand
修改后页面展示:----blue-----100

2. 初始化项目

如果想把自己2.0项目升级为3.0项目通过该命令升级即可: vue add vue-next

2.1 使用vue-cli创建vue3.0项目

  1. 安装vue-cli3
npm install -g @vue/cli
  1. 创建项目
vue create my-project
  1. 在项目中安装composition-api体验vue3新特性
npm install @vue/composition-api --save
  1. 在使用任何@vue/compositon-api提供的能力前,必须先通过Vue.use()进行安装
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)

2.2 使用vite创建vue3.0项目

  1. Vite是一个由原生ESM驱动的Web开发构建工具,在开发环境下基于浏览器原生ESimports开发,在生产环境下基于Rollup打包。

  2. Vite目前仅支持vue3.X,不兼容vue2.X项目。

  3. Vite基本使用

npm init vite-app 项目名称
cd 项目名称
npm install
npm run dev
  1. 扩展阅读: 有了vite,还需要webpack吗?

3. Composition API的使用

3.1 Composition API对比 options API

options API:

  1. 优点是容易学习和使用,代码有明确的书写位置。
  2. 缺点就是相似的逻辑不容易复用,在大项目尤为明显。
  3. 可以通过mixins提取相同的逻辑,但是容易发生命名冲突且来源不清晰。

Composition API:

  1. Composition API是根据逻辑功能来组织代码的,一个功能所有的api放到一起 。
  2. 即便项目很大,功能很多,能够快速的定位到该功能所有的api。
  3. Composition API提供了代码可读性和可维护性。

Vue3.0中推荐使用Composition API,也保留了options API。

options API:

image.png

Composition API:

image.png

3.2 setup的使用

  1. setup函数是一个新的组件选项,作为组件Composition API的起点
  2. 从生命周期钩子的角度来看,setup会在beforeCreate钩子函数之前执行
  3. setup中不能使用this,this的指向undefined

3.3 reactive的使用

reactive函数接受一个普通对象,返回该对象的响应式代理。

  1. setup中无返回的数据,这时就会报错。
举例说明: 
<p>{{car.brand}}</p>

setup() {
    // 定义数据
    const car = {
        brand: '宝马',
        price: 1000
    }
}

image.png

  1. 所以,setup中需要返回值,setup中返回的值才能在模版中使用。但是这时的数据并不是响应式的
举例说明: 
<p>{{car.brand}}</p>

setup() {
    // 定义数据
    const car = {
        brand: '宝马',
        price: 1000
    }
    return {car}
}

  1. reactive中传入一个普通对象,返回一个代理对象。
import {reactive} from 'vue'

setup() {
    // 定义数据
    const car = reactive({
        brand: '宝马',
        price: 1000
    })
    return {car}
}

这时的数据就是响应式的。

3.4 ref的使用

  1. ref函数接受一个简单类型的值,返回一个可改变的ref对象。返回的对象有唯一的属性value.

  2. 在setup函数中,通过ref对象的value属性可以访问到值。

  3. 在模版中,ref属性会自动解套,不需要额外的 .value。

  4. 如果ref接受的是一个对象,会自动调用reactive。

模板中:
// 在模板中使用ref,会自动解套,会自动调用value。
<div>我的金钱{{money}}</div>

import {ref} from 'vue'
setup() {
    // 定义数据
    // ref函数接受一个简单类型,返回一个响应式的对象
    let money = ref(100)
    
    // 这个响应式对象只有一个属性value
    // 通过ref接受简单的数据类型之后,返回新的值其实是一个对象。在下面中如果想使用需要.value访问。但是在模版中就不用.value
    money.value++
    return {money}
}

3.5 toRefs的使用

  1. 把一个响应时对象转换成普通对象, 该对象的每一个属性都是一个ref

  2. Reactive 的响应式功能是赋予给对象的,但是如果对象解构或者展开的时候,会让数据丢失响应式的能力

  3. 使用toRefs可以保证该对象展开的每一个属性都是响应式的

1. 数据都是分开写的,零散
setup() {
    const money = ref(100)
    const car = reactive({
        brand: '宝马',
        price: 100000
    })
    const XXXX
    
    return {money, car,.....}
}
这样会有个问题: 数据越来越多
2. 将数据都放在state中
setup() {
    const state = reactive({
        money: 100,
        car: {
             brand: '宝马',
            price: 100000
        }
    })
    return {...state}
}
这样会有个问题: 对象解构或者展开的时候,会让数据丢失响应式的能力
这时,money就丢失响应式的能力,car没有丢失,因为它还是一个对象。
如果再继续对car进行结构,那么car也丢失响应式的能力
3. 使用toRefs解决响应式能力丢失的问题
import {toRefs} from 'vue'
setup() {
    const state = reactive({
        money: 100,
        car: {
             brand: '宝马',
             price: 100000
        }
    })
    return {
        ...toRefs(state)
    }
}
使用toRefs传入对象,再解构,这时数据就是响应式的

3.6 readonly的使用

  1. 传入一个对象(响应式或普通)或ref,返回一个原始对象的只读代理
  2. 一个只读的代理是“深层的”,对内部任何嵌套的属性也都是只读的
  3. 可以防止对象被修改
模板:
<div>我的钱:{{money}}</div>
<div>{{car.brand}}----{{car.price}}</div>
<button @click=“money++”>按钮</button>
<button @click=“car.price = 200”>按钮</button>

setup() {
    const money = ref(100)
    const car = readonly({
        brand: '宝马',
        price: 100000
    })
    return {
        money:readonly(money)
        car
    }
}
这个时候执行click事件,就是不能修改的money的值的。也不能修改price的值

3.7 computed计算属性的使用

1. 概述

  1. computed函数用于创建一个计算属性
  2. 如果传入的是一个getter函数,会返回一个不允许修改的计算属性
  3. 如果传入的是一个带有getter和setter函数的对象,会返回一个允许修改的计算属性

2. 实现计算属性的方式

  1. 第一种实现计算属性的方式: 传入一个函数getter返回一个不允许修改的计算属性
setup() {
    const age = ref(18)
    
    const nextAge = computed(() => {
        return parseInt(age.value) + 1
    })
    return {
        age,
        nextAge
    }
}
不允许修改nextAge
  1. 第二种实现计算属性的方式(可以获取、修改): 传入一个对象,包括get和set,可以创建一个可以修改的计算属性
setup() {
    const nextAge2 = computed({
        get() {
            reutrn parseInt(age.value) + 2
        }
        set(value) {
            // value 就是修改的值
            age.value = value -2
        }
    })
    return {
        nextAge2
    }
}

3.8 watch监听属性的使用

1. 概述

  1. watch函数接受3个参数。

    1. 参数1 : 数据源,可以是ref或者getter函数
    2. 参数2 : 回调函数
    3. 参数3 : 额外选项,{deep: true, immediate: true}
  2. watch可以监听一个ref或者一个带有返回值的getter函数

  3. watch可以监听单个数据源,也可以监听多个数据源

  4. watch函数会有返回值,用于停止监听

2. 监听属性的使用

  1. 想要监听money的错误演示
模板:
<div>我的钱:{{money}}</div>
<div>{{car.brand}}----{{car.price}}</div>
<button @click=“money++”>按钮</button>
<button @click=“car.price = 200”>按钮</button>


setup() {
    const state = reactive({
        money: 100,
        car: {
             brand: '宝马',
             price: 100000
        }
    })
    watch(state.money, (value, oldValue) => {
        console.log('money变化了', value, oldValue)
    })
    
    return {
        ...toRefs(state)
    }
}
这样写会警告⚠️:可以监听一个ref或者一个带有返回值的getter函数

正确写法:() => state.money

watch(() => state.money, (value, oldValue) => {
        console.log('money变化了', value, oldValue)
    })
  1. 如果money是放在外面的,用到了ref。watch的写法
setup() {
    const money = ref(100)
    
    watch(money, (value, oldValue) => {
        console.log('money变化了', value, oldValue)
    })
    
    return {
        money
    }
}
  1. 监听复杂数据类型 {deep: true}
setup() {
    const state = reactive({
        car: {
             brand: '宝马',
             price: 100000
        }
    })
    watch(() => state.car, (value, oldValue) => {
        console.log('车变化了', value, oldValue)
    }, {deep: true, immediate: true})
    
    return {
        ...toRefs(state)
    }
}
  1. 监听多个值
setup() {
    const state = reactive({
        money: 100,
        car: {
             brand: '宝马',
             price: 100000
        }
    })
    watch([() => state.money, () => state.car], ([newMoney,newCar], [oldMoney,oldCar]) => {
        console.log('数据变化了', newMoney, newCar, oldMoney, oldCar)
    })
    
    return {
        ...toRefs(state)
    }
}
  1. 直接监听state
setup() {
    const state = reactive({
        money: 100,
        car: {
             brand: '宝马',
             price: 100000
        }
    })
    watch(state, (value) => {
        console.log('数据变化了', value)
    },{deep: true, immediate: true})
    
    return {
        ...toRefs(state)
    }
}

3.9 生命周期钩子函数

  1. vue3提供的生命周期钩子函数注册函数只能在setup()期间同步使用
  2. vue3生命周期钩子函数与vue2对比 beforeCreate ==> setup()

created ==> setup()

beforeMount ==> onBeforeMount

Mounted ==> onMounted

beforeUpdate ==> onBeforeUpdate

updated ==> onUpdated

beforeDestroy ==> onBeforeUnmount

destroyed ==> onUnmount

errorCaptured ==> onErrorCaptured

3.10 provide和inject使用

  1. vue3提供了provide和inject提供依赖注入,用于实现组件之间的通讯
  2. vue3提供的provide和inject可以用于跨多级组件惊醒通讯

父传子:

父组件
将money传递给子组件

模板:
<h1>我的钱:{{money}}</h1>
子组件
<Demo></Demo>


import {provide} from 'vue'
setup() {
    const money = ref(100)
    // 父组件向子组件传递数据
    provide('money', money)
    
    return {
        money
    }
}

子组件
接受传递过来的money

import {inject} from 'vue'
const money = inject('money')

子传父:

父组件:
setup() {
    const money = ref(100)
    // 定义事件传递给子组件
    const changeMoney = (m) => {
        money.value = m
    }
    provide('changeMoney', changeMoney)
    
    return {
        money
    }
}
模板:
<button @click=“fn”>按钮</button>

子组件:
setup() {
    const changeMoney = inject('changeMoney')
    
    const fn = () => {
        // 调用父组件的事件,并传值
        changeMoney(200)
    }
    
    return {
        fn
    }
}

说明:依旧可以用props传值,但是子组件有点区别

父组件
<Demo :money="money"></Demo>

子组件: 得用props参数接受(setup里面没有props)
setup(props) {
    console.log(props)
}

props: {
    money: Number
}

3.11 模板ref使用

  1. 为了获得对模板内元素或组件实例的引用,我们可以像往常一样在setup()中声明一个ref并返回它
模板:
// 绑定ref
<h1 ref="hRef"></h1>

setup() {
    // 创建了一个空的ref
    const hRef = ref(null)
    onMounted() {
        console.log(hRef.value)
    }
    
    return {
        hRef
    }
}