v-model源碼解析(超詳細)

拋出問題

咱們先來看一下下面這段代碼html

<template>
  <div>
    <div class="message">{{ info.message }}</div>
    <div><input v-model="info.message" type="text"></div>
    <button @click="change">click</button>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        info: {}
      }
    },
    methods: {
      change () {
        this.info.message = 'hello world'
      }
    }
  }
</script>

上述代碼很簡單,就不作過多的解釋了。若是這段代碼都看不懂,那下面也不必再看下去了vue

問題重現步驟

我如今對上述代碼作兩種操做:react

  1. 一進頁面先在輸入框中輸入hello vue
  2. 一進頁面先點擊click按鈕進行賦值操做,再在輸入框中輸入hello vue

上述兩種狀況分別會出現什麼現象呢?express

第一種操做,當咱們在輸入框中輸入hello vue的時候,class爲message的div中會聯動出現hello vue,也就是說info中的message屬性是響應式的segmentfault

第二種操做,當咱們先進行賦值操做,以後不管在輸入框中輸入什麼內容,class爲message的div中都不會聯動出現任何值,也就是說info中的message屬性非響應式的數組

問題引起的猜測

查閱vue官方文檔咱們得知vue在初始化的時候會對data中全部已經定義的對象及其子屬性進行遍歷,給他們添加gettersetter,使得他們變成響應式的(關於響應式這塊以後會單開文章進行解析),可是vue不能檢測對象屬性的添加或刪除。可是,可使用 Vue.set(object, propertyName, value)方法向嵌套對象添加響應式屬性app

基於上述描述,咱們先看第一種操做。直接在輸入框中輸入hello vue,class爲message的div中會聯動出現hello vue。可是咱們看data中只定義了info對象,其中並無定義message屬性,message屬於新增屬性。根據vue官方文檔中說的,vue不能檢測對象屬性的添加或刪除,因此我猜想vue底層在解析v-model指令的時候,每當觸發表單元素的監聽事件(例如input事件),就會有Vue.set()操做,從而觸發setterdom

帶着這個猜想,咱們來看第二種操做。一進頁面先點擊click按鈕,對info.message進行賦值,message屬於新增屬性,根據官方文檔中說的,此時message並非響應式的,沒問題。可是咱們接着在input輸入框中輸入值,class爲message的div中沒有聯動出現任何值,根據咱們對於第一種狀況的猜想,當輸入框監聽到input事件的時候,會對info中的message進行Vue.set()操做,因此理論上就算一開始click中是對新增屬性message直接賦值的,致使該屬性並不是響應式的,在通過輸入框input事件中的Vue.set()操做以後,應該會變成響應式的,而如今呈現出來的狀況並非這樣的啊,這是爲何呢?編輯器

聰明的大家應該已經猜到在Vue.set()底層源碼中,應該是會判斷message屬性是否一開始就在info中,若是存在就只是進行單純的賦值,不存在的話在進行響應式操做,綁定gettersetteride

可是光猜想確定是不夠的,咱們要用事實說話,作到有理有據。接下來咱們就去看下vue源碼中v-model這塊,看看是否是如咱們猜測的同樣

探索真相-源碼分析

v-model指令使用分爲兩種狀況:一種是在表單元素上使用,另一種是在組件上使用。咱們今天分析的是第一種狀況,也就是在表單元素上使用

v-model實現機制

咱們先簡單說下v-model的機制:v-model會把它關聯的響應式數據(如info.message),動態地綁定到表單元素的value屬性上,而後監聽表單元素的input事件:當v-model綁定的響應數據發生變化時,表單元素的value值也會同步變化;當表單元素接受用戶的輸入時,input事件會觸發,input的回調邏輯會把表單元素value最新值同步賦值給v-model綁定的響應式數據。

v-model實現原理

我用來分析的源碼是在vue官網安裝模塊裏面下載的開發版本(2.6.10),便於調試

編譯

咱們今天講的內容其實就是把模版編譯成render函數的一個流程,這裏不會對每步流程都展開講解,我能夠給出一個步驟實現的流程,你們有興趣的話能夠根據這個流程來閱讀代碼,提升效率
$mount()->compileToFunctions()->compile()->baseCompile()
真正的編譯過程都是在這個baseCompile()裏面執行,執行步驟能夠分爲三個過程

  1. 解析模版字符串生成AST
