逐行分析v-model源碼,助你工做面試排雷解難

歡迎你

文章寫做: Markdown Nice
做者:wangly
發佈地址:掘金,語雀
聲明:轉載註明做者以及地址
此次也時髦的對文章樣式進行了一些更改。但願你們可以喜歡綠綠的。javascript

哈嘍,你們好呀。我是wangly。 一名一年經驗的前端老倒黴蛋了,前兩篇文章很是感謝你們的支持,爲了感謝你們,此次給掘友們帶來了一篇關於Vue中常用到 v-model 指令的源碼分析,充分的給你們說說,碰到相似的面試題和工做上碰到的問題掃盲。但願看完以後能對你有幫助。本篇文章須要有必定的基礎,若是看不懂的話,反覆品讀,你會有一個成長。前端

爲何要看源碼?

不看源碼,咱們只會知曉它表面的工做流程,而不知曉其內部的運轉原理。就會有一種,知其然,而不知其因此然的感受。當某天面試官問你這個東西的時候,你只能回答出它的使用流程,而 get 不到深度,就會給人一種模棱兩可的感受。千里執行,始於足下,跟着我一塊兒探索它的奧祕吧。vue

勸退三連

  • v-model 作了什麼?
  • v-model 在什麼場景下能用,什麼場景下不能用?
  • v-model 解決了什麼問題?

開始發車咯

1.入口函數

v-model自己是一個指令語法糖,來爲input 和 指定的變量作一個雙向綁定的過程,下面咱們來看下model指令,它獲得了什麼東西。請看源碼(這裏使用打包後的代碼, 更加清晰)java

// model 函數
function model ( el, dir, _warn ) {
  console.log(el)
  console.log(dir)
  console.log(_warn)
}
複製代碼

打印結果以下web

打印出現的結果給各位截個圖,其中:面試

  • el 爲 ASTElement AST語法元素
  • dir 爲 ASTDirection AST指令
  • _warn 爲 一個警告函數

2.獲取v-model元素須要用到的一些屬性

下面的代碼,主要是用來v-model綁定的元素獲取一些基本信息。express

  • value: 綁定 data的屬性名稱。
  • modifiers: 修飾符對象,如 v-model.lazy="msg"的修飾符會生成一個對象, { lazy: true }表示 lazy修飾符存在。
  • tag: v-model 綁定的標籤名稱。
  • type: 元素的 attribute中的type類型
// 綁定`data`的屬性名稱
var value = dir.value;
// 修飾符列表
var modifiers = dir.modifiers; 
//標籤名稱, 
var tag = el.tag; 
// 元素的類型
var type = el.attrsMap.type; // 標籤類型
console.log(value, modifiers, tag, type)
複製代碼

3.當 input類型爲file的時候

這裏作了個判斷,當input且類型是file文件的話,則拋出一個警告。用來警示開發者。數組

{
  // 類型爲file的input是隻讀的,設置input的值可能會致使錯誤
  if (tag === 'input' && type === 'file') {
    warn$1(
      // error 信息
      "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
      "File inputs are read only. Use a v-on:change listener instead.",
      el.rawAttrsMap['v-model']
    );
  }
}
複製代碼

當咱們作一個file去使用v-model的時候,控制檯就直接打印了一條錯誤。app

4.根據不一樣形式,作不一樣的處理

在Vue中,v-model先判斷,當前元素是標籤仍是組件,若是是組件,就調用genComponentModel來去處理這個問題。組件v-model額外運行時,就返回。先對組件判斷,在而後對原生標籤作處理。如inputselectcheckbox等標籤的雙向綁定。下面給你們整理一下對應的處理方式吧。我想拆開來你們都能看懂。編輯器

  • 組件: genComponentModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • select下拉選擇框:getSelect( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • checkbox多選框: genCheckboxModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • Radio單選按鈕:genRadioModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • input & textarea (默認Model處理):genDefaultModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • 綁定的元素不支持v-model,則會提示錯誤。v-model不支持該元素。以下圖
// 判斷 el 是不是組件
if (el.component) {
  genComponentModel(el, value, modifiers);
  // component v-model doesn't need extra runtime
  return false
} else if (tag === 'select') {
  // 處理Select
  genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
  // 處理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);
  return false
} else {
  warn$1(
    // 若是不在處理範內,提示錯誤。v-model不支持該元素
    "<" + (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']
  );
}
複製代碼

默認處理方式genDefaultModel

genDefaultModel 主要是用來處理基本文本框和多選文本框。 處理實例: genDefaultModel的第一句話,就是將elattributetype值。由於其中有一個新加入的range與其餘的值是不同的。須要額外作出處理

var type = el.attrsMap.type;
複製代碼

其次,須要判斷v-bind:值與v-model是否衝突,若是衝突就會將錯誤添加到堆棧當中。因此咱們在控制檯能夠看到衝突的提示

// 若是v-bind:值與v-model衝突,則發出警告
// 除了帶有v-bind的輸入: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]
    )
  }
}
複製代碼

