本文可以算是一篇经验分享,在这对比了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,如下图:
.sync修饰的prop将具有“双向绑定”的特性,不然的话你需要声明prop,然后通过@emit来让“子”显示地改变“父”中prop的值
到这有个小疑问,既然.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
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上,不然会出现报错,如下:
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呢?
- 追根溯源是因为Object.defineProperty这个API的一些不足,第一它只能拦截对象属性的变化,第二不支持array
- Vue源码中defineReactive实现,会对child再进行observe,
var childOb = !shallow && observe(val)
P.S. shallow为true就可以停止递归了,默认shallow是false. - 由于this的存在,Vue组件中的东西得从this中取,很多方法都绑定了this, 我需要用,但是this上取不到,那就报错了
Vue3中解决了上面的问题
1. 通过Proxy来代理对象,可以代理对象操作的13种方法,它和Object.defineProperty完全不一样了,Object.defineProperty监听的是一个对象的属性,proxy监听的是整个对象,所以什么动态添加属性,改变数组下标,监听Map,Set,weakMap,weakSet都不再是问题。
图片来源:zhuanlan.zhihu.com/p/28665341
2. Vue3源码中,是按需进行响应式处理,不会一上来就对对象进行深层递归处理
那它是如何做的呢?是当你访问到深层对象时,才去做代理.
举个例子:
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) // 同上
源码:
3. 对不需要响应式的数据,没必要进行响应式处理,通过setup导出即可在<template>上使用
在<template>中访问导入的依赖项或者常量,vue2中需要你在选项中定义它,可以定义在data中,或者在lifecyce hook里把对象挂载到this上, 但在vue3中,借助setup函数,完全可以在模板中使用。