const ast = parse(template.trim(), options)
  1. 優化語法樹
optimize(ast, options)
  1. 生成代碼
const code = generate(ast, options)

而後咱們看下generate裏面的代碼,這也是咱們今天講的重點

function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }

generate() 首先經過 genElement()->genData$2()->genDirectives() 生成code,再把codewith(this){return ${code}}} 包裹起來,最終的到render函數。
接下來咱們從genDirectives()開始講解

genDirectives

在模板的編譯階段,v-model跟其餘指令同樣,會被解析到 el.directives中,以後會經過genDirectives方法處理這些指令,咱們這裏從genDirectives()重點開始講,至於怎麼到這步,若是你們感興趣的話,能夠從generate()開始看

function genDirectives (el, state) {
        var dirs = el.directives;
        if (!dirs) { return }
        var res = 'directives:[';
        var hasRuntime = false;
        var i, l, dir, needRuntime;
        for (i = 0, l = dirs.length; i < l; i++) {
          dir = dirs[i];
          needRuntime = true;
          var gen = state.directives[dir.name];
          if (gen) {
            // compile-time directive that manipulates AST.
            // returns true if it also needs a runtime counterpart.
            needRuntime = !!gen(el, dir, state.warn);
          }
          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.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
          }
        }
        if (hasRuntime) {
          return res.slice(0, -1) + ']'
        }
    }

我對上面這個代碼打個斷點,結合咱們上面的代碼例子,這樣子看的更清楚,以下圖:
getDirectives.png
咱們能夠看到傳進來的elAst語法樹,el.directivesel上的指令,在咱們這裏就是el-model的相關參數,而後賦值給變量dirs

往下看代碼,for循環中有段代碼:

var gen = state.directives[dir.name];
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn);
    }

這裏面的state.dirctives是什麼呢?打個斷點看一下,以下圖:
genDirectives_state.png
咱們能夠看到state.directives裏面包含了不少指令方法,model就在其中,

var gen = state.directives[dir.name];

其實就是等價於

var gen = state.directives[model];

因此代碼中的變量gen獲得的是model()

needRuntime = !!gen(el, dir, state.warn);

其實就是執行了model()

model

那咱們再來看看model這個方法裏面作了些什麼事情,先上model的代碼:

function model (el,dir,_warn) {
    warn$1 = _warn;
    var value = dir.value;
    var modifiers = dir.modifiers;
    var tag = el.tag;
    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.",
          el.rawAttrsMap['v-model']
        );
      }
    }

    if (el.component) {
      genComponentModel(el, value, modifiers);
      // component v-model doesn't need extra runtime
      return false
    } else if (tag === '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') {
      genDefaultModel(el, value, modifiers);
    } 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.',
        el.rawAttrsMap['v-model']
      );
    }

    // ensure runtime directive metadata
    return true
  }

model方法根據傳入的參數對tag的類型進行判斷,調用不一樣的處理邏輯,本demo中tag的類型爲input,因此會執行genDefaultModel方法

genDefaultModel

function genDefaultModel (el,value,modifiers) {
        var type = el.attrsMap.type;
        {
          var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];
          var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
          if (value$1 && !typeBinding) {
            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',
              el.rawAttrsMap[binding]
            );
          }
        }

        var ref = modifiers || {};
        var lazy = ref.lazy;
        var number = ref.number;
        var trim = ref.trim;
        var needCompositionGuard = !lazy && type !== 'range';
        var event = lazy
          ? 'change'
          : type === 'range'
            ? RANGE_TOKEN
            : 'input';

        var valueExpression = '$event.target.value';
        if (trim) {
          valueExpression = "$event.target.value.trim()";
        }
        if (number) {
          valueExpression = "_n(" + valueExpression + ")";
        }

        var code = genAssignmentCode(value, valueExpression);
        if (needCompositionGuard) {
          code = "if($event.target.composing)return;" + code;
        }

        addProp(el, 'value', ("(" + value + ")"));
        addHandler(el, event, code, null, true);
        if (trim || number) {
          addHandler(el, 'blur', '$forceUpdate()');
        }
  }

