浅析mpvue的事件代理系统

发布日期:2019-04-24

前言

说来惭愧,用 mpvue 大半年,小程序快一年了,居然还试图用 event.stopPropagation 方法阻止事件冒泡,也是有点蠢。痛定思痛,写篇博文来认真捋一捋小程序的事件系统和 mpvue 的事件代理

小程序事件系统,mpvue 事件系统和 DOM 事件系统的差别

从文档得知,小程序的 event 对象和 DOM 的 event 对象是不同的,我们还可以通过把两个对象的属性打印出来进行比较:

// 小程序 event 对象属性(8 个)["type", "timeStamp", "target", "currentTarget", "detail", "touches", "changedTouches", "_requireActive"]// DOM event 对象属性 / 方法(54 个)["isTrusted", "screenX", "screenY", "clientX", "clientY", "ctrlKey", "shiftKey", "altKey", "metaKey", "button", "buttons", "relatedTarget", "pageX", "pageY", "x", "y", "offsetX", "offsetY", ..., "cancelable", "timeStamp", "srcElement", "returnValue", "cancelBubble", "path", "composedPath", "stopPropagation", "stopImmediatePropagation", "preventDefault", "initEvent"]// mpvue event 对象属性 / 方法(9 个)["mp", "type", "timeStamp", "x", "y", "target", "currentTarget", "stopPropagation", "preventDefault"]

我们关注一下 stopPropagationpreventDefault 这两个在网页中常用的方法。从打印得到的属性来看,在小程序中的 event 对象中没有这两个方法,取而代之的是借助 catchtap 这样的语法来阻止事件冒泡。另外,小程序中没有什么方式来阻止默认事件。

但是在 mpvue 中,event 对象又被挂载了这两个方法。至于这两个方法是否可以真的有用,这就需要我们了解一下 mpvue 的事件代理机制。

mpvue 的事件代理

<cover-view id="zanIcon" catchtap="handleProxy" data-eventid="{{"4"}}" data-comkey="{{$k}}">

上面是mpvue生成的wxml文件片段,如果你看过 mpvue 的生成文件,可能会发现,在 mpvue 生成的 wxml 文件中,所有的事件都被一个叫 handleProxy 的函数接管,在 handleProxy 进行处理后再去调用我们写的真正的事件处理函数。这个方法在initMp时,作为小程序Page构造函数的一个选项,从而可以在wxml中被正确调用。函数的定义如下:

// https://github.com/Meituan-Dianping/mpvue/blob/63700b2d4e4e678bc4297c71e3acd1f36647907a/src/platforms/mp/runtime/lifecycle.js#L286-L288handleProxy (e) { return rootVueVM.$handleProxyWithVue(e) }// https://github.com/Meituan-Dianping/mpvue/blob/63700b2d4e4e678bc4297c71e3acd1f36647907a/src/platforms/mp/runtime/index.js#L53-L54import { handleProxyWithVue } from "./events"Vue.prototype.$handleProxyWithVue = handleProxyWithVue // handleProxyWithVue实现在 events.js 中

从上面的代码可以看出,handleProxy只是将小程序的event对象传给handleProxyWithVue函数进行进一步处理。接下来我们看看handleProxyWithVue都做了什么工作:

// https://github.com/Meituan-Dianping/mpvue/blob/63700b2d4e4e678bc4297c71e3acd1f36647907a/src/platforms/mp/runtime/events.js#L84-L108export function handleProxyWithVue (e) { const rootVueVM = this.$root const { type, target = {}, currentTarget } = e const { dataset = {}} = currentTarget || target const { comkey = "", eventid } = dataset const vm = getVM(rootVueVM, comkey.split(","))// 根据comkey找到页面对应的mpvue实例(vm) if (!vm) { return } const webEventTypes = eventTypeMap[type] || [type] const handles = getHandle(vm._vnode, eventid, webEventTypes) //找到真正的事件处理函数 if (handles.length) { const event = getWebEventByMP(e) // 包装为mpvue的event对象 if (handles.length === 1) { const result = handles[0](event) return result } handles.forEach(h => h(event))// 调用真实的事件处理函数 }}

handleProxyWithVue做了以下几件事情:

    从根实例开始,根据comkey找出事件处理函数所在的mpvue实例(getVm)

    通过遍历找到的vm的vnode,结合eventid找到小程序事件对应的真实的事件处理函数(getHandle)

    将小程序的event对象包装为mpvue的event对象(getWebEventByMP),并添加了preventDefault和stopPropagation方法

    将包装后的event对象传给真实的处理函数进行调用

这就解释了生成的wxml中绑定事件的节点都有event-comkeyevent-eventid属性,在一个事件触发时,它们是被用来寻找事件对应的vm实例和对应的事件处理函数的。

最后我们来看看getWebEventByMP函数:

// https://github.com/Meituan-Dianping/mpvue/blob/63700b2d4e4e678bc4297c71e3acd1f36647907a/src/platforms/mp/runtime/events.js#L62-L82function getWebEventByMP (e) { const { type, timeStamp, touches, detail = {}, target = {}, currentTarget = {}} = e const { x, y } = detail const event = { mp: e, type, timeStamp, x, y, target: Object.assign({}, target, detail), currentTarget, stopPropagation: noop, preventDefault: noop } if (touches && touches.length) { Object.assign(event, touches[0]) event.touches = touches } return event}

从上面代码可以看出,stopPropagation和preventDefault都对应到一个叫noop的函数,而这个函数是一个空函数,也就是说在mpvue中,尽管可以调用event.stopPropagation(),但是并没有什么用,还是要借助.stop修饰符通过compiler编译成catchEvent来阻止冒泡。(完)