事件注册
原理解析
在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中时则直接使用,不存在时则创建并存入缓存
- Vue3通过检验props中的数据是否满足
render中的this问题
在事件绑定中,会有访问setup中的数据问题,也就存在了
this问题,因此在调用render的时候,需要改当前的this指向问题
相关原理
this不仅需要代理到setup中返回的数据,还需要访问到例如el、props、Slots等,所以可以通过proxy来进行代理实现,具体实现参考链接文章 链接地址
参考文献
相关技术逻辑
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进行代理实现
- 在setup函数调用的时候,将当前组件的props传入到setup函数中即可,通过
- 在子组件的
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)
- 在进行
- 父组件传递给子组件的参数,可以给到子组件的setup的
- 常见代理数据汇总
/**
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) // ... }
- emit的参数是在
内部实现
- 外部使用逻辑
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);
}
}