Vue.js 源碼分析(二十四) 高級應用 自定義指令詳解

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

除了核心功能預設內置的指令 (v-model 和 v-show),Vue 也允許註冊自定義指令。 官網介紹的比較抽象,顯得很高大上,我個人對自定義指令的理解是:當自定義指令作用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節點、更新、解綁時可以執行一些特定的操作(鉤子函數() 自定義指令有兩 ...


除了核心功能預設內置的指令 (v-modelv-show),Vue 也允許註冊自定義指令。

官網介紹的比較抽象,顯得很高大上,我個人對自定義指令的理解是:當自定義指令作用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節點、更新、解綁時可以執行一些特定的操作(鉤子函數()

自定義指令有兩種註冊方式,一種是全局註冊,使用Vue.directive(指令名,配置參數)註冊,註冊之後所有的Vue實例都可以使用,另一種是局部註冊,在創建Vue實例時通過directives屬性創建局部指令,局部自定義指令只能在當前Vue實例內使用

自定義指令可以綁定如下鉤子函數:

    ·bind                      ;只調用一次,元素渲染成DOM節點後,執行directives模塊的初始化工作時調用,在這裡可以進行一次性的初始化設置。
    ·inserted               ;被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中)。
    ·update              ;所在組件的 VNode 更新時調用,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。
    ·componentUpdated       ;指令所在組件的 VNode 及其子 VNode 全部更新後調用。
    ·unbind              ;只調用一次,指令與元素解綁時調用。

每個鉤子函數可以有四個參數,分別是el(對應的DOM節點引用)、binding(一些關於指令的擴展信息,是個對象)、vnode(該節點對應的虛擬VN哦的)和oldVnode(之前的VNode,僅在update和componentUpdated鉤子中可用)

bind鉤子函數執行的時候該DOM元素被渲染出來了,但是並沒有插入到父元素中,例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="d"><input type="" name="" v-focus></div>
    <script>
        Vue.directive('focus', {       
            bind:function(el){console.log(el.parentElement);},                      //列印父節點
            inserted: function (el) {console.log(el.parentElement);el.focus()}      //列印父節點,並將當前元素處於聚焦狀態
        })
        var app = new Vue({el:"#d"})
    </script>
</body>
</html>

輸出如下:

可以看到input元素自動獲得焦點了,控制台輸出如下:

可以看到對於bind()鉤子來說,它的父節點是獲取不到的,因為Vue內部會在執行bind()鉤子後才會將當前元素插入到父元素的子節點里

 

源碼分析


 

在解析模板將DOM轉換成AST對象的時候會執行processAttrs()函數,如下:

function processAttrs (el) {                     //解析Vue的屬性
  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)) { // v-bind                          //bindRD等於/^:|^v-bind:/ ,即該屬性是v-bind指令時
        /*v-bind的分支*/
      } else if (onRE.test(name)) { // v-on
        /*v-on的分支*/
      } else { // normal directives
        name = name.replace(dirRE, '');                         //去掉指令首碼,比如v-show執行後等於show
        // parse arg
        var argMatch = name.match(argRE);
        var arg = argMatch && argMatch[1];
        if (arg) {
          name = name.slice(0, -(arg.length + 1));
        }
        addDirective(el, name, rawName, value, arg, modifiers); //執行addDirective給el增加一個directives屬性
        if ("development" !== 'production' && name === 'model') {
          checkForAliasModel(el, value);
        }
      }
    } else {
      /*非Vue指令的分支*/
    }
  }
}

addDirective會給AST對象上增加一個directives屬性保存指令信息,如下:

function addDirective (                         //第6561行 指令相關,給el這個AST對象增加一個directives屬性,值為該指令的信息
  el,
  name,
  rawName,
  value,
  arg,
  modifiers
) {
  (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
  el.plain = false;
}

例子里的p元素執行到這裡時對應的AST對象如下:

接下來在generate生成rendre函數的時候,會執行genDirectives()函數,將AST轉換成一個render函數,如下:

with(this){return _c('div',{attrs:{"id":"d"}},[_c('input',{directives:[{name:"focus",rawName:"v-focus"}],attrs:{"type":"","name":""}})])}

最後等渲染完成後會執行directives模塊的create鉤子函數,如下:

var directives = {                 //第6173行 directives模塊 
  create: updateDirectives,             //創建DOM後的鉤子
  update: updateDirectives,
  destroy: function unbindDirectives (vnode) {
    updateDirectives(vnode, emptyNode);
  }
}

function updateDirectives (oldVnode, vnode) {         //第6181行   oldVnode:舊的Vnode,更新時才有 vnode:新的VNode
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode);
  }
}

