咱们先来看一下下面这段代码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...