文章寫做: Markdown Nice
做者:wangly
發佈地址:掘金,語雀
聲明:轉載註明做者以及地址
此次也時髦的對文章樣式進行了一些更改。但願你們可以喜歡綠綠的。javascript
哈嘍,你們好呀。我是wangly。 一名一年經驗的前端老倒黴蛋了,前兩篇文章很是感謝你們的支持,爲了感謝你們,此次給掘友們帶來了一篇關於Vue中常用到 v-model 指令的源碼分析,充分的給你們說說,碰到相似的面試題和工做上碰到的問題掃盲。但願看完以後能對你有幫助。本篇文章須要有必定的基礎,若是看不懂的話,反覆品讀,你會有一個成長。前端
不看源碼,咱們只會知曉它表面的工做流程,而不知曉其內部的運轉原理。就會有一種,知其然,而不知其因此然的感受。當某天面試官問你這個東西的時候,你只能回答出它的使用流程,而 get 不到深度,就會給人一種模棱兩可的感受。千里執行,始於足下,跟着我一塊兒探索它的奧祕吧。vue
v-model自己是一個指令語法糖,來爲input 和 指定的變量作一個雙向綁定的過程,下面咱們來看下model指令,它獲得了什麼東西。請看源碼(這裏使用打包後的代碼, 更加清晰)java
// model 函數
function model ( el, dir, _warn ) {
console.log(el)
console.log(dir)
console.log(_warn)
}
複製代碼
打印結果以下web
打印出現的結果給各位截個圖,其中:面試
ASTElement
AST語法元素
ASTDirection AST指令
下面的代碼,主要是用來v-model
綁定的元素獲取一些基本信息。express
data
的屬性名稱。
v-model.lazy="msg"
的修飾符會生成一個對象, { lazy: true }表示
lazy
修飾符存在。
v-model
綁定的標籤名稱。
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)
複製代碼
這裏作了個判斷,當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
在Vue中,v-model
先判斷,當前元素是標籤仍是組件,若是是組件,就調用genComponentModel
來去處理這個問題。組件v-model額外運行時,就返回。先對組件判斷,在而後對原生標籤作處理。如input
,select
,checkbox
等標籤的雙向綁定。下面給你們整理一下對應的處理方式吧。我想拆開來你們都能看懂。編輯器
// 判斷 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
的第一句話,就是將el
的attribute
的type
值。由於其中有一個新加入的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
進行獲取,若是modifiers
爲undefined
的話,那麼它就是一個空對象。
// 獲取修飾符列表
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代碼模板。這裏會對修飾符進行一個斷定。默認的event
爲input
,若是是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做用相同。轉換前,轉換後:
根據上圖,我想你已經知道它的做用了。沒錯。用來獲取當前綁定的數據模型。對屬性和對象屬性的作一個區分。由於咱們都知道,對象屬性更改有可能會丟失響應式,爲了以防萬一,因此才使用$set()
的方式。到了這裏,我想你也應該知道genAssignmentCode
是用來幹嗎的吧?一句話總結:
若是是屬性,就返回
value = assignment
,若是是對象屬性,就使用set('導出模型的exp', '導出模型的key', assignment)的方式。
導出後的code,除了range
須要經歷過needCompositionGuard
的過濾。爲code添加$event.target.composing
,這個實際上是對輸入法IME問題
的解決。防止非必要的軟更新問題。
什麼是IME問題:查看
當code
生成完畢後,那就開始對el進行改造,改造的過程分爲兩個方法addProp
,addHeader
。咱們分別來看看下它作了什麼吧。
addProp
方法主要是對el
的props
的屬性添加,來看一下,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
主要是將上面生成的code模板,添加給元素的event
事件。以下圖,能夠看出,el的event
的下面的事件值作一個處理。這樣在el中就會綁定一個事件。咱們能夠當作以下DOM:
// 轉換前 <input type="text" v-model="msg"> 複製代碼// 轉換後 <input type="text" :value="msg" @input="if(event.target.value"> 複製代碼
經過上面的默認事件,我想你對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) } 複製代碼
多選框的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
)
複製代碼
處理單選按鈕的v-model就沒有那麼多的花花腸子,若是理解了上面checkbox和input的解析,對於radio
,就是獲取bangding
的value
。隨後作修飾符的處理。而後按照套路通常添加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)
}
複製代碼
最後一個就是組件的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模板,爲下面le
的model
的作準備。組件和元素標籤不同,因此組件的模板就沒有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拉我上岸。非外包。