_updat 就是處理指令初始化和更新的,如下:

 

function _update (oldVnode, vnode) {                 //第6187行 初始化/更新指令
  var isCreate = oldVnode === emptyNode;                                                     //是否為初始化
  var isDestroy = vnode === emptyNode;
  var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);          
  var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);                 //調用normalizeDirectives$1()函數規範化參數
     
  var dirsWithInsert = [];
  var dirsWithPostpatch = [];

  var key, oldDir, dir;
  for (key in newDirs) {                                     //遍歷newDirs
    oldDir = oldDirs[key];                                         //oldVnode上的key指令信息
    dir = newDirs[key];                                            //vnode上的key指令信息
    if (!oldDir) {                                                 //如果oldDir不存在,即是新增指令
      // new directive, bind
      callHook$1(dir, 'bind', vnode, oldVnode);                     //調用callHook$1()函數,參數2為bind,即執行v-focus指令的bind函數
      if (dir.def && dir.def.inserted) {                            //如果有定義了inserted鉤子函數
        dirsWithInsert.push(dir);                                     //則保存到dirsWithInsert數組裡
      }
    } else {
      // existing directive, update
      dir.oldValue = oldDir.value;
      callHook$1(dir, 'update', vnode, oldVnode);
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir);
      }
    }
  }
  if (dirsWithInsert.length) {                                    //如果dirsWithInsert存在(即有綁定了inserted鉤子函數)
    var callInsert = function () {                                  //定義一個callInsert函數,該函數會執行dirsWithInsert里的每個函數
      for (var i = 0; i < dirsWithInsert.length; i++) {
        callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);   
      }
    };
    if (isCreate) {                                                 //如果是初始化  
      mergeVNodeHook(vnode, 'insert', callInsert);                    //則調用mergeVNodeHook()函數
    } else {
      callInsert();
    }
  }

  if (dirsWithPostpatch.length) {        
    mergeVNodeHook(vnode, 'postpatch', function () {
      for (var i = 0; i < dirsWithPostpatch.length; i++) {
        callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
      }
    });
  }

  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        // no longer present, unbind
        callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
      }
    }
  }
}

 

對於bind鉤子函數來說是直接執行了,而對於inserted鉤子函數則是把函數保存到dirsWithInsert數組裡,再定義了一個callInsert函數,該函數內部通過作用域訪問dirsWithInsert變數,並遍歷該變數依次執行每個inserted鉤子函數

 

mergeVNodeHook()鉤子函數的作用是把insert作為一個hooks屬性保存到對應的Vnode的data上面,當該Vnode插入到父節點後會調用該hooks,如下:

 

function mergeVNodeHook (def, hookKey, hook) {      //第2074行  合併VNode的鉤子函數 def:一個VNode hookKey:(事件名,比如:insert) hook:回調函數
  if (def instanceof VNode) {                           //如果def是一個VNode
    def = def.data.hook || (def.data.hook = {});          //則將它重置為VNode.data.hook,如果VNode.data.hook不存在則初始化為一個空對象 註:普通節點VNode.data.hook是不存在的。
  }
  var invoker;
  var oldHook = def[hookKey];
 
  function wrappedHook () {     
    hook.apply(this, arguments);                            //先執行hook函數        
    // important: remove merged hook to ensure it's called only once
    // and prevent memory leak
    remove(invoker.fns, wrappedHook);                       //然後把wrappedHook從invoker.fns里remove掉,以且包只執行一次
  }

  if (isUndef(oldHook)) {                               //如果oldHook不存在,即之前沒有定義hookKey這個鉤子函數
    // no existing hook
    invoker = createFnInvoker([wrappedHook]);               //直接調用createFnInvoker()返回一個閉包函數,參數為執行的回調函數
  } else {
    /* istanbul ignore if */
    if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
      // already a merged invoker
      invoker = oldHook;
      invoker.fns.push(wrappedHook);
    } else {
      // existing plain hook
      invoker = createFnInvoker([oldHook, wrappedHook]);
    }
  }

  invoker.merged = true;
  def[hookKey] = invoker;                               //設置def的hookKey屬性指向新的invoker
}

