mpvue 事件代理源码分析
mpvue data 注入
之前我们分析过,mpvue 通过全局事件代理的方式,实现事件的响应,事件响应过程中,通过 data 中的 eventid 和 comkey 区分事件和组件,那么 eventid 和 comkey 是如何注入 data 中的呢?要搞清楚这个问题,需要了解 mpvue 如何向 page 的 data 注入这些属性。我们仍然沿用之前的案例:
<template>
<div>
{{message}}
<button @click="change">change</button>
</div>
</template>
<script>
export default {
data () {
return {
message: 'Hello miniprograme'
}
},
methods: {
change () {
this.message = 'change text'
}
}
}
</script>
在这个案例中我们定义了一个按钮,并且绑定了 change 事件。下面我们来分析 data 注入的源码,data 注入的源码位于 this._initMp 方法中,这个方法非常重要,我们之前反复分析过:
var app = global.getApp();
global.Page({
data: {
$root: {}
},
handleProxy: function handleProxy (e) {
return rootVueVM.$handleProxyWithVue(e)
},
onShow: function onShow () {
mp.page = this;
mp.status = 'show';
callHook$1(rootVueVM, 'onShow');
rootVueVM.$nextTick(function () {
rootVueVM._initDataToMP();
});
}
}
该方法通过 global.Page 创建小程序页面,默认定义了 data 中只包含一个 $root 属性,该属性是一个空对象。page 下还定义了一个 handleProxy 方法,该方法即事件代理分析,这个方法我们会在下一节进行详细解析。这里我们需要关注 onShow 中的 rootVueVM._initDataToMP() 方法,该方法实现了 data 的注入,该方法源码如下:
function initDataToMP () {
var page = getPage(this);
if (!page) {
return
}
var data = collectVmData(this.$root);
page.setData(data);
}
首先,该方法会 getPage 获取当前页面实例,之前章节中我们提到过 mpvue 的 page 实例位于:this.$mp.page,该实例注入是在 page 的 onLaunch 生命周期函数中:
onLoad: function onLoad (query) {
mp.page = this;
// ...
},
之后通过 collectVmData 生成 data 数据:
var data = collectVmData(this.$root);
collectVmData 的源码如下:
function collectVmData (vm, res) {
if ( res === void 0 ) res = {};
var vms = vm.$children;
if (vms && vms.length) {
vms.forEach(function (v) { return collectVmData(v, res); });
}
return Object.assign(res, formatVmData(vm))
}
collectVmData 通过 vm.$children 获取 mpvue 实例的子组件,然后依次遍历迭代调用自身完成子组件的 data 初始,而 collectVmData 方法最终通过调用 formatVmData 生成 data,formatVmData 源码如下:
function formatVmData (vm) {
var $p = getParentComKey(vm).join(KEY_SEP);
var $k = $p + ($p ? KEY_SEP : '') + getComKey(vm);
var data = Object.assign(getVmData(vm), { $k: $k, $kk: ("" + $k + KEY_SEP), $p: $p });
var key = '$root.' + $k;
var res = {};
res[key] = data;
return res
}
最终生成的 res 数据结果如下:
$root.0: {
$k: "0"
$kk: "0_"
$p: ""
message: "Hello miniprograme"
}
$k 即组件的 comkey,$kk 为父组件的前缀,$p 这里没有用到,所以为空。获得 data 之后,mpvue 调用 page.setData(data) 完成 data 的注入。
mpvue 事件代理
搞清楚了 data 注入的过程,我们就可以分析 mpvue 事件代理机制了,我们看下 button 的 DOM 结构:
<button
bindtap="handleProxy"
class="_button data-v-4cf53cc1"
data-comkey="0"
data-eventid="0"
role="button"
aria-disabled="false"
>
change
</button>
点击 button 时,调用了 bindtap 绑定事件 handleProxy,handleProxy 的源码如下:
handleProxy: function handleProxy (e) {
return rootVueVM.$handleProxyWithVue(e)
}
我们点击事件实际调用 $handleProxyWithVue,其源码如下:
Vue$3.prototype.$handleProxyWithVue = handleProxyWithVue;
function handleProxyWithVue (e) {
var rootVueVM = this.$root;
var type = e.type;
var target = e.target; if ( target === void 0 ) target = {};
var currentTarget = e.currentTarget;
var ref = currentTarget || target;
var dataset = ref.dataset; if ( dataset === void 0 ) dataset = {};
var comkey = dataset.comkey; if ( comkey === void 0 ) comkey = '';
var eventid = dataset.eventid;
var vm = getVM(rootVueVM, comkey.split(KEY_SEP$2));
if (!vm) {
return
}
var webEventTypes = eventTypeMap[type] || [type];
var handles = getHandle(vm._vnode, eventid, webEventTypes);
if (handles.length) {
var event = getWebEventByMP(e);
if (handles.length === 1) {
var result = handles[0](event);
return result
}
handles.forEach(function (h) { return h(event); });
}
}
第一部分源码如下:
var rootVueVM = this.$root;
var type = e.type;
var target = e.target; if ( target === void 0 ) target = {};
var currentTarget = e.currentTarget;
var ref = currentTarget || target;
var dataset = ref.dataset; if ( dataset === void 0 ) dataset = {};
var comkey = dataset.comkey; if ( comkey === void 0 ) comkey = '';
var eventid = dataset.eventid;
var vm = getVM(rootVueVM, comkey.split(KEY_SEP$2));
这段代码的主要目的有以下两点:
- 获得 comkey 和 eventid:comkey 获取路径为: e.type.target.dataset.comkey,eventid 与之类似;
- 通过 comkey 获得其对应的 vm:页面包含多个组件时,mpvue 会通过 comkey 找到对应的组件实例,通过 getVM 方法实现。这里由于不存在额外的组件,所以 vm 等于 rootVueVM。
第二部分源码如下:
var webEventTypes = eventTypeMap[type] || [type];
var handles = getHandle(vm._vnode, eventid, webEventTypes);
这段代码的主要目的是通过 eventid 找到点击事件对应的处理方法,即想办法找到我们在 mpvue 中定义在 methods 下的 change 方法。第一行代码中的 eventTypeMap 主要用来映射事件类型,它是一个枚举型对象:
var eventTypeMap = {
tap: ['tap', 'click'],
touchstart: ['touchstart'],
touchmove: ['touchmove'],
touchcancel: ['touchcancel'],
touchend: ['touchend'],
longtap: ['longtap'],
input: ['input'],
blur: ['change', 'blur'],
submit: ['submit'],
focus: ['focus'],
scrolltoupper: ['scrolltoupper'],
scrolltolower: ['scrolltolower'],
scroll: ['scroll']
};
通过 e.type 可以获得事件类型为 tap,在 eventTypeMap 范围内,所以是可以处理的,如果超出 eventTypeMap 范围的事件 mpvue 将不能对其进行处理。getHandle 是获得事件的核心方法,其核心源码如下:
function getHandle (vnode, eventid, eventTypes) {
var res = [];
var ref = vnode || {};
var data = ref.data; if ( data === void 0 ) data = {};
var children = ref.children; if ( children === void 0 ) children = [];
children.forEach(function (node) {
res = res.concat(getHandle(node, eventid, eventTypes));
});
var attrs = data.attrs;
var on = data.on;
if (attrs && on && attrs['eventid'] === eventid) {
eventTypes.forEach(function (et) {
var h = on[et];
if (typeof h === 'function') {
res.push(h);
} else if (Array.isArray(h)) {
res = res.concat(h);
}
});
return res
}
return res
}
这里有一个预备知识,是 vnode 会解析出我们绑定的事件及事件 id,其存储在 vnode.data 对象下,我们看一下 vnode 的结构:
vnode {
data: {
attrs: {eventid: "0"}
on: {click: ƒ}
}
}
可以看到 vnode 下包含一个 data 属性,data 属性下包含 attrs 和 on 属性,attrs 中又包含了一个 eventid 属性,其值为 0,与 button 的 eventid 一致,on 属性下包含一个 click 方法,该方法即我们自定义的 change 方法。mpvue 通过以下判断找到我们自定义的 change 方法:
if (attrs && on && attrs['eventid'] === eventid) {
eventTypes.forEach(function (et) {
var h = on[et];
if (typeof h === 'function') {
res.push(h);
} else if (Array.isArray(h)) {
res = res.concat(h);
}
});
return res
}
找到后会将该方法 push 到 res 数组中,最终返回 res 数组,此时 res 数组中包含一个 change 方法。最后通过 handleProxyWithVue 中的第三部分源码执行这些方法:
if (handles.length) {
var event = getWebEventByMP(e);
if (handles.length === 1) {
var result = handles[0](event);
return result
}
handles.forEach(function (h) { return h(event); });
}
这里我们的 handles 只有一个方法,所以会执行:
var result = handles[0](event);
在执行事件对应的方法时,会获取 event 会作为参数传入 handles [0] 方法中,小程序原生的 event 会通过 getWebEventByMP 方法进行包装:
var event = getWebEventByMP(e);
getWebEventByMP 方法源码如下:
function getWebEventByMP (e) {
var type = e.type;
var timeStamp = e.timeStamp;
var touches = e.touches;
var detail = e.detail; if ( detail === void 0 ) detail = {};
var target = e.target; if ( target === void 0 ) target = {};
var currentTarget = e.currentTarget; if ( currentTarget === void 0 ) currentTarget = {};
var x = detail.x;
var y = detail.y;
var event = {
mp: e,
type: type,
timeStamp: timeStamp,
x: x,
y: y,
target: Object.assign({}, target, detail),
currentTarget: currentTarget,
stopPropagation: noop,
preventDefault: noop
};
if (touches && touches.length) {
Object.assign(event, touches[0]);
event.touches = touches;
}
return event
}
可以看到 mpvue 将原生小程序的 event 包装到一个 mp 参数下,再加上一些的新的属性,构成一个新的 event 对象,这也是为什么我们在开发 mpvue 过程中,取参数需要通过 event.mp 来获取的原因。