Vue.js 源碼分析(十六) 指令篇 v-on指令詳解

能夠用 v-on 指令監聽 DOM 事件,並在觸發時運行一些 JavaScript 代碼,例如:html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <button @click="show('click',$event)" @mouseenter="show('mouseenter',$event)">測試</button>
    </div>
    <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        var app = new Vue({
            el:'#app',
            methods:{ show(type,ev){console.log(type)} }
        })
    </script>
</body>
</html>

渲染結果爲:vue

咱們給測試按鈕添加了一個mouseenter和click事件,鼠標移上去式控制檯輸出:node

當點擊時,輸出爲:數組

 Vue的事件綁定有不少種寫法,例如:app

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <p>{{message}}</p> 
        <button @click="test1">Test1</button>                       <!--事件能夠對應一個方法-->
        <button @click="test2('test2',$event)">Test2</button>          <!--方法還能夠傳遞參數,$event表示原始的DOM事件-->
        <button @click="message='test3'">Test3</button>               <!--也能夠是一個表達式-->
        <button @click="function(){message='test4'}">Test4</button>    <!--也能夠是一個函數-->
        <button @click="()=>{message='test5'}">Test5</button>          <!--也能夠是一個箭頭函數-->
    </div>  
    <script>
        var App = new Vue({
            el:'#app',
            data(){
                return {message:"Hello Vue"}
            },
            methods:{
                test1(){console.log('test1');},
                test2(text,ev){console.log(text);console.log(ev.type)}
            }
        })
    </script>
</body>
</html>

能夠看到v-on對應事件能夠不少種格式的,能夠是當前Vue實例的一個方法、一個表達式、一個函數,或者一個箭頭函數ide

 

 源碼分析函數


 以上面的第一個例子爲例,Vue將DOM解析成AST對象時的時候執行到a節點時會執行processElement()函數,而後會執行processAttrs()函數,該函數會遍歷每一個屬性,而後用判斷是否以:或v-bind:開頭,以下:源碼分析

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)) {                       //若是該屬性以v-、@或:開頭,表示這是Vue內部指令
      // mark element as dynamic
      el.hasBindings = true;
      // modifiers
      modifiers = parseModifiers(name);
      if (modifiers) {
        name = name.replace(modifierRE, '');
      }
      if (bindRE.test(name)) {                    //bindRD等於/^:|^v-bind:/ ,即該屬性是v-bind指令時 例如:<a :href="url">你好</a>
        /*這裏時v-bind指令對應的分支*/
      } else if (onRE.test(name)) {               //onRE等於/^@|^v-on:/,即該屬性是v-on指令時
        name = name.replace(onRE, '');                              //獲取綁定的事件類型  好比@click,此時name等於click   v-on:click此時name也等於click
        addHandler(el, name, value, modifiers, false, warn$2);      //調用addHandler()函數將事件相關信息保存到el.events或nativeEvents裏面
      } else {                                  // normal directives
       /*自定義指令的分支*/
      }
    } else {                                      //存儲普通屬性的分支
      // literal attribute
      {
        var res = parseText(value, delimiters);
        if (res) {
          warn$2(
            name + "=\"" + value + "\": " +
            'Interpolation inside attributes has been removed. ' +
            'Use v-bind or the colon shorthand instead. For example, ' +
            'instead of <div id="{{ val }}">, use <div :id="val">.'
          );
        }
      }
      addAttr(el, name, JSON.stringify(value));
      // #6887 firefox doesn't update muted state if set via attribute
      // even immediately after element creation
      if (!el.component &&
          name === 'muted' &&
          platformMustUseProp(el.tag, el.attrsMap.type, name)) {
        addProp(el, name, 'true');
      }
    }
  }
}

addHandler()函數用於給對應的AST對象增長一個events屬性,保存事件對應的信息,以下:性能

function addHandler (     //第6573行  給el這個AST對象增長event或nativeEvents,用於記錄事件的信息 
  el,
  name,
  value,
  modifiers,
  important,
  warn
) {
  modifiers = modifiers || emptyObject;
  // warn prevent and passive modifier
  /* istanbul ignore if */
  if (
    "development" !== 'production' && warn &&
    modifiers.prevent && modifiers.passive
  ) {
    warn(
      'passive and prevent can\'t be used together. ' +
      'Passive handler can\'t prevent default event.'
    );
  }

  // check capture modifier
  if (modifiers.capture) {
    delete modifiers.capture;
    name = '!' + name; // mark the event as captured
  }
  if (modifiers.once) {             //若是有once修飾符
    delete modifiers.once;
    name = '~' + name; // mark the event as once
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive;
    name = '&' + name; // mark the event as passive
  }

  // normalize click.right and click.middle since they don't actually fire
  // this is technically browser-specific, but at least for now browsers are
  // the only target envs that have right/middle clicks.
  if (name === 'click') {         //鼠標按鍵修飾符:若是是click事件,則根據modiflers進行修正
    if (modifiers.right) {
      name = 'contextmenu';
      delete modifiers.right;
    } else if (modifiers.middle) {
      name = 'mouseup';
    }
  }

  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 newHandler = {
    value: value.trim()
  };
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers;
  }

  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 {
    events[name] = newHandler;           //不然表示是第一次新增該事件,則值爲對應的newHandler
  }

  el.plain = false;
}

例子裏執行到這裏這裏後對應的AST等於:測試

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

 

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

genHandler會獲取每一個事件對應的代碼,以下:

