Skip to content
微信公众号

mpvue 事件代理源码分析

mpvue data 注入

之前我们分析过,mpvue 通过全局事件代理的方式,实现事件的响应,事件响应过程中,通过 data 中的 eventid 和 comkey 区分事件和组件,那么 eventid 和 comkey 是如何注入 data 中的呢?要搞清楚这个问题,需要了解 mpvue 如何向 page 的 data 注入这些属性。我们仍然沿用之前的案例:

js
<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 方法中,这个方法非常重要,我们之前反复分析过:

js
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 的注入,该方法源码如下:

js
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 生命周期函数中:

js
onLoad: function onLoad (query) {
  mp.page = this;
  // ...
},

之后通过 collectVmData 生成 data 数据:

js
var data = collectVmData(this.$root);

collectVmData 的源码如下:

js
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 源码如下:

js
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 数据结果如下:

js
$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 结构:

js
<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 的源码如下:

js
handleProxy: function handleProxy (e) {
  return rootVueVM.$handleProxyWithVue(e)
}

我们点击事件实际调用 $handleProxyWithVue,其源码如下:

js
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); });
  }
}

第一部分源码如下:

js
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。

第二部分源码如下:

js
var webEventTypes = eventTypeMap[type] || [type];
var handles = getHandle(vm._vnode, eventid, webEventTypes);

这段代码的主要目的是通过 eventid 找到点击事件对应的处理方法,即想办法找到我们在 mpvue 中定义在 methods 下的 change 方法。第一行代码中的 eventTypeMap 主要用来映射事件类型,它是一个枚举型对象:

js
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 是获得事件的核心方法,其核心源码如下:

js
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 的结构:

js
vnode {
	data: {
		attrs: {eventid: "0"}
		on: {click: ƒ}
	}
}

可以看到 vnode 下包含一个 data 属性,data 属性下包含 attrs 和 on 属性,attrs 中又包含了一个 eventid 属性,其值为 0,与 button 的 eventid 一致,on 属性下包含一个 click 方法,该方法即我们自定义的 change 方法。mpvue 通过以下判断找到我们自定义的 change 方法:

js
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 中的第三部分源码执行这些方法:

js
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 只有一个方法,所以会执行:

js
var result = handles[0](event);

在执行事件对应的方法时,会获取 event 会作为参数传入 handles [0] 方法中,小程序原生的 event 会通过 getWebEventByMP 方法进行包装:

js
var event = getWebEventByMP(e);

getWebEventByMP 方法源码如下:

js
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 来获取的原因。

本站总访问量次,本站总访客数人次
Released under the MIT License.