公司项目使用的vue版本是2.7,所以本文都是基于这个版本来描述
之后会继续补充,或者新开一篇文章
vue2和vue3响应式的区别
- vue2使用的
Object.defineProperty,有很多局限性,有时候发现界面不刷新,可能就与这个有关- 需要对对象里的key进行遍历,性能差
- 无法对新增的字段自动增加响应式,需要额外用
Vue.set(obj,'a')手动添加
ts
const observe = (obj) =>{
for (const k in obj){
let v = obj[k];
if(isObject(v)){
// 递归处理对象的情况
observe(v)
}
Object.defineProperty(obj,k,{
get(){
return v;
}
set(val){
v = val;
}
})
}
}
- vue3使用
new Proxy,是针对对象本身。- 性能好,不用遍历key
- 不会漏响应式
ts
const reactive = (obj) =>{
const proxy = new Proxy(obj,{
get(target,k){
if(isObject(target[k])){
return reactive(target[k]);
}
return target[k];
},
set(target,k,v){
if(target[k] == v){
return;
}
target[k] = v;
}
})
return proxy;
}
弹窗组件要显示写出来才能用
- 比如我写了一个弹窗组件TestPopup.vue,那么我在每个使用的地方,都要在template里放入这个组件,并给它配上props,很麻烦
ts
<template>
...
<!--❌ 显示添加,并且每个地方都要维护visible状态-->
<TestPopup
v-model="visible"
...
/>
</template>
- 封装了一个函数,动态去挂载节点,弹窗组件符合v-model(用value和input事件),就可以使用
ts
import * as Vue from 'vue'
import { nextTick } from 'vue'
import type { VueConstructor } from 'vue'
interface UsePopupOptions {
onVisibleChange?: (val: boolean) => void
}
type EventCallback = (...args: any[]) => void
/**
* 通用弹窗 hooks,用于函数式调用弹窗组件
*
* 特性:
* - 无需在模板中引入组件,函数调用即可弹出
* - 支持响应式数据,传入 ref/reactive 时内容自动更新
* - 支持事件订阅,通过 subscribe 监听组件事件
* - 单例模式,多次 show 不会重复挂载
*
* 组件约定(适配 v-model):
* interface Props {
* value: boolean
* title?: string
* }
* const emit = defineEmits(['input', 'confirm'])
* // 点击关闭时
* emit('input', false)
*
* ‼️注意:
* - 模板(<template></template>)中使用 props 要写 props.xxx,或在 script 中定义 computed 变量
* - 不要直接在模板用 data,对于动态创建的组件响应式可能失效
*
* 用法:
* // onVisibleChange 可选,是否显示回调
* const popup = usePopup(MyPopup, { onVisibleChange: (val) => {} })
* popup.subscribe('confirm', () => {})
* popup.show({ title: '标题' })
* popup.close()
* popup.destroy()
*/
export function usePopup<T extends VueConstructor>(
PopupComponent: T,
options?: UsePopupOptions
) {
let instance: any = null
// 保存未挂载时的订阅事件,event 唯一
let pendingListeners: Record<string, EventCallback> = {}
const createInstance = () => {
if (instance) return
const PopupConstructor = Vue.extend(PopupComponent)
const container = document.createElement('div')
document.body.appendChild(container)
instance = new PopupConstructor({
propsData: {
value: false,
},
}).$mount(container)
// 监听 input 更新 value,并触发外部回调
instance.$on('input', (val: boolean) => {
if (val === instance._props.value) {
return
}
instance._props.value = val
options?.onVisibleChange?.(val)
})
// 挂载保存的订阅事件
Object.entries(pendingListeners).forEach(([event, callback]) => {
instance.$on(event, callback)
})
pendingListeners = {}
}
const subscribe = (event: string, callback: EventCallback) => {
// input 事件不允许通过 subscribe 订阅
if (event === 'input') {
console.warn(
'input event is not allowed to subscribe, use UsePopupOptions instead'
)
return () => {
// empty
}
}
if (instance) {
instance.$off(event)
instance.$on(event, callback)
} else {
// instance 不存在,先保存(同名覆盖)
pendingListeners[event] = callback
}
// 返回取消订阅函数
return () => {
if (instance) {
instance.$off(event, callback)
} else {
delete pendingListeners[event]
}
}
}
/**
* 显示弹窗,props 支持 ref/reactive 响应式数据
*/
const show = <D>(props?: D) => {
createInstance()
if (instance) {
// 合并 props 到 instance._props
if (props) {
Object.assign(instance._props, props)
}
// ⚠️下一个tick再显示组件,防止没有动画
nextTick(() => {
instance.$emit('input', true)
})
}
}
const close = () => {
if (instance) {
instance.$emit('input', false)
}
}
const destroy = () => {
if (instance) {
instance.$destroy()
if (instance.$el?.parentNode) {
instance.$el.parentNode.removeChild(instance.$el)
}
instance = null
}
pendingListeners = {}
}
return {
show,
close,
destroy,
subscribe,
}
}
组件的props不能从外面导入
- 在.vue文件(composition api)中,模板对应的props不能由外部导入。
- 如果外部要知道props的具体类型,就会变得很麻烦,我们注册成自定义vue组件,就得在其他地方再写一个一模一样的类型,如果组件里改了,还要同步改外面的,增加维护成本也容易忘改
ts
// ❌ 编译会报错
import TestProps from './types.ts'
const props = defineProps(TestProps)
- 用接口继承的方式,就可以定义在外部了
ts
//types.ts
export interface TestViewProps {
name: string
age: number
}
//TestView.vue
import { TestViewProps } from './types.ts'
// 如果由于interface继承后没有修改报错,加上下面这句忽略eslint报错
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Props extends TestViewProps {}
// ✅ 只用维护types里的定义就行
const props = defineProps(Props)
eslint-vue插件
- 在使用vue2.7,发现有些同学在使用ref的变量时,没有使用.value还是直接用ref对象。
ts
const name = ref("");
// ❌name.value是空,但是name本身是个对象是有值的
if(name){
console.log('name有值')
}
// ✅使用.value
- 为了避免这种情况发生,我做了一个eslint插件,检查ref对象在使用时,是否使用了.value,效果如下
- vue3的官方插件自带了这个检查,但是vue2没有

- 支持很多种情况,也排除了很多情况,具体可以看npm地址
- 安装方式
yarn add @azsxdc12356/eslint-plugin-vue-ref-value -D