Vue3 → 内部属性解析 - 事件注册、Emit注册

855 阅读2分钟

事件注册

原理解析

在Vue3使用事件的逻辑中,是通过onXxxx的方式进行注册的,在普通的template模版中是通过v-on的方式进行注册的,编译后会以onXxxx的形式打到VNode中的prop中;

  • 格式规范
    • onEvent事件必须在props中定义的
    • 事件的格式必须是on+Event的格式
  • 事件缓存
    • Vue3通过检验props中的数据是否满足/^on[A-Z]/i这个正则格式来进行事件注册
    • 通过对当前的el增加一个属性实现事件缓存的功能,具体是添加例如el._vei || (el._vei = {})来进行缓存,当存在于el._vel中时则直接使用,不存在时则创建并存入缓存

render中的this问题

在事件绑定中,会有访问setup中的数据问题,也就存在了this问题,因此在调用render的时候,需要改当前的this指向问题

相关原理

this不仅需要代理到setup中返回的数据,还需要访问到例如elpropsSlots等,所以可以通过proxy来进行代理实现,具体实现参考链接文章 链接地址

参考文献

Vue3 → 初始化Component与element

相关技术逻辑

h 函数

在Vue3中,h函数中的props中有onXxxx的事件时,会作为事件监听器为响应的DOM天假监听事件

  • 使用方式
export const App = {
  render() {
    window.self = this;
    return h(
      "div",
      {
        id: "123",
        class: ["lbxin", "lbxin-active"],
        onClick(){
          console.log(123,'onClick=========',this.msg)
        },
        onMousedown(){
          console.log(333,'onMousedown=========',this.age)
        }
      },
      "name " + this.msg + " age:" + this.age
      //Proxy代理this.setup返回的值 代理this.$el(根实例 - root element)的值
      // [h("p",{class:'red'},'hi'),h("span",{class:"blue"},"Lbxin")]
    );
  },
  setup() {
    // 返回当前组件的数据
    return {
      msg: "Lbxin",
      age: 12,
    };
  },
};
对props中的事件进行特殊处理

根据事件的特点,事件名是以on开头,且on后的事件是驼峰的形式命名,因此可以通过正则的形式进行所有事件的代理拦截,而非单一拦截操作;
处理props是在mountElement逻辑中处理的,所以需要添加兼容逻辑处理;

任何没有显示的声明为props的属性都会存储到attrs中,而非props中,定义在组件自身的数据会存储为props,为组件传递vNode.props没有定义在组件中的数据会视为attrs

function resolveProps(options, propsData) {
  const props = {};
  const attrs = {};
  for (const key in propsData) {
    // 以字符串 on 开头的 props,无论是否显式地声明,都将其添加到 props 数据中,而不是添加到 attrs 中
    if (key in options || key.startsWith("on")) {
      props[key] = propsData[key];
    } else {
      attrs[key] = propsData[key];
    }
  }

  return [props, attrs];
}
  • props 规则浅析
    • 父组件传递给子组件的参数,可以给到子组件的setup的第一个参数
      • 在setup函数调用的时候,将当前组件的props传入到setup函数中即可,通过new Proxy进行代理实现
    • 在子组件的render函数中,可以使用this来访问props的值
      • 通过代理实现后,将新生成的proxy挂载到实例instance上,在进行组件render调用时进行改变this指向到instance.proxy即可 - 所有可以通过this访问的都可以通过该方法进行实现,例如setup中的data数据等
    • 子组件中不允许修改props的值 - 即是shallowReadonly类型的
      • 在进行setup调用时需要注意将props的值变成只读的,即setup(shallowReadonly(instance.props)); createGetter(true = false, shallow = true)
  • 常见代理数据汇总
/**
  const publicPropertiesMap = extend(Object.create(null), {
    $: i => i,
    $el: i => i.vnode.el,
    $data: i => i.data,
    $props: i => (shallowReadonly(i.props) ),
    $attrs: i => (shallowReadonly(i.attrs) ),
    $slots: i => (shallowReadonly(i.slots) ),
    $refs: i => (shallowReadonly(i.refs) ),
    $parent: i => getPublicInstance(i.parent),
    $root: i => getPublicInstance(i.root),
    $emit: i => i.emit,
    $options: i => (resolveMergedOptions(i) ),
    $forceUpdate: i => () => queueJob(i.update),
    $nextTick: i => nextTick.bind(i.proxy),
    $watch: i => (instanceWatch.bind(i) )
  })
**/

mountElement的加载顺序是render -> patch -> mountElement