咱們對genDefaultModel()中的代碼進行分塊解析,首先看下面這段代碼:

是否同時具備指令v-modelv-bind
var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];
    var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
    if (value$1 && !typeBinding) {
      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',
        el.rawAttrsMap[binding]
      );
    }

這塊代碼其實就是解釋表單元素是否同時有指令v-modelv-bind

var ref = modifiers || {};
    var lazy = ref.lazy;
    var number = ref.number;
    var trim = ref.trim;
修飾符

這段代碼就是獲取修飾符lazy, number及trim

  1. .lazy 取代input監聽change事件
  2. .number 輸入字符串轉爲數字
  3. .trim 輸入首尾空格過濾
var needCompositionGuard = !lazy && type !== 'range';

這裏的needCompositionGuard後面再說有什麼用,如今只用知道默認是true就好了

var event = lazy
      ? 'change'
      : type === 'range'
        ? RANGE_TOKEN
        : 'input';

    var valueExpression = '$event.target.value';
    if (trim) {
      valueExpression = "$event.target.value.trim()";
    }
    if (number) {
      valueExpression = "_n(" + valueExpression + ")";
    }

上面這段代碼中,event = ‘input’,定義變量valueExpression,修飾符trimnumber在咱們這個demo中默認都沒有,因此跳過往下看

genAssignmentCode
var code = genAssignmentCode(value, valueExpression);
    if (needCompositionGuard) {
      code = "if($event.target.composing)return;" + code;
    }

這裏涉及到一個函數genAssignmentCode,上源碼:

function genAssignmentCode (
    value,
    assignment
  ) {
    var res = parseModel(value);
    if (res.key === null) {
      return (value + "=" + assignment)
    } else {
      return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
    }
  }

這段代碼是生成v-model綁定的value的值,看到這段代碼,咱們就知道離真相不遠了,由於咱們看到了$set()。如今咱們經過斷點具體分析下,以下圖:
getAssignmentCode.png
經過斷點咱們能夠很清楚的看到咱們先執行parseModel('info.message')獲取到一個對象res,因爲咱們的demo中綁定的值是路徑形式的對象,即info.message,因此此時res經過parseModel解析出來就是{exp: "info", key: "message"}。那下面的判斷就進入else,即:

return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")

回到上面的getDefaultModel()中

var code = genAssignmentCode(value, valueExpression);
    if (needCompositionGuard) {
      code = "if($event.target.composing)return;" + code;
    }

此時code獲取到genAssignmentCode()返回的字符串值"$set(info, "message", $event.target.value)"

$event.target.composing

上面我說的到變量needCompositionGuard = true,通過拼接,最終code = 「if($event.target.composing)return;$set(info, "message", $event.target.value)」

這裏的$event.target.composing有什麼用呢?其實就是用於判斷這次input事件是不是IME構成觸發的,若是是IME構成,直接return。IME 是輸入法編輯器(Input Method Editor) 的英文縮寫,IME構成指咱們在輸入文字時,處於未確認狀態的文字。如圖:
composing.png
帶下劃線的ceshi就屬於IME構成,它會一樣會觸發input事件,但不會觸發v-model更新數據。

繼續往下看

addProp(el, 'value', ("(" + value + ")"));
    addHandler(el, event, code, null, true);
    if (trim || number) {
      addHandler(el, 'blur', '$forceUpdate()');
    }
addProp

先說下addProp(el, 'value', ("(" + value + ")"))

function addProp (el, name, value, range, dynamic) {
      (el.props || (el.props = [])).push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
      el.plain = false;
    }

照常打個斷點看下:,以下圖
addProp.png

能夠看到此方法的功能爲給el添加props,首先判斷el上有沒有props,若是沒有的話建立props並賦值爲一個空數組,隨後拼接對象並推到props中,代碼在此demo中至關於push{name: "value", value: "(info.message)"}

若是一直往下追,能夠看到這個方法實際上是在input輸入框上綁定了value,對照咱們的demo來看,就是將<input v-model="info.message" type="text">變成<input v-bind:value="info.message" type="text">

