【经验总结】Vue3中的breaking changes & Vue3与Vue2响应式对比

274 阅读6分钟

本文可以算是一篇经验分享,在这对比了Vue2和Vue3中一些API的变化,总结Vue2响应式遇到的一些问题,分析Vue3中是如何解决的。

P.S. 如有不对,欢迎指正~,欢迎评论,点赞,收藏,哈哈哈🤩🤩🤩

Breaking changes

v-bind.sync被v-model arguments取代

从本质上来说,.sync和v-model做了一件事,就是双向数据绑定,它俩都是props+event的语法糖。
vue2中,v-model的属性默认绑定的是value,事件默认绑定的是input,如下图:

image.png

.sync修饰的prop将具有“双向绑定”的特性,不然的话你需要声明prop,然后通过@emit来让“子”显示地改变“父”中prop的值

image.png

image.png

到这有个小疑问,既然.sync做的事v-model都已经做了,为啥还要有.sync呢?
我觉得是因为一个组件上只能绑定一个v-model,但是要是这个组件还想要双向绑定其他的prop呢?那就得通过prop+event一个个写了,写太多了又嫌麻烦,看着能不能有一个简写版本出现,那就有了.sync语法糖。所以本质上还是在vue2中单个组件无法绑定多个v-model,那在vue3中,已经把v-model这个api进行了更改,使其更加的标准化

到了vue3,v-model变成了v-model argument, 可以给v-model绑定自定义的属性名,除此之外还支持给一个组件绑定多个v-model,这样v-model可以完全取代.sync了
P.S. vue2中v-model属性默认绑定的是value,事件默认绑定input,但在vue3中,属性默认绑定modelValue,事件默认绑定update:modelValue

image.png

image.png

Vue.extend

Toast,Confirm, Dialog 这种支持函数式调用的组件,其内部可以通过Vue.extend来实现。这里就写一个大致的实现步骤:

import Vue from 'vue'

// 参数是一个包含组件选项的对象
const Toast = Vue.extend({
    template: `<div v-if="show" @click="close">{{ message }}</div>`,
    props: {
        message: '',
    },
    data() {
        return {
            show: false,
        }
    },
    created() {
        this.show = true,
    },
    methods: {
        close() {
            this.show = false,
        },
    }
})

export function toast({message}) {
    const toastInstance = new Toast({
        message
    })
    const toastEle = toastInstance.$mount().$el
    document.body.appendChild(toastEle)
}

Vue.extend返回的是一个构造函数,这个构造函数的prototype是Vue.prototype, 所以说你要是在Vue.prototype上绑定了自定义的方法或属性,Vue.extend生成的构造函数的原型上也会存在。Vue.extend基本流程如下:

Vue.extend = function(extendOptions) {
    var Super = this;
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    
    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.use = Super.use;
    
    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type];
    });
    
    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    return Sub
}

Vue.extend生成的构造函数,需要通过new关键字来创建一个Vue实例对象,创建完后,需要手动调用$mount方法(来自于Vue.prototype),开启编译,最后把实例上的$el挂载到document.body上, 就大功告成了。 通过new Vue.extend生成的实例对象,已经脱离了通过new Vue()生成的根实例对象所营造的组件树了, 有点拗口,简单说就是使用new Vue.extend生成的组件树,和new Vue生成的组件树是两棵, 这两棵树之间没啥交集.

这就引出了一个问题,要是我想在这两棵树上共享一个数据咋办呢,比如共享store,共享router?
那就借助两者的公共父结点,去定义共享的数据状态,然后当做propData或者通过Vue.use, Vue.mixin共享到两个实例中。
P.S. 这块可分享的太多了,我慢慢来分享,请持续关注哦,也欢迎指正~

// 思路大致这样,仅供参考
const sharedStaff = {a:1}

const Ctor = Vue.extend(VueComponentsOption1)
const instance1 = new Ctor({
    sharedStaff
})
const instance2 = new Vue({
    ...VueComponentsOption2,
    sharedStaff
})

