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

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

Vue.js提供了v-model指令用於雙向數據綁定,比如在輸入框上使用時,輸入的內容會事實映射到綁定的數據上,綁定的數據又可以顯示在頁面里,數據顯示的過程是自動完成的。 v-model本質上不過是語法糖。它負責監聽用戶的輸入事件以更新數據,並對一些極端場景進行一些特殊處理。例如: 渲染如下: 當我 ...


Vue.js提供了v-model指令用於雙向數據綁定,比如在輸入框上使用時,輸入的內容會事實映射到綁定的數據上,綁定的數據又可以顯示在頁面里,數據顯示的過程是自動完成的。

v-model本質上不過是語法糖。它負責監聽用戶的輸入事件以更新數據,並對一些極端場景進行一些特殊處理。例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <p>Message is: {{message}}</p>
        <input v-model="message" placeholder="edit me" type="text">
    </div>
    <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        new Vue({el: '#app',data(){return { message:'' }}})
    </script>
</body>
</html>

渲染如下:

當我們在輸入框輸入內容時,Message is:後面會自動顯示輸入框里的內容,反過來當修改Vue實例的message時,輸入框也會自動更新為該內容。

與事件的修飾符類似,v-model也有修飾符,用於控制數據同步的時機,v-model可以添加三個修飾符:lazy、number和trim,具體可以看官網。

我們如果不用v-model,手寫一些事件也可以實現例子里的效果,如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <p>Message is: {{message}}</p>
        <input :value="message" @input="message=$event.target.value" placeholder="edit me" type="text">
    </div>
    <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        new Vue({el: '#app',data(){return { message:'' }}})
    </script>
</body>
</html>

我們自己手寫的和用v-model有一點不同,就是當輸入中文時,輸入了拼音,但是沒有按回車時,p標簽也會顯示message信息的,而用v-model實現的雙向綁定是只有等到回車按下去了才會渲染的,這是因為v-model內部監聽了compositionstart和compositionend事件,有興趣的同學具體可以查看一下這兩個事件的用法,網上教程挺多的。

 

源碼分析


Vue是可以自定義指令的,其中v-model和v-show是Vue的內置指令,它的寫法和我們的自定義指令是一樣的,都保存到Vue.options.directives上,例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <script>
        console.log(Vue.options.directives)     //列印Vue.options.directives的值
    </script>
</body>
</html>

輸出如下:

Vue內部通過extend(Vue.options.directives, platformDirectives); 將v-model和v-show的指令信息保存到Vue.options.directives裡面,如下:

var platformDirectives = {                           //第8417行  內置指令 v-module和v-show  platformDirectives的意思是這兩個指令和平臺無關的,不管任何環境都可以用這兩個指令
  model: directive,
  show: show
}

extend(Vue.options.directives, platformDirectives); //第8515行 將兩個指令信息保存到Vue.options.directives裡面

Vue的源碼實現代碼比較多,我們一步步來,以上面的第一個例子為例,當Vue將模板解析成AST對象解析到input時會processAttrs()函數,如下:

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)) { // v-bind              //bindRD等於/^:|^v-bind:/ ,即該屬性是v-bind指令時
       /*v-bind邏輯*/
      } else if (onRE.test(name)) { // v-on           //onRE等於/^@|^v-on:/,即該屬性是v-on指令時
        /*v-on邏輯*/
      } else { // normal directives                   //普通指令
        name = name.replace(dirRE, '');                   //去掉指令首碼,比如v-model執行後等於model
        // parse arg
        var argMatch = name.match(argRE);                 //argRE等於:(.*)$/,如果name以:開頭的話
        var arg = argMatch && argMatch[1];
        if (arg) {
          name = name.slice(0, -(arg.length + 1));
        }
        addDirective(el, name, rawName, value, arg, modifiers);   //執行addDirective給el增加一個directives屬性,值是一個數組,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
        if ("development" !== 'production' && name === 'model') {
          checkForAliasModel(el, value);
        }
      }
    } else {
      /*普通特性的邏輯*/
    }
  }
}

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;
}

例子里的 <input v-model="message" placeholder="edit me" type="text">對應的AST對象如下:

接下來在generate生成rendre函數的時候,獲取data屬性時會執行genDirectives()函數,該函數會執行全局的model函數,也就是v-model的初始化函數,如下:

function genDirectives (el, state) {        //第10352行 獲取指令
  var dirs = el.directives;                   //獲取元素的directives屬性,是個數組,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
  if (!dirs) { return }                       //如果沒有directives則直接返回
  var res = 'directives:[';
  var hasRuntime = false;
  var i, l, dir, needRuntime;
  for (i = 0, l = dirs.length; i < l; i++) {        //遍歷dirs
    dir = dirs[i];                                  //每一個directive,例如:{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}
    needRuntime = true;
    var gen = state.directives[dir.name];           //獲取對應的指令函數,如果是v-model,則對應model函數,可能為空的,只有內部指令才有
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn);       //執行指令對應的函數,也就是全局的model函數
    }
    if (needRuntime) {
      hasRuntime = true;
      res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:\"" + (dir.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
    }
  }
  if (hasRuntime) { 
    return res.slice(0, -1) + ']'                 //去掉最後的逗號,並加一個],最後返回
  }
}

model()函數會根據不同的tag(select、input的不同)做不同的處理,如下:

function model (      //第6854行 v-model指令的初始化
  el,
  dir,
  _warn
) {   
  warn$1 = _warn;
  var value = dir.value;                                        //
  var modifiers = dir.modifiers;                                //修飾符
  var tag = el.tag;                                             //標簽名,比如:input
  var type = el.attrsMap.type;

  {
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === 'input' && type === 'file') {
      warn$1(
        "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
        "File inputs are read only. Use a v-on:change listener instead."
      );
    }
  }

  if (el.component) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {                              //如果typ為select下拉類型
    genSelect(el, value, modifiers);
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers);
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers);
  } else if (tag === 'input' || tag === 'textarea') {         //如果是input標簽,或者是textarea標簽
    genDefaultModel(el, value, modifiers);             //則執行genDefaultModel()函數
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false
  } else {
    warn$1(
      "<" + (el.tag) + " v-model=\"" + value + "\">: " +
      "v-model is not supported on this element type. " +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.'
    );
  }

  // ensure runtime directive metadata
  return true
}

genDefaultModel會在el的value綁定對應的值,並調用addHandler()添加對應的事件,如下:

function genDefaultModel (          //第6965行  nput標簽 和textarea標簽 el:AST對象 value:對應值
  el,
  value,
  modifiers
) {
  var type = el.attrsMap.type;                                  //獲取type值,比如text,如果未指定則為undefined

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  {
    var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];           //嘗試獲取動態綁定的value值
    var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];         //嘗試獲取動態綁定的type值
    if (value$1 && !typeBinding) {                                                //如果動態綁定了value 且沒有綁定type,則報錯
      var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';                  
      warn$1(
        binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +
        'because the latter already expands to a value binding internally'
      );
    }
  }

  var ref = modifiers || {}; 
  var lazy = ref.lazy;                                                        //獲取lazy修飾符
  var number = ref.number;                                                    //獲取number修飾符
  var trim = ref.trim;                                                        //獲取trim修飾符
  var needCompositionGuard = !lazy && type !== 'range';
  var event = lazy                                                            //如果有lazy修飾符則綁定為change事件,否則綁定input事件
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input';

  var valueExpression = '$event.target.value';
  if (trim) {                                                                 //如果有trim修飾符,則在值後面加上trim()
    valueExpression = "$event.target.value.trim()";
  }
  if (number) {                                                               //如果有number修飾符,則加上_n函數,就是全局的toNumber函數
    valueExpression = "_n(" + valueExpression + ")";
  }

  var code = genAssignmentCode(value, valueExpression);                       //返回一個表達式,例如:message=$event.target.value
  if (needCompositionGuard) {                                                 //如果需要composing配合,則在前面加上一段if語句
    code = "if($event.target.composing)return;" + code;
  }
 
  //雙向綁定就是靠著兩行代碼的
  addProp(el, 'value', ("(" + value + ")"));                                  //添加一個value的prop
  addHandler(el, event, code, null, true);                                    //添加event事件
  if (trim || number) {                                             
    addHandler(el, 'blur', '$forceUpdate()');
  }
}

渲染完成後對應的render函數如下:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v("Message is: "+_s(message))]),_v(" "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me","type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})])}

我們整理一下就看得清楚一點,如下:

with(this) {
    return _c('div', {
        attrs: {
            "id": "app"
        }
    },
    [_c('p', [_v("Message is: " + _s(message))]), _v(" "), _c('input', {
        directives: [{
            name: "model",
            rawName: "v-model",
            value: (message),
            expression: "message"
        }],
        attrs: {
            "placeholder": "edit me",
            "type": "text"
        },
        domProps: {
            "value": (message)
        },
        on: {
            "input": function($event) {
                if ($event.target.composing) return;
                message = $event.target.value
            }
        }
    })])
}

最後等DOM節點渲染成功後就會執行events模塊的初始化事件 並且會執行directive模塊的inserted鉤子函數:

var directive = {
  inserted: function inserted (el, binding, vnode, oldVnode) {      //第7951行
    if (vnode.tag === 'select') {
      // #6903
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', function () {
          directive.componentUpdated(el, binding, vnode);
        });
      } else {
        setSelected(el, binding, vnode.context);
      }
      el._vOptions = [].map.call(el.options, getValue);
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {      //如果tag是textarea節點,或者type為這些之一:text,number,password,search,email,tel,url
      el._vModifiers = binding.modifiers;                                       //保存修飾符
      if (!binding.modifiers.lazy) {                                            //如果沒有lazy修飾符,先後綁定三個事件
        el.addEventListener('compositionstart', onCompositionStart);
        el.addEventListener('compositionend', onCompositionEnd);
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener('change', onCompositionEnd);
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true;
        }
      }
    }
  },

onCompositionStart和onCompositionEnd分別對應compositionstart和compositionend事件,如下:

function onCompositionStart (e) {       //第8056行
  e.target.composing = true;
}

function onCompositionEnd (e) {
  // prevent triggering an input event for no reason
  if (!e.target.composing) { return }   //如果e.target.composing為false,則直接返回,即保證不會重覆觸發
  e.target.composing = false;
  trigger(e.target, 'input');               //觸發e.target的input事件
}

function trigger (el, type) {           //觸發el上的type事件 例如type等於:input
  var e = document.createEvent('HTMLEvents');   //創建一個HTMLEvents類型
  e.initEvent(type, true, true);                //初始化事件
  el.dispatchEvent(e);                           //向el這個元素派發e這個事件
}

最後執行的el.dispatchEvent(e)就會觸發我們生成的render函數上定義的input事件


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

-Advertisement-
Play Games
更多相關文章
  • 原文:https://www.jianshu.com/p/b5a484cecd7c 本文主要說明2018年蘋果開發者賬號申請的流程,申請流程相較於2017年有一些改變,希望大家能夠通過本文少走一些彎路,能夠順利完成開發者賬號的申請。關於新流程中可能出現的一些問題以及部分流程的變更均在下文中運用灰色塊 ...
  • 一、數據綁定 二、列表渲染 <!--index.wxml--> 三、綁定一個點擊事件 四、事件冒泡 五、事件傳參 ...
  • 系統自帶的分段選擇就是 UISegmentedControl ,也有一些大佬自定義的 Segmented ,比如Git上的 HMSegmentedControl ,我以前最初的項目中,也有用到過,如果自己寫,或者想自定義一些UI,該從哪裡出發,其實在用過 HMSegmentedControl 之後, ...
  • 第一步:手機連接到itunes 選擇本電腦備份 備份的時候不要加密 然後立即備份 第二步:前往文件夾,找到itunes的備份路徑~/Library/Application Support/MobileSync/Backup 在這目錄下搜索 3d0d開頭的文件,這就是 iPhone 簡訊的資料庫文件。 ...
  • 目前iOS組件化常用的解決方案是Pod+路由+持續集成,通常架構設計完成後第一步就是將原來工程里的模塊按照架構圖分解為一個個獨立的pod工程(組件),今天我們就來看看如何創建一個Pod私有庫。 新建:pod lib create 假設我們需要創建的庫名為TestLib,下麵我們使用Pod官方提供的創 ...
  • jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“Write Less, Do More”,即倡導寫更少的代碼,做更多的事情。它封裝JavaScript常用的功能代碼,提供... ...
  • 解釋CSS3 中新增的選擇器中最具有代表性的就是序選擇器,大致可以分為兩類: (1)同級別的第幾個(2)同類型的第幾個 先寫一個公共代碼 1.選中同級別中的第一個 註意點:不區分類型,只管取第一個,不管第一個是什麼標簽 解釋:在同級別中只選取第一個為h1標簽和div下的p標簽,然後在這些裡面只選p標 ...
  • Javascript是前端面試的重點,本文重點梳理下 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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...