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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...