Vue开发尝试一下

118 阅读5分钟

1. provide传多个函数/属性

  • 常规

    // parent.vue
    const key = Symbol('sonContext')
    provide(key,{
        fn1:()=>{},
        fn2:()=>{}
    })
    // son.vue
    const context = inject(key)
    context?.fn1()
  • 进阶

// 封装成hook,调用方只关注传值即可,key在hook维护与组件解耦
import { inject, provide } from 'vue'
function useCreateProvide(key){
    // key也可以不传写死,每次调用都是最新的Symbol
    // const key = Symbol('provideKey')
    return [
        function useProvide(value){
            provide(key,value)
        },
        function useInject(defaultValue){
            return inject(key,defaultValue)
        }
    ]
}
const [useUserProvide,useUserInject] = createProvideState(Symbol('user'))
const [useSonProvide,useSonInject] = createProvideState(Symbol('son'))
export { useUserProvide, useUserInject, useSonProvide, useSonInject }

2.异步组件

  • 使用场景
// 场景  组件从远程js获取 需要二次处理
import { h } from 'vue'
const items = [
    {type:'input',key:'name'},
    {url:'https://cdn.jsdelivr.net/npm/element-plus@2.10.4/es/components/affix/index.mjs'}
]
const Comp = {
  setup(_, { attrs, slots }) {
    const component = () =>
      defineAsyncComponent(() => {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve({
              render() {
                return h('div', 'hello world')
              },
            })
          }, 1000)
        })
      })
    return h(component, attrs, slots)
  },
}
  • 简单实现一下低代码时远程获取组件
const Comp = {
  props:['item'],
  setup(props){
    const component = ref(()=>return h('div',''))
    const url = props.item.url || 'https://unpkg.com/ant-design-vue/lib/index.js'
    const name = props.item.name || 'AButton'
    function loadScript(){
      import(url).then(module=>{
        const Comp = module[name]
        component.value = Comp
      })
    }
    loadScript()
    return h(component.value)
  }
}
  • 有了上面的基础来实现defineAsyncComponent
//loader是个函数返回promise resove一个组件
function defineAsyncCompont(loader) {
  return {
    setup() {
      const componet = ref(() => h('span'))
        loader().then((res) => {
         // 处理一下 defineAsyncCompont(import('./Button.js'))
          if (res && res[Symbol.toStringTag] === 'module') {
            res = res.default
          }
          componet.value = res
        })
      return () => {
        return h(componet.value)
      }
    },
  }
}

  • 进阶(defineAsyncComponent接收对象可定义loader和loading error组件)
import { h } from 'vue'
const Comp = {
  setup(_, { attrs, slots }) {
    const component = () =>
      defineAsyncComponent({
        loader: () => import('./ceshi.vue'),
        loadingComponent: {
            render(){
                return h('div','loading')
            }
        },
        errorComponent: () => import('./error.vue'),
      })
    return h(component, attrs, slots)
  },
}

  • 终极版实现异步组件
export function defineAsyncComponent(options) {
  if (typeof options === 'function') {
    options = {
      loader: options,
    }
  }

  // 占位符组件
  const defaultComponent = () => h('span')

  const {
    loader,
    loadingComponent = defaultComponent,
    errorComponent = defaultComponent,
    timeout,
  } = options

  return {
    setup(_, { attrs, slots }) {
      const component = shallowRef(loadingComponent)

      function loadComponent() {
        return new Promise((resolve, reject) => {
          /**
           * 到点了,还没完成,我就调用 reject
           */
          if (timeout && timeout > 0) {
            setTimeout(() => {
              reject('超时了')
            }, timeout)
          }

          loader().then(resolve, reject)
        })
      }
      // 如果函数返回promise可以拦截一下,自己写promise做一些拦截操作
      /**
       * loader 函数返回一个 Promise
       * 但是这个 Promise 我们不能控制它的成功和拒绝
       */
      loadComponent()
        .then((res) => {
          if (res && res[Symbol.toStringTag] === 'Module') {
            res = res.default
          }
          /**
           * 1000 ms 会进来
           */

          component.value = res
        })
        .catch(() => {
          // 组件加载失败了,改为 defaultComponent
          component.value = errorComponent
        })

      return () => {
        return h('div', [h(component.value, attrs, slots)])
      }
    },
  }
}

2.封装权限组件

指令销毁只销毁了dom,但是js全部会执行,dom虽然销毁了,但是创建的对象还在,引用还在,ref暴露的方法依然会被外部引用执行。 比如

比如列表上加了v-auth,那列表的dom还在, v-if是编译时,自己写的指令是运行时。所以v-auth做不到v-if的效果

  • v-auth指令

const a = document.createElement('div') document.body.appendChild(a) document.body.remoreChild(a) // 虽然把a dom移除了但是创建的a还在 所以创建一个 v-auth函数式组件,使用插槽 控制slot显示。

  • 插槽实现

