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

来源:https://www.cnblogs.com/greatdesert/archive/2019/07/02/11121952.html
-Advertisement-
Play Games

可以用 v-on 指令監聽 DOM 事件,併在觸發時運行一些 JavaScript 代碼,例如: 渲染結果為: 我們給測試按鈕添加了一個mouseenter和click事件,滑鼠移上去式控制台輸出: 當點擊時,輸出為: Vue的事件綁定有很多種寫法,例如: 可以看到v-on對應事件可以很多種格式的, ...


可以用 v-on 指令監聽 DOM 事件,併在觸發時運行一些 JavaScript 代碼,例如:

<!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>

渲染結果為:

我們給測試按鈕添加了一個mouseenter和click事件,滑鼠移上去式控制台輸出:

當點擊時,輸出為:

 Vue的事件綁定有很多種寫法,例如:

<!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實例的一個方法、一個表達式、一個函數,或者一個箭頭函數

 

 源碼分析


 以上面的第一個例子為例,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進行了封裝,使我們用起來更方便而已。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • DOM(屬性節點) 屬性節點沒有過參加家族關係中,其專用選擇器:attributes,返回值為對象的形式,它的鍵是索引值,也就是用對象模擬了一個偽數組,DOM中選擇器返回的都是偽數組(可以使用數組的形式遍歷,操作。但是不能使用數組的方法),下麵是屬性節點的操作 <div class="box" ti ...
  • 301:永久重定向 302:臨時重定向 相同點:輸入網址A,都會重定向到網址B 不同點: ① 301:舊地址A的資源不可訪問了(永久移除),重定向到網址B,搜索引擎會抓取網址B的內容,同時將網址保存為B網址。 ② 302:舊地址A的資源仍可訪問,這個重定向只是臨時從舊地址A跳轉到B地址,這時搜索引擎 ...
  • js api 之 fetch、querySelector、form、atob及btoa 轉載請註明出處: "https://www.cnblogs.com/funnyzpc/p/11095862.html" js api即為JavaScript內置函數,本章就說說幾個比較實用的內置函數,內容大致如下 ...
  • 本例參考並改進自:https://www.jianshu.com/p/2961d9c317a3 大家可以一起學習!! ...
  • 基本調用: 自動關閉: 銷毀Loading Dom節點: ...
  • 如果只需要知道一種編程語言就可以構建一個全棧的應用程式,是不是特別了不起?Ryan Dahl為了把這個想法成為現實,創造了node.js。Node.js是建立在Chrome強勁的V8 JavaScript引擎上的伺服器端框架。雖然最初是用C++編寫的,但是應用程式通過JavaScript運行。 這樣 ...
  • 1.XHTML和HTML有什麼區別 HTML是一種基本的WEB網頁設計語言,XHTML是一個基於XML的置標語言最主要的不同:XHTML 元素必須被正確地嵌套。XHTML 元素必須被關閉。標簽名必須用小寫字母。XHTML 文檔必須擁有根元素。2.什麼是語義化的HTML? 直觀的認識標簽 對於搜索引擎 ...
  • 博客園添加背景音樂,背景效果 [TOC] 申請博客園JS許可權 申請話術 實例 開通以後就可以使用js代碼進行裝飾了 流程圖 添加網易雲背景音樂 單曲添加 打開網易雲登錄賬戶,搜索自己喜歡的歌曲 複製代碼到博客園 看看效果 添加歌單為背景音樂 創建歌單 把喜歡的音樂添加到歌單 拷貝代碼到博客園 點擊鼠 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...