其次,在獲取當前修飾符的狀態去生成表達式,下面對modifiers 進行獲取,若是modifiersundefined的話,那麼它就是一個空對象。

// 獲取修飾符列表
var ref = modifiers || {}
// 懶加載修飾符
var lazy = ref.lazy
// 數字修飾符
var number = ref.number
// 空格過濾修飾符
var trim = ref.trim
// 在未打包下是這樣的
const { lazy, number, trim } = modifiers || {}
複製代碼

當獲取到了修飾符的狀態後,下一步開始生成event事件,由於其中有一些事件是vue本身定義的,好比:

// RANGE
export const RANGE_TOKEN = '__r'
// CHECK & RADIO
export const CHECKBOX_RADIO_TOKEN = '__c'
複製代碼

經過event,生成code代碼模板。這裏會對修飾符進行一個斷定。默認的eventinput,若是是lazy的話就使用change事件。不是的話對range作判斷。若是type是range的話就使用RANGE_TOKEN反之則就是input了。當生成了事件名後,根據不一樣的修飾符生成對應的value表現模板,經過genAssignmentCode方法,獲取代碼字符串。

// 非懶加載進度條時候
const needCompositionGuard = !lazy && type !== 'range'
// event 事件名稱
const event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'
// value模板。默認狀況下,做爲
let valueExpression = '$event.target.value'
if (trim) {
  // trim事件
  valueExpression = `$event.target.value.trim()`
}
if (number) {
  // _n($event.target.value)
  valueExpression = `_n(${valueExpression})`
}
// 獲取code
let code = genAssignmentCode(value, valueExpression)
// 若是是range,那麼須要對range的composing進行判斷。
if (needCompositionGuard) {
  code = `if($event.target.composing)return;${code}`
}
複製代碼

給出一個默認的實例,genAssignmentCode默認兩個參數,value assignment,咱們能夠看一下,它作了什麼,有什麼用?

