前言:
最近基于vue3.0进行项目开发,做了前端埋点的特性.具体方案是通过vue的自定义指令,对真实的dom节点进行事件监听.但封装后的组件,内部的事件是通过$emit
触发,我们无法通过原生方式对这类事件进行捕获,以下是我寻找合适解决方案的过程.
$on方法
vue2.0中是可以通过$on
方法对同一实例上出发的$emit
进行捕获的,翻看了github上关于vue埋点方案,也是基于这个api进行实现的,但是遗憾的是vue3.0移除了这个实例方法.提出的替代方案mitt
或tiny-emitter
似乎也满足不了我们的需求.
奇怪的是,在我阅读3.0的源码时,居然依旧能找$on
的实现:
path:packages\runtime-core\src\compat\instanceEventEmitter.ts
const eventRegistryMap = /*#__PURE__*/ new WeakMap<
ComponentInternalInstance,
EventRegistry
>()
export function getRegistry(
instance: ComponentInternalInstance
): EventRegistry {
let events = eventRegistryMap.get(instance)
if (!events) {
eventRegistryMap.set(instance, (events = Object.create(null)))
}
return events!
}
export function on(
instance: ComponentInternalInstance,
event: string | string[],
fn: Function
) {
if (isArray(event)) {
event.forEach(e => on(instance, e, fn))
} else {
if (event.startsWith('hook:')) {
assertCompatEnabled(
DeprecationTypes.INSTANCE_EVENT_HOOKS,
instance,
event
)
} else {
assertCompatEnabled(DeprecationTypes.INSTANCE_EVENT_EMITTER, instance)
}
const events = getRegistry(instance)
;(events[event] || (events[event] = [])).push(fn)
}
return instance.proxy
}
export function emit(
instance: ComponentInternalInstance,
event: string,
args: any[]
) {
const cbs = getRegistry(instance)[event]
if (cbs) {
callWithAsyncErrorHandling(
cbs.map(cb => cb.bind(instance.proxy)),
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
return instance.proxy
}
复制代码
大体上的实现,就是通过WeakMap存储了实例对象,以及实例上定义的各种事件.$on
将事件推入对应的数组当中,$emit
将事件对应的回调数组进行执行.
再次在代码中遨游后,我发现了__COMPAT__
这个变量,似乎可以通过这个变量将$on
加入到实例上
if (__COMPAT__) {
installCompatInstanceProperties(publicPropertiesMap)
}
export function installCompatInstanceProperties(map: PublicPropertiesMap) {
......
extend(map, {
$on: i => on.bind(null, i),
$once: i => once.bind(null, i),
$off: i => off.bind(null, i),
} as PublicPropertiesMap)
......
}
复制代码
但即使根据rollup.config.js
中的代码,设置环境变量__COMPAT__=true
,在demo中依旧无法生效,打开node_modules
,发现vue3.0对外发布的代码是已经打包好的,这也能解释了为什么配置不生效的原因.
至此,看起来没有其他办法再使用$on
了.
vnode.props属性
基于上面的指令式埋点方案,在进一步探究vue源码的过程中,发现vue3.0使用的事件机制已经发生了变化,从下面这段代码中可以看出,$emit执行时,会读取当前实例的vnode下的props属性,并根据触发的事件进行查找,调用.
// packages\runtime-core\src\componentEmits.ts
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
const props = instance.vnode.props || EMPTY_OBJ
......
let handlerName
let handler =
props[(handlerName = toHandlerKey(event))] ||
// also try camelCase event handler (#2249)
props[(handlerName = toHandlerKey(camelize(event)))]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
}
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
......
}
复制代码
而通过自定义指令,我们是能拿到vndoe这个对象的,这里似乎大有可为.虽然官网说除el
外都应该保持只读,但都到这里了,不甘心放弃,还是打算试一试.通过对vnode.props赋值对应的事件,咦,真的成功了!!通过这个方法,的确能监听到组件内部的$emit('click')
.
const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('track', {
// 当被绑定的元素挂载到 DOM 中时……
created(el,binding,vnode ) {
vnode.props.onClick=function(){
console.log('测试埋点')
}
}
})
复制代码
当然不要高兴得太早,在测试的过程当中,发现了一个奇怪的现象,就是当有props传递的时候,直接赋值对应的事件是可以生效的.但是,当实例没有props时,我执行一下操作,最终是无法生效的!
//vnode.props===null -----true
vnode.props={}
vnode.props.onClick=function(){
console.log('测试埋点')
}
复制代码
看来还可以继续深究~通过patchEvent-->patchProp-->hostPatchProp
的反向查找,最终定位到了原因:
在最开始拿到了props的索引,当props初始化为null时,无法执行hostPatchProp
,当props为object时,在invokeDirectiveHook
阶段,我们对props进行了修改,hostPatchProp
能读取到我们后来添加的onClick
属性,并通过patchEvent
存储了对应的事件或者注册原生事件.
// packages\runtime-core\src\renderer.ts
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
//这里就是执行指令生命周期钩子的地方
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
}
}
复制代码
以上是vue3.0的埋点方案探索,其实在使用上和2.0的指令埋点是一致的,只是在寻找$on
的替代方案.
decorator
前面说的,都是基于vue的能力开发的埋点方案,无法普及到其他的应用框架.虽然埋点方案有代码埋点,可视化埋点,无埋点等三类方案.但是在业务场景中,往往采用代码埋点,开发成本小,贴近需求,可行性高.但是代码埋点往往需要对事件增加额外的代码,侵入业务逻辑,这是我们不想看到的.幸好es6推出了decorator,通过装饰器的形式,我们可以很方便得去隔离埋点代码和业务代码,不至于把两者写在一个函数体中.
export default {
methods: {
@track(someConfig)
onClick() {
console.log('触发clik')
}
}
}
复制代码
小结
本文主要探究了在vue2.0中$on
的一些相关代码,以及vue3.0下的指令埋点的一种方案.如果有更好的方案,也希望能解惑.如果文章中存在错误理解,请留言指正.