Vue源碼解讀之事件機制

Vue定義了四種添加事件監聽的方法:
clipboard.png
例如$on,可經過vm.$on(event,callback)添加監聽。javascript

事件監聽

$on

Vue.prototype.$on = function (event, fn) {
    var this$1 = this;
    var vm = this;
    //若是傳參event是數組,遞歸調用$on
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this$1.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      // 這裏在註冊事件的時候標記bool值也就是個標誌位來代表存在鉤子,而不須要經過哈希表的方法來查找是否有鉤子,這樣作能夠減小沒必要要的開銷,優化性能。
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

$once

$once監聽只能觸發一次的事件,觸發之後會自動移除該事件。html

Vue.prototype.$once = function (event, fn) {
    var vm = this;
    function on () {
      //在第一次執行的時候將該事件銷燬
      vm.$off(event, on);
      //執行註冊的方法
      fn.apply(vm, arguments);
    }
    on.fn = fn;
    vm.$on(event, on);
    return vm
  };

$off

$off用來移除自定義事件。vue

Vue.prototype.$off = function (event, fn) {
    var this$1 = this;
    var vm = this;
    // 若是沒有參數,關閉所有事件監聽器
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm
    }
    // 關閉數組中的事件監聽器
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this$1.$off(event[i], fn);
      }
      return vm
    }
    // 具體的某個事件
    var cbs = vm._events[event];
    if (!cbs) {
      return vm
    }
    //  fn回調函數不存在,將事件監聽器變爲null,返回vm
    if (!fn) {
      vm._events[event] = null;
      return vm
    }
    // 回調函數存在
    if (fn) {
      // specific handler
      var cb;
      var i$1 = cbs.length;
      while (i$1--) {
        cb = cbs[i$1];
        if (cb === fn || cb.fn === fn) {
          // 移除 fn 這個事件監聽器
          cbs.splice(i$1, 1);
          break
        }
      }
    }
    return vm
  };

$emit

$emit用來觸發指定的自定義事件。java

Vue.prototype.$emit = function (event) {
    var vm = this;
    {
      var lowerCaseEvent = event.toLowerCase();
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          "Event \"" + lowerCaseEvent + "\" is emitted in component " +
          (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
          "Note that HTML attributes are case-insensitive and you cannot use " +
          "v-on to listen to camelCase events when using in-DOM templates. " +
          "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
        );
      }
    }
    var cbs = vm._events[event];
    if (cbs) {
      //將類數組的對象轉換成數組
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      var args = toArray(arguments, 1);
      for (var i = 0, l = cbs.length; i < l; i++) {
        try {
          //觸發當前實例上的事件,附加參數都會傳給監聽器回調。
          cbs[i].apply(vm, args);
        } catch (e) {
          handleError(e, vm, ("event handler for \"" + event + "\""));
        }
      }
    }
    return vm
  };

HTML元素上點擊事件

先來研究發生在原生dom元素上的事件。
用v-on指令監聽DOM事件,並在觸發時運行一些JS代碼,也能夠經過@簡寫的方式,緣由:
Vue定義了這樣的正則表達式var onRE = /^@|^v-on:/;,在processAttrs()解析屬性時經過onRE.test(name)判斷來添加屬性。
html的編寫以下:node

<body>
<div id="app">
    <button @click="test01">點我01號</button>
</div>
<script>
    new Vue({
        'el':'#app',
        methods:{
            test01:function(){
                alert('01號被點了!');
            }
        }
    });
</script>
</body>

Vue初始化時,調用initEvents(vm)對事件進行初始化:正則表達式

function initEvents (vm) {
  vm._events = Object.create(null);
  //_hasHookEvent標誌位代表是否存在鉤子,而不須要經過哈希表來查找是否有鉤子,這樣作能夠減小沒必要要的開銷,優化性能。
  vm._hasHookEvent = false;
  //初始化父組件attach的事件
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}

initEvents方法在vm上建立一個_events對象,用來存放事件。
接下來就調用initState()方法初始化事件,相關代碼段:
if (opts.methods) { initMethods(vm, opts.methods); }
這個例子methods內有方法test01,所以會執行initMethods。segmentfault

vm[key] = methods[key] == null ? noop : bind(methods[key], vm);

initMethods遍歷定義的methods,經過調用bind改變函數的this指向,修飾了事件的回調函數,組件上掛載的事件都是在父做用域中的。數組