// @ Function
export function genAssignmentCode ( value: string, assignment: string ): string {
  const res = parseModel(value)
![](https://imgkr.cn-bj.ufileos.com/783a46d8-0d4a-4ea7-9880-562b99f36f9d.png)
 if (res.key === null) { // value = xxxx return `${value}=${assignment}` } else { // $set()方式 return `$set(${res.exp}, ${res.key}, ${assignment})` } } 複製代碼

genAssignmentCode方法中,調用了一個parseModel方法。它的做用主要是作一個解析的過程,這裏就不去作介紹了。和JSON.parse做用相同。轉換前,轉換後:

  • 單獨msg
  • 對象中的msg

根據上圖,我想你已經知道它的做用了。沒錯。用來獲取當前綁定的數據模型。對屬性和對象屬性的作一個區分。由於咱們都知道,對象屬性更改有可能會丟失響應式,爲了以防萬一,因此才使用$set()的方式。到了這裏,我想你也應該知道genAssignmentCode是用來幹嗎的吧?一句話總結:

若是是屬性,就返回value = assignment,若是是對象屬性,就使用set('導出模型的exp', '導出模型的key', assignment)的方式。

導出後的code,除了range須要經歷過needCompositionGuard的過濾。爲code添加$event.target.composing,這個實際上是對輸入法IME問題的解決。防止非必要的軟更新問題。

什麼是IME問題:查看

code生成完畢後,那就開始對el進行改造,改造的過程分爲兩個方法addPropaddHeader。咱們分別來看看下它作了什麼吧。

addProp

addProp 方法主要是對elprops的屬性添加,來看一下,addProp作了什麼吧。

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

能夠看到,它主要就是給props添加一些屬性。看下圖能夠看到,el中props中數據更換爲了傳遞進來的參數了。

addHeadle

addHeadle主要是將上面生成的code模板,添加給元素的event事件。以下圖,能夠看出,el的event的下面的事件值作一個處理。這樣在el中就會綁定一個事件。咱們能夠當作以下DOM:

// 轉換前 <input type="text" v-model="msg"> 複製代碼// 轉換後 <input type="text" :value="msg" @input="if(event.target.composing)return;message =event.target.value"> 複製代碼

genSelect下拉選擇框

經過上面的默認事件,我想你對v-model的大體流程有了基本的概念,那麼就來聊一聊下拉選擇框的問題吧。相對於input默認的流程,select的話就少了addProp,只有一個addHeader的方法。在一開始有一個selectVal保存默認的val。能夠根據下面代碼,看下轉換前,轉換後的代碼

// 源碼
var number = modifiers && modifiers.number
// 默認數據
var selectedVal =
  'Array.prototype.filter' +
  '.call($event.target.options,function(o){return o.selected})' +
  '.map(function(o){var val = "_value" in o ? o._value : o.value;' +
  'return ' +
  (number ? '_n(val)' : 'val') +
  '})'
 // 生成後的代碼 Array.prototype.filter .call($event.target.options, function (o) { return o.selected }) .map(function (o) { var val = '_value' in o ? o._value : o.value return val })  複製代碼

其次是assignment的代碼模板,根據$event.target.multiple來去判斷到底是$$selectedVal 仍是 $$selectedVal[0]

var assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]';
複製代碼

最後就是生成code,而且將code和el的methods綁定。

var code = "var $$selectedVal = " + selectedVal + ";";
code = code + " " + (genAssignmentCode(value, assignment));
addHandler(el, 'change', code, null, true);
複製代碼

這是最後生成綁定的code:

var $selectedVal = Array.prototype.filter
  .call($event.target.options, function (o) {
    return o.selected
  })
  .map(function (o) {
    var val = '_value' in o ? o._value : o.value
    return val
  })
msg = $event.target.multiple ? $selectedVal : $selectedVal[0]
複製代碼

貼上genSelect的代碼

function genSelect ( el: ASTElement, value: string, modifiers: ?ASTModifiers ) {
  // 獲取numver指令
  const number = modifiers && modifiers.number
  // selectVal函數模板
  const selectedVal = `Array.prototype.filter` +
    `.call($event.target.options,function(o){return o.selected})` +
    `.map(function(o){var val = "_value" in o ? o._value : o.value;` +
    `return ${number ? '_n(val)' : 'val'}})`
 // assignment獲取值 const assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]' // 生成code let code = `var $selectedVal = ${selectedVal};` code = `${code} ${genAssignmentCode(value, assignment)}` // 添加事件並將code模板加入進去 addHandler(el, 'change', code, null, true) } 複製代碼

genCheckboxModel多選框

多選框的v-model 有了一個新的方法getBindingAttr ,那麼這個方法是用來幹什麼的呢? 其實主要是用來處理v-bind的數據。經過getAndRemoveAttr來去數據對val進行處理,其中主要是對v-bind: + msg兩種方式的數據處理,以下圖: getAndRemoveAttr 只會從數組attrsList中刪除attr,不會被processAttrs處理。隨後將el.attrsMap[name]拿出來,

function getBindingAttr(el, name, getStatic) {
  // 獲取綁定的value(動態的)
  var dynamicValue =
    getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
  // 根據value進行處理
  if (dynamicValue != null) {
    return parseFilters(dynamicValue)
  } else if (getStatic !== false) {
    var staticValue = getAndRemoveAttr(el, name)
    if (staticValue != null) {
      console.log(JSON.stringify(staticValue))
      return JSON.stringify(staticValue)
    }
  }
}
複製代碼

隨後就是添加Prop Handle的操做,這個參考上面的處理方式,作一些處理,處理後的event會有一個change事件,做爲值修改的方法:

var $a = msg,
  $el = $event.target,
  $c = $el.checked ? true : false
if (Array.isArray($a)) {
  var $v = '1',
    $i = _i($a, $v)
  if ($el.checked) {
    $i < 0 && (msg = $a.concat([$v]))
  } else {
    $i > -1 && (msg = $a.slice(0, $i).concat($a.slice($i + 1)))
  }
} else {
  msg = $c
}
複製代碼

添加props和handle的源碼,參考上面的分析。這裏就很少作贅述,只要知道,往prop添加了什麼?handle的方法是什麼?內容是什麼?

addProp(
  el,
  'checked',
  'Array.isArray(' +
    value +
    ')' +
    '?_i(' +
    value +
    ',' +
    valueBinding +
    ')>-1' +
    (trueValueBinding === 'true'
      ? ':(' + value + ')'
      : ':_q(' + value + ',' + trueValueBinding + ')')
)
addHandler(
  el,
  'change',
  'var $a=' +
    value +
    ',' +
    '$el=$event.target,' +
    '$c=$el.checked?(' +
    trueValueBinding +
    '):(' +
    falseValueBinding +
    ');' +
    'if(Array.isArray($a)){' +
    'var $v=' +
    (number ? '_n(' + valueBinding + ')' : valueBinding) +
    ',' +
    '$i=_i($a,$v);' +
    'if($el.checked){$i<0&&(' +
    genAssignmentCode(value, '$a.concat([$v])') +
    ')}' +
    'else{$i>-1&&(' +
    genAssignmentCode(value, '$a.slice(0,$i).concat($a.slice($i+1))') +
    ')}' +
    '}else{' +
    genAssignmentCode(value, '$c') +
    '}',
  null,
  true
)
複製代碼

genRadioModel單選框

處理單選按鈕的v-model就沒有那麼多的花花腸子,若是理解了上面checkbox和input的解析,對於radio,就是獲取bangdingvalue。隨後作修飾符的處理。而後按照套路通常添加Prop事件handle

function genRadioModel(el, value, modifiers) {
  // 獲取修飾符
  var number = modifiers && modifiers.number
  // 綁定的value值
  var valueBinding = getBindingAttr(el, 'value') || 'null'
  // number修飾符和非number修飾符下的區別.生成value處理方式
  valueBinding = number ? '_n(' + valueBinding + ')' : valueBinding
  // 添加prop
  addProp(el, 'checked', '_q(' + value + ',' + valueBinding + ')')
  // 添加事件
  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}
複製代碼

genComponentModel組件

最後一個就是組件的v-model的綁定的了。首先,須要知道如何實現組件的v-model,這裏給一個基本的demo。 點擊後:

<div id="app">
  <my-component v-model="title"></my-component>
</div>
<script src="./dist/vue.js"></script>
<script> Vue.component('my-component', { template: `<div> {{value}} <button @click="handleInput">提交input</button> </div>`, props: ['value'], methods: { handleInput() { this.$emit('input', '我觸發了input emit'); //觸發 input 事件,並傳入新值 } } }); new Vue({ el: '#app', data: { title: '我是默認' } }) </script>
複製代碼

能夠看到,當在組件中定義prop存在value的時候,將修改的值經過$emit發佈input事件發佈。從而能夠經過v-model來作一個雙向綁定。那麼咱們探究下組件內的v-model作了一些什麼吧。

// 解構指令
const { number, trim } = modifiers || {}
 // 基本value模板 const baseValueExpression = '$v' let valueExpression = baseValueExpression // trim下的模板語法 if (trim) { valueExpression = `(typeof ${baseValueExpression} === 'string'` + `? ${baseValueExpression}.trim()` + `: ${baseValueExpression})` } // number下的模板,執行了_n的代理方法toNumber if (number) { valueExpression = `_n(${valueExpression})` } // 獲取code模板 const assignment = genAssignmentCode(value, valueExpression) // 對el的model進行修改 el.model = { value: `(${value})`, expression: JSON.stringify(value), callback: `function (${baseValueExpression}) {${assignment}}`, } 複製代碼

其大部分都是在渲染code模板,爲下面lemodel的作準備。組件和元素標籤不同,因此組件的模板就沒有addProps, addHanndle這兩個步驟。取而代之的是是對el.model的修改。

尾篇

本文所述的v-model只是單獨的源碼分析,其實不少內容在渲染後的模板仍是要從一開始開始,模板的渲染,若是不看其餘的源碼壓根就不明白,舉個例子: number修飾符下都會給默認的valueExpression添加一個_n()其實就是一個函數,那麼這個函數是幹什麼的?

valueExpression = "_n(" + valueExpression + ")";
複製代碼

咱們能夠看到這個方法,其中_n指向了toNumber

function installRenderHelpers(target) {
  target._o = markOnce
  // _n
  target._n = toNumber
  ......
}
複製代碼

toNumber只是作一個很簡單的事情,將傳入的字符串轉換爲Int也就是number,若是轉換失敗就返回原來的字符串。

function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}
複製代碼

總結

vue的源碼很長,很晦澀。不少人只是看了一些免費視頻的分析,如:xxxxVue源碼解析。其實內容無非就是講了一些vue響應式MVVM淺顯的概念,就以爲vue不過如此。卻不知,只是夜郎自大。精心啃讀vue的源碼,會對工做中使用vue出現的一些問題。快速的找到解決方案。本篇文章只是對v-model的簡單的理解。若是面試官問到你,若是你看完,說不定可以吹半小時呢。固然,具體深刻,還須要去理解渲染的模板具體作了什麼。原本是準備通篇詳解。後面發現這樣寫的話就脫離了本文的範疇。屬於離題,超綱。 因此,若是你以爲技術停滯不前,不妨將vue反覆細品。 若是對你有幫助能夠評論 點贊 收藏三連。

有意換坑:
學歷:專科
經驗: 一年
目標地: 上海杭州深圳廣州 薪資: 8K ~ 12K 歡迎遠程boss拉我上岸。非外包。

相關文章
相關標籤/搜索