Vue.js 源碼分析(十四) 基礎篇 組件 自定義事件詳解

咱們在開發組件時有時須要和父組件溝通,此時能夠用自定義事件來實現html

組件的事件分爲自定義事件和原生事件,前者用於子組件給父組件發送消息的,後者用於在組件的根元素上直接監聽一個原生事件,區別就是綁定原生事件須要加一個.native修飾符。vue

子組件裏經過過this.$emit()將自定義事件以及須要發出的數據經過如下代碼發送出去,第一個參數是自定義事件的名稱,後面的參數是依次想要發送出去的數據,例如:node

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <title>Document</title>    
</head>
<body>
    <div id="d"><com @myclick="MyClick" @mouseenter.native="Enter"></com></div>
    <script>
Vue.config.productionTip=false; Vue.config.devtools=false; Vue.component('com',{ template:'<button @click="childclick">Click</button>', methods:{ childclick:function(){this.$emit('myclick','gege','123')} //子組件的事件,經過this.$emit觸發父組件的myclick事件 } }) debugger var app = new Vue({ el:'#d', methods:{ MyClick:function(){console.log('parent MyClick method:',arguments)}, //響應子組件的事件函數 Enter:function(){console.log("MouseEnter")} //子組件的原生DOM事件 } }) </script> </body> </html>

子組件就是一個按鈕,渲染以下:git

咱們給整個組件綁定了兩個事件,一個DOM原生的mouseenter事件和自定義的MyClick組件事件,當鼠標移動到按鈕上時,打印出:MouseEnter,以下:github

當點擊按鈕時輸出子組件傳遞過來的信息,以下:npm

自定義事件實際上是存儲在組件實例的_events屬性上的,咱們在控制檯輸入console.log(app.$children[0]["_events"])就能夠打印出來,以下:數組

myclick就是咱們自定義的事件對象weex

 

 源碼分析app


 父組件在解析模板時會執行processAttrs()函數,會在AST對象上增長一個events和nativeevents屬性,以下async

function processAttrs (el) {      //第9526行 對屬性進行解析
  var list = el.attrsList; 
  var i, l, name, rawName, value, modifiers, isProp;
  for (i = 0, l = list.length; i < l; i++) {              //遍歷每一個屬性名
    name = rawName = list[i].name;
    value = list[i].value;
    if (dirRE.test(name)) {
      // mark element as dynamic
      el.hasBindings = true;
      // modifiers
      modifiers = parseModifiers(name);
      if (modifiers) {
        name = name.replace(modifierRE, '');
      }
      if (bindRE.test(name)) { // v-bind
        /**/
      } else if (onRE.test(name)) { // v-on                   //若是name以@或v-on:開頭,表示綁定了事件
        name = name.replace(onRE, '');
        addHandler(el, name, value, modifiers, false, warn$2);    調用addHandler()函數將事件相關信息保存到el.events或nativeEvents裏面
      } else { // normal directives
        /**/
      }
    } else {
      /**/
    }
  }
}

function addHandler (                //第6573行 給el這個AST對象增長event或nativeEvents
  el,
  name,
  value,
  modifiers,
  important,
  warn
) {
  modifiers = modifiers || emptyObject;
  /**/

  var events;
  if (modifiers.native) {                       //若是存在native修飾符,則保存到el.nativeEvents裏面
    delete modifiers.native;
    events = el.nativeEvents || (el.nativeEvents = {});
  } else {                                      //不然保存到el.events裏面
    events = el.events || (el.events = {});
  }

  /**/
  var handlers = events[name];                  //嘗試獲取已經存在的該事件對象
  /* istanbul ignore if */ 
  if (Array.isArray(handlers)) {                //若是是數組,表示已經插入了兩次了,則再把newHandler添加進去
    important ? handlers.unshift(newHandler) : handlers.push(newHandler);
  } else if (handlers) {                        //若是handlers存在且不是數組,則表示只插入過一次,則把events[name]變爲數組
    events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
  } else {                                      //不然表示是第一次新增該事件,則值爲對應的newHandler
    events[name] = newHandler;
  }

  el.plain = false;
}

例子裏執行完後AST對象裏對應的信息以下:(AST能夠這樣認爲:Vue把模板經過正則解析後以對象的形式表現出來)

接下來在generate生成rendre函數的時候會調用genHandlers函數根據不一樣修飾符等生成對應的屬性(做爲_c函數的第二個data參數一部分),以下:

function genData$2 (el, state) {  //第10274行  拼湊data值
  var data = '{'; 

  /**/
  // event handlers
  if (el.events) {               //若是el有綁定事件(沒有native修飾符時)
    data += (genHandlers(el.events, false, state.warn)) + ",";
  }
  if (el.nativeEvents) {        //若是el有綁定事件(native修飾符時)
    data += (genHandlers(el.nativeEvents, true, state.warn)) + ",";
  }
  /**/
  return data
}

genHandlers會根據參數2的值將事件存儲在nativeOn或on屬性裏,以下:

function genHandlers (      //第9992行 拼湊事件的data函數
  events,
  isNative,
  warn
) {
  var res = isNative ? 'nativeOn:{' : 'on:{';       //若是參數isNative爲true則設置res爲:nativeOn:{,不然爲:on:{
  for (var name in events) {
    res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
  }
  return res.slice(0, -1) + '}'
}

例子裏執行到這裏時等於:

 _render將rendre函數轉換爲VNode時候會調用createComponent()函數建立組件佔位符VNode,此時會有

function createComponent (  //第4182行
  Ctor, 
  data,
  context,
  children,
  tag
) {
  /**/
  var listeners = data.on;          //對自定義事件(沒有native修飾符)的處理,則保存到listeners裏面,一下子存到佔位符VNode的配置信息裏
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn;          //對原生DOM事件,則保存到data.on裏面,這樣等該DOM渲染成功後會執行event模塊的初始化,就會綁定對應的函數了

  /**/
  var name = Ctor.options.name || tag;
  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 },        //自定義事件做爲listeners屬性存儲在組件Vnode的配置參數裏了
    asyncFactory
  );

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  return vnode
}

原生事件存儲在on屬性上,後面介紹v-on指令時再詳細介紹,對於自定義事件存儲在組件Vnode配置參數的listeners屬性裏了。

當組件實例化的時候執行_init()時首先執行initInternalComponent()函數,該函數會獲取listeners屬性,以下:

function initInternalComponent (vm, options) {        //第4632行  初始化子組件
  var opts = vm.$options = Object.create(vm.constructor.options);
  // doing this because it's faster than dynamic enumeration.
  var parentVnode = options._parentVnode;                          //該組件的佔位符VNode
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;
  opts._parentElm = options._parentElm;
  opts._refElm = options._refElm;

  var vnodeComponentOptions = parentVnode.componentOptions;         //佔位符VNode初始化傳入的配置信息
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;          //將組件的自定義事件保存到_parentListeners屬性裏面
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}

 回到_init函數,接着執行initEvents()函數,該函數會初始化組件的自定義事件,以下:

function initEvents (vm) {      //第2412行 初始化自定義事件
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;       //獲取佔位符VNode上的自定義事件
  if (listeners) {                                    
    updateComponentListeners(vm, listeners);          //執行updateComponentListeners()新增事件
  }
}

  updateComponentListeners函數用於新增/更新組件的事件,以下:

function add (event, fn, once) {      //第2424行
  if (once) {
    target.$once(event, fn);            //自定義事件最終調用$once綁定事件的
  } else {
    target.$on(event, fn);
  }
}

function remove$1 (event, fn) {
  target.$off(event, fn);
}

function updateComponentListeners (       //第2436行
  vm,
  listeners,
  oldListeners
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove$1, vm);    //調用updateListeners()更新DOM事件,傳入add函數
  target = undefined;
}

