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

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

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/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>

渲染以下:node

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

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

咱們若是不用v-model,手寫一些事件也能夠實現例子裏的效果,以下:數組

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/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事件,有興趣的同窗具體能夠查看一下這兩個事件的用法,網上教程挺多的。app

 

源碼分析dom


Vue是能夠自定義指令的,其中v-model和v-show是Vue的內置指令,它的寫法和咱們的自定義指令是同樣的,都保存到Vue.options.directives上,例如:ide

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/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事件

相關文章
相關標籤/搜索