addHandler

一樣的,addHandler()至關於在input上綁定了input事件,最終咱們demo的模版就會被編譯成

<input v-bind:value="info.message" v-on:input="info.message=$event.target.value">
render

後續再根據一些指令拼接,咱們最終的到的render以下:

with(this) {
    return _c('div', {
        attrs: {
            "id": "app-2"
        }
    }, [_c('div', [_v(_s(info.message))]), _v(" "), _c('div', [_c('input', {
        directives: [{
            name: "model",
            rawName: "v-model",
            value: (info.message),
            expression: "info.message"
        }],
        attrs: {
            "type": "text"
        },
        domProps: {
            "value": (info.message)
        },
        on: {
            "input": function ($event) {
                if ($event.target.composing) return;
                $set(info, "message", $event.target.value)
            }
        }
    })]), _v(" "), _c('button', {
        on: {
            "click": change
        }
    }, [_v("click")])])
}

最後經過createFunction()render代碼串經過new Function的方式轉換成可執行的函數,賦值給 vm.options.render,這樣當組件經過vm._render的時候,就會執行這個render函數

至此,針對表單元素上的v-model指令從開始編譯到最終生成render()並執行的過程就講解完了,咱們驗證了在編譯階段,v-model會在監聽到input事件時對咱們綁定的value進行Vue.$set()操做

還記得咱們上面說的對demo第二種操做狀況麼?先進行click操做賦值,那v-model中的Vue.$set()操做彷佛沒有做用了。咱們當時猜想的是Vue.$set()底層源碼中有應該是會判斷message屬性是否一開始就在info中,若是存在就只是進行單純的賦值,不存在的話在進行響應式操做,綁定gettersetter

如今咱們就去Vue.$set()中看一下

set

先上代碼:

/**
   * Set a property on an object. Adds the new property and
   * triggers change notification if the property doesn't
   * already exist.
   */
  function set (target, key, val) {
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    var ob = (target).__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
        'at runtime - declare it upfront in the data option.'
      );
      return val
    }
    if (!ob) {
      target[key] = val;
      return val
    }
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
  }

看到這句代碼了麼?這就是證據,驗證咱們猜測的證據

if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
}
驗證猜測

當咱們首先點擊click的時候,執行this.info.message = 'hello world',此時info對象中新增了一個message屬性。當咱們在input框中輸入值並觸發Vue.$set()時,key in targettrue,而且message又不是Object原型上的屬性,因此!(key in Object.prototype)也爲true,此時message屬性並非響應式屬性,沒有綁定setter,因此僅僅進行了單純的賦值操做。

而當咱們一進頁面首次input中執行輸入操做時,根據上面咱們的分析input框監聽到了input事件,先執行了Vue.$set()操做,由於時首次,因此info中尚未message屬性,因此上面的key in targetfalse,跳過了賦值操做,到了下面的

defineReactive$$1(ob.value, key, val);
ob.dep.notify();

這個defineReactive的做用就是爲message綁定了getter()setter(),以後再對message的賦值操做都會直接進入自身綁定的setter中進行響應式操做

一個意外的發現

我忽然奇想把vue的版本換到了2.3.0,發現v-model不能對demo中的message屬性實現響應化,跑去看了下vue更新日誌,發如今2.5.0版本中,有這麼一句話
now creates non-existent properties as reactive (non-recursive) e1da0d5, closes #5932 (See reasoning behind this change)
上面這句話的意思是從2.5.0版本開始支持將不存在的屬性響應化,非遞歸的。
由於message屬性一開始在info中並無定義,在2.3.0中,還不支持將不存在的屬性響應化的操做,因此對demo無效

總結

到這裏,咱們這篇文章就結束了 裏面有一些細節若是你們有興趣的話能夠本身再去深究一下。有時候很小的一個問題,背後牽扯到的知識點也是不少的,儘可能把每一個不懂背後的邏輯搞清楚,才能儘快的成爲你想成爲的人

參考資料

https://segmentfault.com/a/11...
https://blog.csdn.net/fabulou...

相關文章
相關標籤/搜索