function genHandler (   //第10004行  name:事件名,好比:name handler:事件綁定的對象信息,好比:{value: "show", modifiers: {…}}
  name,
  handler
) {
  if (!handler) {
    return 'function(){}'
  }

  if (Array.isArray(handler)) {   
    return ("[" + (handler.map(function (handler) { return genHandler(name, handler); }).join(',')) + "]")
  }

  var isMethodPath = simplePathRE.test(handler.value);          //是否爲簡單的表達式,好比show、show_d、show1等
  var isFunctionExpression = fnExpRE.test(handler.value);       //是否爲函數表達式(箭頭函數或function(){}格式的匿名函數)

  if (!handler.modifiers) {                                     //若是該事件的修飾符爲空
    if (isMethodPath || isFunctionExpression) {                     //若是是簡單表達式或者是函數表達式
      return handler.value                                              //則直接返回handler.value,好比:show
    }
    /* istanbul ignore if */
    return ("function($event){" + (handler.value) + "}") // inline statement  //不然返回帶有一個$event變量的函數形式,好比:當value是個表達式時,例如:value=a+123,返回格式:function($event){a+123;}
  } else {                                                      //若是還存在修飾符(解析模板時有些修飾符被過濾掉了)
    var code = '';
    var genModifierCode = '';
    var keys = [];
    for (var key in handler.modifiers) {                          //遍歷每一個修飾符,好比:prevent
      if (modifierCode[key]) {                                        //若是有在modifierCode裏面定義   modifierCode是個數組,保存了一些內置修飾符對應的代碼
        genModifierCode += modifierCode[key];                            //則拼湊到genModifierCode裏面
        // left/right
        if (keyCodes[key]) {
          keys.push(key);
        }
      } else if (key === 'exact') {
        var modifiers = (handler.modifiers);
        genModifierCode += genGuard(
          ['ctrl', 'shift', 'alt', 'meta']
            .filter(function (keyModifier) { return !modifiers[keyModifier]; })
            .map(function (keyModifier) { return ("$event." + keyModifier + "Key"); })
            .join('||')
        );
      } else {
        keys.push(key);
      }
    }
    if (keys.length) {                                          //若是有按鍵
      code += genKeyFilter(keys);                                   //則拼湊按鍵
    }   
    // Make sure modifiers like prevent and stop get executed after key filtering
    if (genModifierCode) {
      code += genModifierCode;
    }
    var handlerCode = isMethodPath
      ? ("return " + (handler.value) + "($event)")
      : isFunctionExpression
        ? ("return (" + (handler.value) + ")($event)")
        : handler.value;
    /* istanbul ignore if */
    return ("function($event){" + code + handlerCode + "}")
  }
}

例子裏執行到這裏後生成的render函數等於:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:{"click":function($event){show('click',$event)},"mouseenter":function($event){show('mouseenter',$event)}}},[_v("測試")])])}

其中和事件有關的以下:

on: {
    "click": function($event) {
        show('click', $event)
    },
    "mouseenter": function($event) {
        show('mouseenter', $event)
    }
}

最後在_watch渲染成真實的DOM節點後,就會調用events模塊的updateDOMListeners鉤子函數,該函數會獲取該Vnode的on屬性,依次遍歷on對象裏的每一個元素,最後調用addEventListener去綁定對應的事件

 

function updateDOMListeners (oldVnode, vnode) {     //第7083行 DOMN事件相關
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  var on = vnode.data.on || {};                                   //新Node上的事件  例如:{click: ƒ ($event){}}
  var oldOn = oldVnode.data.on || {};
  target$1 = vnode.elm;                                           //DOM引用
  normalizeEvents(on);                                            //處理V-model的
  updateListeners(on, oldOn, add$1, remove$2, vnode.context);     //調用updateListeners作進一步處理
  target$1 = undefined;
}

updateListeners()函數又會調用add$1函數去添加DOM事件,以下:

function updateListeners (      //第2036行 更新DOM事件
  on,
  oldOn,
  add,
  remove$$1,
  vm
) {
  var name, def, cur, old, event;
  for (name in on) {                  //遍歷on,此時name就是對應的事件類型,好比:click
    def = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    /* istanbul ignore if */
    if (isUndef(cur)) {
      "development" !== 'production' && warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    } else if (isUndef(old)) {                //若是old沒有定義,則表示這是一個建立事件
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur);
      }
      add(event.name, cur, event.once, event.capture, event.passive, event.params);   //調用add()綁定事件
    } else if (cur !== old) {
      old.fns = cur;
      on[name] = old;
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name);
      remove$$1(event.name, oldOn[name], event.capture);
    }
  }
}

updateListeners裏的add函數,也就是全局的add$1函數纔是最終的添加事件函數,以下:

function add$1 (    //第7052行  綁定事件  event:事件名 handler:事件的函數 once$$1:是否只執行一次 capture:是否採用捕獲狀態 passive:可用於移動端性能提高
  event,
  handler,
  once$$1,
  capture,
  passive
) {
  handler = withMacroTask(handler);
  if (once$$1) { handler = createOnceHandler(handler, event, capture); }  //若是有設置了once$$1,則繼續使用createOnceHandler封裝
  target$1.addEventListener(                                              //調用原生的DOM APIaddEventListener添加對應的事件,2017年DOM規範對addEventListener()的第三個參數作了修訂,能夠是一個對象
    event,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

咱們看到Vue內部添加DOM事件最終也是經過addEventListener()來添加的,說到底,Vue只是把這些API進行了封裝,使咱們用起來更方便而已。

相關文章
相關標籤/搜索