updateListeners內部會調用add()函數,這裏用了一個優化措施,實際上咱們綁定的是Vue內部的createFnInvoker函數,該函數會遍歷傳給updateListeners的函數,依次執行。

add()最終執行的是$on()函數,該函數定義以下:

  Vue.prototype.$on = function (event, fn) {  //第2448行 自定義事件的新增  event:函數名 fn:對應的函數
    var this$1 = this; 

    var vm = this;
    if (Array.isArray(event)) {                         //若是event是一個數組
      for (var i = 0, l = event.length; i < l; i++) {       //則遍歷該數組
        this$1.$on(event[i], fn);                               //依次調用this$1.$on
      }
    } else {                                             //若是不是數組
      (vm._events[event] || (vm._events[event] = [])).push(fn);     //則將事件保存到ev._event上
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {                                  //若是事件名以hook:開頭                    
        vm._hasHookEvent = true;                                    //則設置vm._hasHookEvent爲true,這樣生命週期函數執行時也會執行這些函數
      }
    }
    return vm
  };

從這裏能夠看到自定義事件實際上是保存到組件實例的_events屬性上的

當子組件經過$emit觸發當前實例上的事件時,會從_events上拿到對應的自定義事件並執行,以下:

  Vue.prototype.$emit = function (event) {  //第2518行  子組件內部經過$emit()函數執行到這裏
    var vm = this;
    {
      var lowerCaseEvent = event.toLowerCase();                       //先將事件名轉換爲小寫    
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {   //若是lowerCaseEvent不等於event則報錯(即事件名只能是小寫)
        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];                                      //從_events屬性裏獲取對應的函數數組
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;                        //獲取全部函數
      var args = toArray(arguments, 1);                                 //去掉第一個參數,後面的都做爲事件的參數
      for (var i = 0, l = cbs.length; i < l; i++) {                     //遍歷cbs
        try {
          cbs[i].apply(vm, args);                                           //依次執行每一個函數,值爲子組件的vm實例
        } catch (e) {
          handleError(e, vm, ("event handler for \"" + event + "\""));
        }
      }
    }
    return vm
  };

大體流程跑完了,有點繁瑣,多調試一下就行了。

相關文章
相關標籤/搜索