拋出問題
我們先來看一下下面這段代碼
<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>
上述代碼很簡單,就不做過多的解釋了。如果這段代碼都看不懂,那下面也沒必要再看下去了
問題重現(xiàn)步驟
我現(xiàn)在對上述代碼做兩種操作:
- 一進(jìn)頁面先在輸入框中輸入
hello vue
- 一進(jìn)頁面先點擊click按鈕進(jìn)行賦值操作,再在輸入框中輸入
hello vue
上述兩種情況分別會出現(xiàn)什么現(xiàn)象呢?
第一種操作,當(dāng)我們在輸入框中輸入hello vue
的時候,class為message
的div中會聯(lián)動出現(xiàn)hello vue
,也就是說info
中的message
屬性是響應(yīng)式的
第二種操作,當(dāng)我們先進(jìn)行賦值操作,之后無論在輸入框中輸入什么內(nèi)容,class為message
的div中都不會聯(lián)動出現(xiàn)任何值,也就是說info
中的message
屬性非響應(yīng)式的
問題引發(fā)的猜想
查閱vue官方文檔我們得知vue
在初始化的時候會對data中所有已經(jīng)定義
的對象及其子屬性進(jìn)行遍歷,給他們添加getter
和setter
,使得他們變成響應(yīng)式的(關(guān)于響應(yīng)式這塊之后會單開文章進(jìn)行解析),但是vue
不能檢測對象屬性的添加或刪除。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套對象添加響應(yīng)式屬性
基于上述描述,我們先看第一種操作。直接在輸入框中輸入hello vue
,class為message
的div中會聯(lián)動出現(xiàn)hello vue
。但是我們看data
中只定義了info
對象,其中并沒有定義message
屬性,message
屬于新增屬性。根據(jù)vue
官方文檔中說的,vue
不能檢測對象屬性的添加或刪除,所以我猜測vue底層在解析v-model
指令的時候,每當(dāng)觸發(fā)表單元素的監(jiān)聽事件(例如input事件),就會有Vue.set()
操作,從而觸發(fā)setter
帶著這個猜測,我們來看第二種操作。一進(jìn)頁面先點擊click按鈕,對info.message
進(jìn)行賦值,message
屬于新增屬性,根據(jù)官方文檔中說的,此時message
并不是響應(yīng)式的,沒問題。但是我們接著在input
輸入框中輸入值,class為message
的div中沒有聯(lián)動出現(xiàn)任何值,根據(jù)我們對于第一種情況的猜測,當(dāng)輸入框監(jiān)聽到input
事件的時候,會對info
中的message
進(jìn)行Vue.set()
操作,所以理論上就算一開始click中是對新增屬性message
直接賦值的,導(dǎo)致該屬性并非響應(yīng)式的,在經(jīng)過輸入框input
事件中的Vue.set()
操作之后,應(yīng)該會變成響應(yīng)式的,而現(xiàn)在呈現(xiàn)出來的情況并不是這樣的啊,這是為什么呢?
聰明的你們應(yīng)該已經(jīng)猜到在Vue.set()
底層源碼中,應(yīng)該是會判斷message
屬性是否一開始就在info
中,如果存在就只是進(jìn)行單純的賦值,不存在的話在進(jìn)行響應(yīng)式操作,綁定getter
和setter
但是光猜測肯定是不夠的,我們要用事實說話,做到有理有據(jù)。接下來我們就去看下vue
源碼中v-model
這塊,看看是不是如我們猜想的一樣
探索真相-源碼分析
v-model
指令使用分為兩種情況:一種是在表單元素上使用,另外一種是在組件上使用。我們今天分析的是第一種情況,也就是在表單元素上使用
v-model
實現(xiàn)機(jī)制
我們先簡單說下v-model
的機(jī)制:v-model
會把它關(guān)聯(lián)的響應(yīng)式數(shù)據(jù)(如info.message
),動態(tài)地綁定到表單元素的value屬性上,然后監(jiān)聽表單元素的input
事件:當(dāng)v-model
綁定的響應(yīng)數(shù)據(jù)發(fā)生變化時,表單元素的value值也會同步變化;當(dāng)表單元素接受用戶的輸入時,input
事件會觸發(fā),input
的回調(diào)邏輯會把表單元素value最新值同步賦值給v-model
綁定的響應(yīng)式數(shù)據(jù)。
v-model
實現(xiàn)原理
我用來分析的源碼是在vue官網(wǎng)安裝模塊里面下載的開發(fā)版本(2.6.10),便于調(diào)試
編譯
我們今天講的內(nèi)容其實就是把模版編譯成render
函數(shù)的一個流程,這里不會對每步流程都展開講解,我可以給出一個步驟實現(xiàn)的流程,大家有興趣的話可以根據(jù)這個流程來閱讀代碼,提高效率
$mount()->compileToFunctions()->compile()->baseCompile()
真正的編譯過程都是在這個baseCompile()
里面執(zhí)行,執(zhí)行步驟可以分為三個過程
- 解析模版字符串生成AST
const ast = parse(template.trim(), options)
- 優(yōu)化語法樹
optimize(ast, options)
- 生成代碼
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
,再把code
用 with(this){return ${code}}}
包裹起來,最終的到render函數(shù)。
接下來我們從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) + ']'
}
}
我對上面這個代碼打個斷點,結(jié)合我們上面的代碼例子,這樣子看的更清楚,如下圖:
我們可以看到傳進(jìn)來的el
是Ast
語法樹,el.directives
是el
上的指令,在我們這里就是el-model
的相關(guān)參數(shù),然后賦值給變量dirs
往下看代碼,for循環(huán)中有段代碼:
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
是什么呢?打個斷點看一下,如下圖:
我們可以看到state.directives
里面包含了很多指令方法,model
就在其中,
var gen = state.directives[dir.name];
其實就是等價于
var gen = state.directives[model];
所以代碼中的變量gen
得到的是model()
needRuntime = !!gen(el, dir, state.warn);
其實就是執(zhí)行了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
方法根據(jù)傳入的參數(shù)對tag的類型進(jìn)行判斷,調(diào)用不同的處理邏輯,本demo中tag的類型為input
,所以會執(zhí)行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()
中的代碼進(jìn)行分塊解析,首先看下面這段代碼:
是否同時具有指令v-model
和v-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-model
和v-bind
var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
修飾符
這段代碼就是獲取修飾符lazy, number及trim
- .lazy 取代input監(jiān)聽change事件
- .number 輸入字符串轉(zhuǎn)為數(shù)字
- .trim 輸入首尾空格過濾
var needCompositionGuard = !lazy && type !== 'range';
這里的needCompositionGuard
后面再說有什么用,現(xiàn)在只用知道默認(rèn)是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
,修飾符trim
和number
在我們這個demo中默認(rèn)都沒有,所以跳過往下看
genAssignmentCode
var code = genAssignmentCode(value, valueExpression);
if (needCompositionGuard) {
code = "if($event.target.composing)return;" + code;
}
這里涉及到一個函數(shù)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
的值,看到這段代碼,我們就知道離真相不遠(yuǎn)了,因為我們看到了$set()
。現(xiàn)在我們通過斷點具體分析下,如下圖:
通過斷點我們可以很清楚的看到我們先執(zhí)行parseModel('info.message')
獲取到一個對象res
,由于我們的demo中綁定的值是路徑形式的對象,即info.message
,所以此時res通過parseModel
解析出來就是{exp: "info", key: "message"}
。那下面的判斷就進(jìn)入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
,經(jīng)過拼接,最終code = “if($event.target.composing)return;$set(info, "message", $event.target.value)”
這里的$event.target.composing
有什么用呢?其實就是用于判斷此次input事件是否是IME構(gòu)成觸發(fā)的,如果是IME構(gòu)成,直接return。IME 是輸入法編輯器(Input Method Editor) 的英文縮寫,IME構(gòu)成指我們在輸入文字時,處于未確認(rèn)狀態(tài)的文字。如圖:
帶下劃線的ceshi就屬于IME構(gòu)成,它會同樣會觸發(fā)input事件,但不會觸發(fā)v-model更新數(shù)據(jù)。
繼續(xù)往下看
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;
}
照常打個斷點看下:,如下圖
可以看到此方法的功能為給el
添加props
,首先判斷el
上有沒有props
,如果沒有的話創(chuàng)建props
并賦值為一個空數(shù)組,隨后拼接對象并推到props
中,代碼在此demo中相當(dāng)于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()
相當(dāng)于在input
上綁定了input
事件,最終我們demo的模版就會被編譯成
<input v-bind:value="info.message" v-on:input="info.message=$event.target.value">
render
后續(xù)再根據(jù)一些指令拼接,我們最終的到的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
的方式轉(zhuǎn)換成可執(zhí)行的函數(shù),賦值給 vm.options.render
,這樣當(dāng)組件通過vm._render
的時候,就會執(zhí)行這個render
函數(shù)
至此,針對表單元素上的v-model
指令從開始編譯到最終生成render()
并執(zhí)行的過程就講解完了,我們驗證了在編譯階段,v-model
會在監(jiān)聽到input
事件時對我們綁定的value
進(jìn)行Vue.$set()
操作
還記得我們上面說的對demo第二種操作情況么?先進(jìn)行click操作賦值,那v-model
中的Vue.$set()
操作似乎沒有作用了。我們當(dāng)時猜測的是Vue.$set()
底層源碼中有應(yīng)該是會判斷message
屬性是否一開始就在info
中,如果存在就只是進(jìn)行單純的賦值,不存在的話在進(jìn)行響應(yīng)式操作,綁定getter
和setter
現(xiàn)在我們就去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
}
看到這句代碼了么?這就是證據(jù),驗證我們猜想的證據(jù)
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
驗證猜想
當(dāng)我們首先點擊click的時候,執(zhí)行this.info.message = 'hello world'
,此時info
對象中新增了一個message
屬性。當(dāng)我們在input
框中輸入值并觸發(fā)Vue.$set()
時,key in target
為true
,并且message
又不是Object
原型上的屬性,所以!(key in Object.prototype)
也為true
,此時message
屬性并不是響應(yīng)式屬性,沒有綁定setter
,所以僅僅進(jìn)行了單純的賦值操作。
而當(dāng)我們一進(jìn)頁面首次
在input
中執(zhí)行輸入操作時,根據(jù)上面我們的分析input
框監(jiān)聽到了input
事件,先執(zhí)行了Vue.$set()
操作,因為時首次,所以info
中還沒有message
屬性,所以上面的key in target
為false
,跳過了賦值操作,到了下面的
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
這個defineReactive
的作用就是為message
綁定了getter()
和setter()
,之后再對message
的賦值操作都會直接進(jìn)入自身綁定的setter
中進(jìn)行響應(yīng)式操作
一個意外的發(fā)現(xiàn)
我突然奇想把vue的版本換到了2.3.0,發(fā)現(xiàn)v-model
不能對demo中的message
屬性實現(xiàn)響應(yīng)化,跑去看了下vue更新日志,發(fā)現(xiàn)在2.5.0版本中,有這么一句話
now creates non-existent properties as reactive (non-recursive) e1da0d5, closes #5932 (See reasoning behind this change)
上面這句話的意思是從2.5.0版本開始支持將不存在的屬性響應(yīng)化,非遞歸的。
因為message
屬性一開始在info中并沒有定義,在2.3.0中,還不支持將不存在的屬性響應(yīng)化的操作,所以對demo無效
總結(jié)
到這里,我們這篇文章就結(jié)束了 里面有一些細(xì)節(jié)如果大家有興趣的話可以自己再去深究一下。有時候很小的一個問題,背后牽扯到的知識點也是很多的,盡量把每個不懂背后的邏輯搞清楚,才能盡快的成為你想成為的人
參考資料
https://segmentfault.com/a/1190000015848976#articleHeader0
https://blog.csdn.net/fabulous1111/article/details/85265503