思路:定义一个组件,父组件使用V-Auth如果有权限就渲染 默认插槽,否则返回空节点
const haveAuth = isAdmin
if(!haveAuth) return ()=>null
 const VAuth = (props,{slots})=>{
     return  ()=> haveAuth ? slots.default() :null
 }

3.vei的妙用和symbol带来的思考

  • invoker

常规事件换绑 document.addEventListener('click',handler) 如果patch时事件函数变成了handler2,此时需要先进行removeListener

  • dom监听事件换绑

 vei是vue event invocker的缩写,用来做patch event时事件的换绑。
function createInvocker(handler){
    const invocker = (e)=>{invoker.value(e)}
    invocker.value = handler
    return invocker
}
const invoker = createInvoker(newHandler)
document.addEventListener('click',invoker) 只绑定一次即可换绑事件
vei做了存储 const invokers = el.evi??={}  通过存起来事件名和事件处理函数,
下次进来先找有没有之前的有就直接替换,没有就正常创建invocker。

思考:为什么vei会用invoker这样做呢,因为在很多场景下需要 保持同一个函数引用但支持逻辑替换
譬如上面的事件换绑,保持同一个引用才可以被remove移除掉监听,由此我想了下,定时器也可以采用invoker的模式,

  • 定时器换绑

 function createInvoker(val){
     const invoker = (...args)=>invoker.value(...args) // 为了保持通用 是传一下参数
     invoker.value = val 
     return invoker
 }
 const invoker=ceateInvoker(()=>{console.log('执行逻辑1')})
 const timer = setInteval(()=> invoker,1000) 
 // 此时如果需要更换定时器的逻辑可以直接修改invoker.value 不要换绑时每次都清楚定时器
 invoker.value = ()=>{console.log('执行逻辑2')}
 unMounted(){clearInterVal(timer)}
  • eventBus.on的handler

// 初始化函数返回true
// createInvoker和上面一样就不写了
const authInvoker = createInvoker = ()=>{return true}
function getAuth(){
    ... 获取权限接口
    authInvoker.value = (user)=>{user.row === 'admin'}
}
EventBus.on('action',authInvoker)   
// 这样做下次需要更换获取权限逻辑时不需要解绑之前的on监听
  • 拓展 实现可支持换绑的debounce

 function createInvokerDebounce(fn,delay){
     const invoker = (...args)=>{
         clearTimeout(invoker._timer)
         invoker._timer  =setTimeout(()=>{
             invoker.value(...args)
         },delay)
     }
     invoker._timer = null // 保证结构一致,否则初始化调用 clearTimeout可能会报错
     invoker.value = fn
     const timer = setTimeout(()=>invoker,delay)
 }
 
 案例
 const searchInvoker = createDebouncedInvoker((e) => {
  console.log('搜索:', e.target.value);
}, 500);
input.addEventListener('input', searchInvoker);
// 想动态切换逻辑?只需:
searchInvoker.value = (e) => {
  console.log('新逻辑:记录历史输入 →', e.target.value);
};

当然这是原生操作需要这样做,vue中onclick 事件经过了vei优化,已经具备了动态更换事件
  • symbol

这里将el.evi优化成el[eviKey] eviKey = new Symbol('evi') 原因是此属性不要显示被用户看到,不能被.访问,同时由于symbol属性不能被 for in Object.keys()拿到。相对安全(Object.getOwnPropertySymbols(obj) 依然可以拿到)Object只能用字符串和Symbol当key,所以面试时候如果问起Symbol的用法,或许此处是一个不错的回答。

4. 轻量级状态管理

有个项目起步需要用到状态管理,考虑到项目不是很大,不想加一些损耗不考虑用库,同事建议我用一个全局的ref。我想起来pinia是传入函数导出一个对象,用ref不是很合适。所以我想用闭包实现一个类似pinia的状态管理。

思路:函数执行时将结果用闭包存起来,有则返回。保证返回同一个对象。后面因为我的逻辑有打开弹窗的逻辑(CreateApp),写了很多watch的业务逻辑,组件销毁时如果每个watch都单独调stop太麻烦,所以我想到了用effectscope(true 代表隔离上下effectscope作用域互不影响,flase作关联清除) ,把函数用scope.run包一下,unMounted里调effect.stop 清除所有effect。 为什么要手动调watch因为非组件上下文中的watch不会自动销毁,比如js函数的watch或者createApp弹窗中的watch

const createGlobalState = (fn)=>{
    let state;
    const scope = effectScope(true)
    return () => {
        if(state)return state
        state = scope.run(fn)
        return state
    }
}

const useGlobalState = (fn)=>{
   const count = ref(0)
   function increment() {
       count.value++
   }
   const currentScope = getCurrentScope()
   return {count, increment,stop:currentScope.stop}
}

TodoList

  • 预加载插件