又有个问题,new Vue和new Vue.extend有啥区别呢,之前的toast的例子,我是不是用new Vue也能实现呀?
我感觉是的,都能生成一个组件instance,也都能使用Vue.prototype上的上的方法和属性,那我能找出来的不同是在语法层面,Vue是构造函数,Vue.extend是Vue构造函数上的静态方法,也就是说它不能访问Vue构造函数上定义的属性和方法。

不过到了Vue3,你就不用纠结它们的不同了,因为Vue.extend API被移除了,取而代之的是createApp
可以参考vant中dialog的写法 References:
github.com/youzan/vant…
github.com/youzan/vant…
P.S. 其中利用composable function实现usePopupState抽离popUp逻辑,也值得借鉴~

Reactivity

这里才是大头,也可以说是头大的部分,哈哈哈
Vue3中响应式涉及reactive,ref,toRef,toRefs etc.,这里我就先说这四个,听我娓娓道来~

Vue2中如何定义响应式数据

利用Option API的data选项,你完全不用考虑使用reactive,还是ref,只要你想用响应式数据,你就把它定义在data方法中。
不过也有情况是你不想用响应式数据,但你得在template中使用,你也得把它定义在this上,不然会出现报错,如下: image.png

Vue2中响应式的problems

  • 给对象动态添加一个属性时,不会有响应性 解决:使用Vue.set或者Vue实例的$setAPI, 动态添加属性,手动触发一次劫持,就可以在其更新的时候触发视图重新渲染了.
    通过$forceUpdate()也可以触发重新渲染,即使数据仍不是响应性的,但不妨碍获取它的最新的数据
  • 访问数组下标,改变数组length,都不能触发响应式拦截 解决:上一条说的仍适用,如下:
var vm = new Vue({
el:"#div",
  data: { items: ['a', 'b', 'c']}
});

Vue.set(vm.items,2,"d")

还可以通过操作以下7种Array方法,实现变化监听,push,pop,shift,unshift,splice,sort,reverse

  • vue2对data选项中的数据响应式处理,是通过递归的方式,如果你有一个嵌套层级比较深的对象,那它的每一层都会被处理,这说明vue2在背后做的比我们看到的要多的多。有时只需要对象第一层有reactive,但是这种处理方式就决定了,你不要reactive,我也得给你reactive。

  • 还有之前提到的,你不想用响应式数据,但你得在template中使用,你也得把它定义在this上,不然会报错

为什么会有problems呢?

  1. 追根溯源是因为Object.defineProperty这个API的一些不足,第一它只能拦截对象属性的变化,第二不支持array
  2. Vue源码中defineReactive实现,会对child再进行observe,var childOb = !shallow && observe(val)
    P.S. shallow为true就可以停止递归了,默认shallow是false.
  3. 由于this的存在,Vue组件中的东西得从this中取,很多方法都绑定了this, 我需要用,但是this上取不到,那就报错了

Vue3中解决了上面的问题

1. 通过Proxy来代理对象,可以代理对象操作的13种方法,它和Object.defineProperty完全不一样了,Object.defineProperty监听的是一个对象的属性,proxy监听的是整个对象,所以什么动态添加属性,改变数组下标,监听Map,Set,weakMap,weakSet都不再是问题。

image.png 图片来源:zhuanlan.zhihu.com/p/28665341

2. Vue3源码中,是按需进行响应式处理,不会一上来就对对象进行深层递归处理 那它是如何做的呢?是当你访问到深层对象时,才去做代理.
image.png

举个例子:

const reactiveData1 = {
  a: 1,
  first: {
    b: 2,
    second: {
      c: 3,
    },
  },
}
const state = reactive(reactiveData1)
console.log(state) // 只代理state,即它第一层的a和proxy
console.log(state.first) // 当获取state上的first时,触发Proxy上get的回调,在回调中再去对state.first的值进行Proxy拦截
console.log(state.first.second) // 同上


源码:

image.png

3. 对不需要响应式的数据,没必要进行响应式处理,通过setup导出即可在<template>上使用 在<template>中访问导入的依赖项或者常量,vue2中需要你在选项中定义它,可以定义在data中,或者在lifecyce hook里把对象挂载到this上, 但在vue3中,借助setup函数,完全可以在模板中使用。

image.png