Vue.js提供了v-model指令用於雙向數據綁定,比如在輸入框上使用時,輸入的內容會事實映射到綁定的數據上,綁定的數據又可以顯示在頁面里,數據顯示的過程是自動完成的。 v-model本質上不過是語法糖。它負責監聽用戶的輸入事件以更新數據,並對一些極端場景進行一些特殊處理。例如: 渲染如下: 當我 ...
Vue.js提供了v-model指令用於雙向數據綁定,比如在輸入框上使用時,輸入的內容會事實映射到綁定的數據上,綁定的數據又可以顯示在頁面里,數據顯示的過程是自動完成的。
v-model
本質上不過是語法糖。它負責監聽用戶的輸入事件以更新數據,並對一些極端場景進行一些特殊處理。例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> </head> <body> <div id="app"> <p>Message is: {{message}}</p> <input v-model="message" placeholder="edit me" type="text"> </div> <script> Vue.config.productionTip=false; Vue.config.devtools=false; new Vue({el: '#app',data(){return { message:'' }}}) </script> </body> </html>
渲染如下:
當我們在輸入框輸入內容時,Message is:後面會自動顯示輸入框里的內容,反過來當修改Vue實例的message時,輸入框也會自動更新為該內容。
與事件的修飾符類似,v-model也有修飾符,用於控制數據同步的時機,v-model可以添加三個修飾符:lazy、number和trim,具體可以看官網。
我們如果不用v-model,手寫一些事件也可以實現例子里的效果,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> </head> <body> <div id="app"> <p>Message is: {{message}}</p> <input :value="message" @input="message=$event.target.value" placeholder="edit me" type="text"> </div> <script> Vue.config.productionTip=false; Vue.config.devtools=false; new Vue({el: '#app',data(){return { message:'' }}}) </script> </body> </html>
我們自己手寫的和用v-model有一點不同,就是當輸入中文時,輸入了拼音,但是沒有按回車時,p標簽也會顯示message信息的,而用v-model實現的雙向綁定是只有等到回車按下去了才會渲染的,這是因為v-model內部監聽了compositionstart和compositionend事件,有興趣的同學具體可以查看一下這兩個事件的用法,網上教程挺多的。
源碼分析
Vue是可以自定義指令的,其中v-model和v-show是Vue的內置指令,它的寫法和我們的自定義指令是一樣的,都保存到Vue.options.directives上,例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> </head> <body> <script> console.log(Vue.options.directives) //列印Vue.options.directives的值 </script> </body> </html>
輸出如下:
Vue內部通過extend(Vue.options.directives, platformDirectives); 將v-model和v-show的指令信息保存到Vue.options.directives裡面,如下:
var platformDirectives = { //第8417行 內置指令 v-module和v-show platformDirectives的意思是這兩個指令和平臺無關的,不管任何環境都可以用這兩個指令 model: directive, show: show } extend(Vue.options.directives, platformDirectives); //第8515行 將兩個指令信息保存到Vue.options.directives裡面
Vue的源碼實現代碼比較多,我們一步步來,以上面的第一個例子為例,當Vue將模板解析成AST對象解析到input時會processAttrs()函數,如下:
function processAttrs (el) { //第9526行 對剩餘的屬性進行分析 var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { //遍歷每個屬性 name = rawName = list[i].name; value = list[i].value; if (dirRE.test(name)) { //如果該屬性以v-、@或:開頭,表示這是Vue內部指令 // mark element as dynamic el.hasBindings = true; // modifiers modifiers = parseModifiers(name); if (modifiers) { name = name.replace(modifierRE, ''); } if (bindRE.test(name)) { // v-bind //bindRD等於/^:|^v-bind:/ ,即該屬性是v-bind指令時 /*v-bind邏輯*/ } else if (onRE.test(name)) { // v-on //onRE等於/^@|^v-on:/,即該屬性是v-on指令時 /*v-on邏輯*/ } else { // normal directives //普通指令 name = name.replace(dirRE, ''); //去掉指令首碼,比如v-model執行後等於model // parse arg var argMatch = name.match(argRE); //argRE等於:(.*)$/,如果name以:開頭的話 var arg = argMatch && argMatch[1]; if (arg) { name = name.slice(0, -(arg.length + 1)); } addDirective(el, name, rawName, value, arg, modifiers); //執行addDirective給el增加一個directives屬性,值是一個數組,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}] if ("development" !== 'production' && name === 'model') { checkForAliasModel(el, value); } } } else { /*普通特性的邏輯*/ } } }
addDirective會給AST對象增加一個directives屬性,用於保存對應的指令信息,如下:
function addDirective ( //第6561行 指令相關,給el這個AST對象增加一個directives屬性,值為該指令的信息,比如: el, name, rawName, value, arg, modifiers ) { (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers }); el.plain = false; }
例子里的 <input v-model="message" placeholder="edit me" type="text">對應的AST對象如下:
接下來在generate生成rendre函數的時候,獲取data屬性時會執行genDirectives()函數,該函數會執行全局的model函數,也就是v-model的初始化函數,如下:
function genDirectives (el, state) { //第10352行 獲取指令 var dirs = el.directives; //獲取元素的directives屬性,是個數組,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}] if (!dirs) { return } //如果沒有directives則直接返回 var res = 'directives:['; var hasRuntime = false; var i, l, dir, needRuntime; for (i = 0, l = dirs.length; i < l; i++) { //遍歷dirs dir = dirs[i]; //每一個directive,例如:{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined} needRuntime = true; var gen = state.directives[dir.name]; //獲取對應的指令函數,如果是v-model,則對應model函數,可能為空的,只有內部指令才有 if (gen) { // compile-time directive that manipulates AST. // returns true if it also needs a runtime counterpart. needRuntime = !!gen(el, dir, state.warn); //執行指令對應的函數,也就是全局的model函數 } 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.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},"; } } if (hasRuntime) { return res.slice(0, -1) + ']' //去掉最後的逗號,並加一個],最後返回 } }
model()函數會根據不同的tag(select、input的不同)做不同的處理,如下:
function model ( //第6854行 v-model指令的初始化 el, dir, _warn ) { warn$1 = _warn; var value = dir.value; //值 var modifiers = dir.modifiers; //修飾符 var tag = el.tag; //標簽名,比如:input 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." ); } } if (el.component) { genComponentModel(el, value, modifiers); // component v-model doesn't need extra runtime return false } else if (tag === 'select') { //如果typ為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') { //如果是input標簽,或者是textarea標簽 genDefaultModel(el, value, modifiers); //則執行genDefaultModel()函數 } 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.' ); } // ensure runtime directive metadata return true }
genDefaultModel會在el的value綁定對應的值,並調用addHandler()添加對應的事件,如下:
function genDefaultModel ( //第6965行 nput標簽 和textarea標簽 el:AST對象 value:對應值 el, value, modifiers ) { var type = el.attrsMap.type; //獲取type值,比如text,如果未指定則為undefined // warn if v-bind:value conflicts with v-model // except for inputs with v-bind:type { var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']; //嘗試獲取動態綁定的value值 var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']; //嘗試獲取動態綁定的type值 if (value$1 && !typeBinding) { //如果動態綁定了value 且沒有綁定type,則報錯 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' ); } } var ref = modifiers || {}; var lazy = ref.lazy; //獲取lazy修飾符 var number = ref.number; //獲取number修飾符 var trim = ref.trim; //獲取trim修飾符 var needCompositionGuard = !lazy && type !== 'range'; var event = lazy //如果有lazy修飾符則綁定為change事件,否則綁定input事件 ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'; var valueExpression = '$event.target.value'; if (trim) { //如果有trim修飾符,則在值後面加上trim() valueExpression = "$event.target.value.trim()"; } if (number) { //如果有number修飾符,則加上_n函數,就是全局的toNumber函數 valueExpression = "_n(" + valueExpression + ")"; } var code = genAssignmentCode(value, valueExpression); //返回一個表達式,例如:message=$event.target.value if (needCompositionGuard) { //如果需要composing配合,則在前面加上一段if語句 code = "if($event.target.composing)return;" + code; } //雙向綁定就是靠著兩行代碼的 addProp(el, 'value', ("(" + value + ")")); //添加一個value的prop addHandler(el, event, code, null, true); //添加event事件 if (trim || number) { addHandler(el, 'blur', '$forceUpdate()'); } }
渲染完成後對應的render函數如下:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v("Message is: "+_s(message))]),_v(" "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me","type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})])}
我們整理一下就看得清楚一點,如下:
with(this) { return _c('div', { attrs: { "id": "app" } }, [_c('p', [_v("Message is: " + _s(message))]), _v(" "), _c('input', { directives: [{ name: "model", rawName: "v-model", value: (message), expression: "message" }], attrs: { "placeholder": "edit me", "type": "text" }, domProps: { "value": (message) }, on: { "input": function($event) { if ($event.target.composing) return; message = $event.target.value } } })]) }
最後等DOM節點渲染成功後就會執行events模塊的初始化事件 並且會執行directive模塊的inserted鉤子函數:
var directive = { inserted: function inserted (el, binding, vnode, oldVnode) { //第7951行 if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', function () { directive.componentUpdated(el, binding, vnode); }); } else { setSelected(el, binding, vnode.context); } el._vOptions = [].map.call(el.options, getValue); } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { //如果tag是textarea節點,或者type為這些之一:text,number,password,search,email,tel,url el._vModifiers = binding.modifiers; //保存修飾符 if (!binding.modifiers.lazy) { //如果沒有lazy修飾符,先後綁定三個事件 el.addEventListener('compositionstart', onCompositionStart); el.addEventListener('compositionend', onCompositionEnd); // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd); /* istanbul ignore if */ if (isIE9) { el.vmodel = true; } } } },
onCompositionStart和onCompositionEnd分別對應compositionstart和compositionend事件,如下:
function onCompositionStart (e) { //第8056行 e.target.composing = true; } function onCompositionEnd (e) { // prevent triggering an input event for no reason if (!e.target.composing) { return } //如果e.target.composing為false,則直接返回,即保證不會重覆觸發 e.target.composing = false; trigger(e.target, 'input'); //觸發e.target的input事件 } function trigger (el, type) { //觸發el上的type事件 例如type等於:input var e = document.createEvent('HTMLEvents'); //創建一個HTMLEvents類型 e.initEvent(type, true, true); //初始化事件 el.dispatchEvent(e); //向el這個元素派發e這個事件 }
最後執行的el.dispatchEvent(e)就會觸發我們生成的render函數上定義的input事件