function nativeBind (fn, ctx) {
  //fn的this指向ctx
  return fn.bind(ctx)
}
var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind;

這裏bind用來改變函數中this的指向。(關於call,apply,bind的學習來自於https://www.cnblogs.com/xljzl...
接下來Vue將html解析成ast,解析後的render如圖所示緩存

clipboard.png
解析過程參考https://segmentfault.com/a/11...
接下來調用add$1經過addEventListener將事件綁定到target上。app

function add$1 (event,handler,once$$1,apture,passive) {
  handler = withMacroTask(handler);
  if (once$$1) { handler = createOnceHandler(handler, event, capture); }
  //綁定點擊事件
  target$1.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

component上點擊事件

<body>
<div id="app">
    <child-test :test-message="mess" v-on:click.native="test01" v-on:componenton="test02"></child-test>
</div>
<template id="tplt01">
    <button>{{testMessage}}</button>
</template>
<script>
    new Vue({
        'el':'#app',
        data:{mess:'組件點擊001'},
        methods:{
            test01:function(){
                alert('01號被點了!');
            },
            test02:function(){
                alert('01號被點了!');
            }
        },
        components:{
            'childTest':{
                template:"#tplt01",
                props:['testMessage'],
                methods:{
                    test02:function(){
                        alert('001號被點了!');
                    }
                },
            }
        }
    });
</script>
</body>

<child-test>標籤訂義了click事件,若click事件沒加修飾符.native,點擊按鈕不出發任何事件,
若添加了.native修飾符,點擊按鈕執行的就是test01,alert('01號被點了!');。.native的做用就是在原生dom上綁定事件。
不添加.native的的事件解析過程與上文相同,而添加了.native以後,v-on的事件會被放到nativeOn數組中,解析後的render如圖所示:

clipboard.png

在事件初始化,調用genHandlers的時候,會先判斷該事件是否爲native,若是是,解析的事件字符串就會用'nativeOn{}'包裹。

function genHandlers (
  events,
  isNative,
  warn
) {
  var res = isNative ? 'nativeOn:{' : 'on:{';
  for (var name in events) {
    res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
  }
  return res.slice(0, -1) + '}'
}

html解析成vnode以後會調用createComponent進行處理。

function createComponent (
  Ctor,
  data,
  context,
  children,
  tag
) {
  if (isUndef(Ctor)) {
    return
  }
  var baseCtor = context.$options._base;
  /*其餘代碼省略*/
  //緩存data.on的函數,這些須要做爲子組件監聽器而不是DOM監聽器來處理。就是componenton事件
  var listeners = data.on;
  //data.on被native修飾符的事件所替換
  data.on = data.nativeOn;
  /*其餘代碼省略*/
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );
  return vnode
}

createComponent將.native修飾符的事件放在data.on上面。接下來data.on上的事件(本文中的alert('001號被點了!');)會按普通的html事件往下走。

function updateDOMListeners (oldVnode, vnode) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  var on = vnode.data.on || {};
  var oldOn = oldVnode.data.on || {};
  target$1 = vnode.elm;
  normalizeEvents(on);
  updateListeners(on, oldOn, add$1, remove$2, vnode.context);
  target$1 = undefined;
}

最終經過target$1.addEventListener添加事件監聽。
而標籤內沒有.native的修飾符調用的是$on方法。

function updateComponentListeners (
  vm,
  listeners,
  oldListeners
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
  target = undefined;
}

updateListeners又調用了add方法

function add (event, fn, once) {
  if (once) {
    target.$once(event, fn);
  } else {
    target.$on(event, fn);
  }
}

也就是說對於普通html元素和在組件標籤內添加了.native修飾符的事件,都經過target$1.addEventListener()來掛載事件。而定義在組件上的事件會調用原型上的$on等方法。

事件修飾符及其餘

關於事件的使用官網已經說得很清楚啦,這裏就不贅述啦。
https://cn.vuejs.org/v2/guide...

<!-- 阻止單擊事件繼續傳播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件再也不重載頁面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修飾符能夠串聯 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修飾符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件監聽器時使用事件捕獲模式 -->
<!-- 即元素自身觸發的事件先在此到處理,而後才交由內部元素進行處理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只當在 event.target 是當前元素自身時觸發處理函數 -->
<!-- 即事件不是從內部元素觸發的 -->
<div v-on:click.self="doThat">...</div>
相關文章
相關標籤/搜索