function processElement(vnode: any, container: any) {
  mountElement(vnode, container)
}
function mountElement(vnode: any, container: any) {
  // vnode → element → div
  const { children, props, type, shapeFlag } = vnode
  const el = (vnode.el = document.createElement(type))

  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode, container)
  }
  // mountElement 中处理props
  for (const key in props) {
    if (Object.prototype.hasOwnProperty.call(props, key)) {
        const val = props[key];
      // if(key  === 'onClick'){ // 具体的状态 后续转换成通用的拦截
      //   el.addEventListener("click",val)
      // }
      // on + Event name  → onMouncedown onClick...
      const isOn = (key:string) => /^on[A-Z]/.test(key)
      // const isOn = (p:string) => p.match(/^on[A-Z]/i)
      if(isOn(key)){
        const event = key.slice(2).toLowerCase()
        el.addEventListener(event,val)
      } else {
        el.setAttribute(key, val)
      }
    }
  }
  container.append(el)
}

Emit注册

原理解析

emit用来发射组件的自定义事件

自定义事件会被编译成onEvent的形式,并存储在props中,其本质是根据事件名称去props数据对象中寻找对应的事件处理函数并执行

  • Emit 规则解析
    • emit的参数是在父组件的props里,且是以on+Event的格式
      • 可以在setup函数调用时传入第二个参数
    • emit作为setup第二个参数,且可以解构出来使用
    • emit函数里是触发事件的,事件名称,事件名称格式是小写或者xxx-xxx的格式
      • 将函数名进行判断转换,将xxx-xxx转换为xxxXxx的形式,然后后续操作与事件注册类似
    • emit函数后续可以传入多个参数,作为父组件callback的参数
      • 通过剩余参数的形式进行传递调用
      export function emit(instance,event,...args){
        // ...
        const handler = props[toHandlerKey(camelize(event))]
        handler && handler(...args)
        // ...
      }
      

内部实现

  • 外部使用逻辑
import { h } from "../../lib/guide-mini-vue.esm.js"

export const Foo = {
    setup(props,{emit}){
        const emitAdd = () => {
            console.log('emitAdd=========') 
            emit("add",1)
            emit("add-foo",2)
        }

        return {
            emitAdd
        }
    },
    render() {
        const btn = h("button",{
            onClick:this.emitAdd
        },"emitAdd")
        
        return h("div",{},[btn,h("div",{},"foo")])
    }
}
  • 在setup函数执行的时候,传入第二个参数
if (setup) {
  // setup可以返回函数或对象 函数-是组件的render函数 对象-将对象返回的对象注入到这个组件上下文中
  const setupResult = setup(shallowReadonly(instance.props),{
    emit: instance.emit
  });
  // setup返回当前组件的数据
  handleSetupResult(instance, setupResult);
}
  • 完整代码
// emit内部转换props中的注册事件名
export function emit(instance, event, ...args) {
  const { props } = instance;

  // TPP  先写一个特定的行为 =》 后重构成通用行为
  // event : add => onAdd
  const capitalize = (str: string) => {
    return str.charAt(0).toUpperCase() + str.slice(1);
  };

  // add-foo => addFoo
  const camelize = (str: string) => {
    return str.replace(/-(\w)/g, (_, c: String) => {
      return c ? c.toUpperCase() : "";
    });
  };

  const toHandlerKey = (str: string) => {
    return str && "on" + capitalize(str);
  };
  const handler = props[toHandlerKey(camelize(event))];
  handler && handler(...args);
}

export function createComponentInstance(vnode) {
  const component = {
    vnode,
    type: vnode.type,
    props: {},
    emit: ()=>{},//实例配置中添加对应的入口
    render: vnode.render,
    setupState: {},
  };
  component.emit = emit.bind(null,component) as any
  return component;
}
// 处理setup的信息 初始化props  初始化Slots等
export function setupComponent(instance) {
  initProps(instance,instance.vnode.props),
  // initSlots() ToDo
  setupStatefulComponent(instance);
}

//调用setup逻辑 实现数据的代理与结果获取处理
function setupStatefulComponent(instance: any) {
  // 调用组件的setup
  // const Component = instance.vNode.type
  const Component = instance.type;
  instance.proxy = new Proxy(
    { _: instance },
    PublicInstanceProxyHandlers
    // {
    //     get(target,key){
    //         const { setupState } = instance
    //         if(key in setupState){
    //             return setupState[key]
    //         }

    //         if(key === '$el'){
    //             return instance.vnode.el
    //         }
    //     }
    // }
  );
  const { setup } = Component;

  if (setup) {
    // setup可以返回函数或对象 函数-是组件的render函数 对象-将对象返回的对象注入到这个组件上下文中
    const setupResult = setup(shallowReadonly(instance.props),{
      emit: instance.emit
    });
    // setup返回当前组件的数据
    handleSetupResult(instance, setupResult);
  }
}