Vue总结系列一

公司项目使用的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
相关推荐
渐儿1 小时前
React Native 实操开发文档
前端
HYCS1 小时前
用pixijs实现fabricjs(三):对象继承链和自定义对象
前端·javascript·canvas
渐儿1 小时前
Electron 实操开发文档
前端
小则又沐风a1 小时前
深入了解进程概念 第二章
java·linux·服务器·前端
亲亲小宝宝鸭1 小时前
微前端方案探索:qiankun
前端·微服务
渐儿1 小时前
跨端框架实操开发文档:Electron / Tauri / React Native
前端
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_60:(表单与按钮技能测试实战)
服务器·前端·javascript·数据库·ui·html
lihaozecq1 小时前
做 Agent SDK 必须支持的插件能力:8 个钩子搞定横切关注点
前端·agent·ai编程
秦歌6661 小时前
Agent Skills详解
服务器·前端·数据库