咱們先來看一下下面這段代碼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
hello vue
hello vue
上述兩種狀況分別會出現什麼現象呢?express
第一種操做,當咱們在輸入框中輸入hello vue
的時候,class爲message
的div中會聯動出現hello vue
,也就是說info
中的message
屬性是響應式的segmentfault
第二種操做,當咱們先進行賦值操做,以後不管在輸入框中輸入什麼內容,class爲message
的div中都不會聯動出現任何值,也就是說info
中的message
屬性非響應式的數組
查閱vue官方文檔咱們得知vue
在初始化的時候會對data中全部已經定義
的對象及其子屬性進行遍歷,給他們添加getter
和setter
,使得他們變成響應式的(關於響應式這塊以後會單開文章進行解析),可是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()
操做,從而觸發setter
dom
帶着這個猜想,咱們來看第二種操做。一進頁面先點擊click按鈕,對info.message
進行賦值,message
屬於新增屬性,根據官方文檔中說的,此時message
並非響應式的,沒問題。可是咱們接着在input
輸入框中輸入值,class爲message
的div中沒有聯動出現任何值,根據咱們對於第一種狀況的猜想,當輸入框監聽到input
事件的時候,會對info
中的message
進行Vue.set()
操做,因此理論上就算一開始click中是對新增屬性message
直接賦值的,致使該屬性並不是響應式的,在通過輸入框input
事件中的Vue.set()
操做以後,應該會變成響應式的,而如今呈現出來的狀況並非這樣的啊,這是爲何呢?編輯器
聰明的大家應該已經猜到在Vue.set()
底層源碼中,應該是會判斷message
屬性是否一開始就在info
中,若是存在就只是進行單純的賦值,不存在的話在進行響應式操做,綁定getter
和setter
ide
可是光猜想確定是不夠的,咱們要用事實說話,作到有理有據。接下來咱們就去看下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()
裏面執行,執行步驟能夠分爲三個過程
const ast = parse(template.trim(), options)
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函數。
接下來咱們從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) + ']' } }
我對上面這個代碼打個斷點,結合咱們上面的代碼例子,這樣子看的更清楚,以下圖:
咱們能夠看到傳進來的el
是Ast
語法樹,el.directives
是el
上的指令,在咱們這裏就是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
是什麼呢?打個斷點看一下,以下圖:
咱們能夠看到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的代碼:
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
方法
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-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
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
,修飾符trim
和number
在咱們這個demo中默認都沒有,因此跳過往下看
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()
。如今咱們經過斷點具體分析下,以下圖:
經過斷點咱們能夠很清楚的看到咱們先執行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)"
上面我說的到變量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構成指咱們在輸入文字時,處於未確認狀態的文字。如圖:
帶下劃線的ceshi就屬於IME構成,它會一樣會觸發input事件,但不會觸發v-model更新數據。
繼續往下看
addProp(el, 'value', ("(" + value + ")")); addHandler(el, event, code, null, true); if (trim || number) { addHandler(el, 'blur', '$forceUpdate()'); }
先說下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
,若是沒有的話建立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()
至關於在input
上綁定了input
事件,最終咱們demo的模版就會被編譯成
<input v-bind:value="info.message" v-on:input="info.message=$event.target.value">
後續再根據一些指令拼接,咱們最終的到的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
中,若是存在就只是進行單純的賦值,不存在的話在進行響應式操做,綁定getter
和setter
如今咱們就去Vue.$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 target
爲true
,而且message
又不是Object
原型上的屬性,因此!(key in Object.prototype)
也爲true
,此時message
屬性並非響應式屬性,沒有綁定setter
,因此僅僅進行了單純的賦值操做。
而當咱們一進頁面首次
在input
中執行輸入操做時,根據上面咱們的分析input
框監聽到了input
事件,先執行了Vue.$set()
操做,由於時首次,因此info
中尚未message
屬性,因此上面的key in target
爲false
,跳過了賦值操做,到了下面的
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...