createFnInvoker就是v-on指令對應的那個函數,用到了同一個API,執行完後,我們就把invoker插入到input對應的VNode.data.hook里了,如下:

最後等到該VNode插入到父節點後就會執行invokeCreateHooks()函數,該函數會遍歷VNode.hook.insert,依次執行每個函數,也就執行到我們自定義定義的inserted鉤子函數了。


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

-Advertisement-
Play Games
更多相關文章
  • 創建word文件內容如下 將word導出為xml格式 將文件尾碼名改為 .ftl 打開文件 修改圖片的數據內容使用表達式代替 修改後 後查看${username}是否分家了,如果分家了將其多餘部分刪除 使其團聚 在springboot項目中添加freemarker依賴 將生成的test.ftl放在 ...
  • 1.如何理解 JS 中的this關鍵字? JS 初學者總是對this關鍵字感到困惑,因為與其他現代編程語言相比,JS 中的這this關鍵字有點棘手。 “this” 一般是表示當前所在的對象,但是事情並沒有像它應該的那樣發生。JS中的this關鍵字由函數的調用者決定,誰調用就this就指向哪個。如果找 ...
  • 1、what? 垃圾回收:js代碼想要運行,需要操作系統或者運行時提供記憶體空間,來存儲變數及它的值。在某些變數(例如局部變數)在不參與運行時,就需要系統回收被占用的記憶體空間,稱為垃圾回收 記憶體泄漏:某些情況下,不再用到的變數所占記憶體沒有及時釋放,導致程式運行中,記憶體越占越大,極端情況下可導致系統崩潰 ...
  • 最近在面試的時候,遇到過兩次問繼承實現的幾種方式,這裡能我給大家列舉了以下的這幾種,給大家參考參考 方式一:藉助構造函數實現繼承 這裡使用的原理就是在Child裡面,把Parent的this指向改為是Child的this指向,從而實現繼承 缺點:只能解決屬性的繼承,使用屬性的值不重覆,但是父級類別的 ...
  • 摘要: 高效使用Set! 作者:前端小智 原文: "如何使用 Set 來提高代碼的性能" "Fundebug" 經授權轉載,版權歸原作者所有。 為了保證的可讀性,本文采用意譯而非直譯。 我確信有很多開發人員堅持使用基本的全局對象:數字,字元串,對象,數組和布爾值。對於許多用例,這些都是需要的。 但是 ...
  • 恢復內容開始 這篇文章很長,但的確是一篇非常乾的乾貨,講訴了 HTML、JavaScript、CSS、jQuery使用的一些規範與建議,前端的同學可以認真閱讀此文,並比較自己平時的一些習慣,看是否有改進的地方…… HTML 咋的啦,DOCTYPE? 不定義DOCTYPE是一種可以被判死刑的罪行。 以 ...
  • 對象的擴展運算符(...),用於取出參數對象中的所有可遍歷屬性,然後拷貝到當前對象之中 對象擴展運算符: 1. 複製對象 let obj1 = { x: 1, y: 2, z: 3 } let obj2 = { ...obj1 } obj2 = { x: 1, y: 2, z: 3 } 2. 合併對 ...
  • 前端開發要學的知識內容涉及的會很寬泛,雖然說主要是HTML、CSS和JavaScript這些基礎知識點,但達妹今天想強調一下,學前端開發除了要學這些基礎知識外,學員還要在這之上進行延伸和深入的去學,而且互聯網時代不斷發展,掌握了這些新技術、新技能,在職場的競爭力必然會翻倍提升。 就前端開發要怎麼學的 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...