抛出问题
我们先来看一下下面这段代码
<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>
上述代码很简单,就不做过多的解释了。如果这段代码都看不懂,那下面也没必要再看下去了
问题重现步骤
我现在对上述代码做两种操作:
- 一进页面先在输入框中输入
hello vue
- 一进页面先点击click按钮进行赋值操作,再在输入框中输入
hello vue
上述两种情况分别会出现什么现象呢?
第一种操作,当我们在输入框中输入hello vue
的时候,class为message
的div中会联动出现hello vue
,也就是说info
中的message
属性是响应式的
第二种操作,当我们先进行赋值操作,之后无论在输入框中输入什么内容,class为message
的div中都不会联动出现任何值,也就是说info
中的message
属性非响应式的
问题引发的猜想
查阅vue官方文档我们得知vue
在初始化的时候会对data中所有已经定义
的对象及其子属性进行遍历,给他们添加getter
和setter
,使得他们变成响应式的(关于响应式这块之后会单开文章进行解析),但是vue
不能检测对象属性的添加或删除。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式属性
基于上述描述,我们先看第一种操作.直接在输入框中输入hello vue
,class为message
的div中会联动出现’hello vue’。但是我们看data
中只定义了info
对象,其中并没有定义message
属性,message
属于新增属性。根据vue
官方文档中说的,vue
不能检测对象属性的添加或删除,所以我猜测vue底层在解析v-model
指令的时候,每当触发表单元素的监听事件(例如input事件),就会有Vue.set()
操作,从而触发setter
带着这个猜测,我们来看第二种操作。一进页面先点击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
但是光猜测肯定是不够的,我们要用事实说话,做到有理有据。接下来我们就去看下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()
里面执行,执行步骤可以分为三个过程
- 解析模版字符串生成AST
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()
开始讲解
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:\"" +