首先需要明确两个概念——
数据绑定:一旦更新data中的某个属性数据,所有页面上直接使用或者间接使用了该属性的节点都会自动更新。
数据劫持:通过Object.defineProperty()来监视data中的所有属性(任意层次)数据的变化,一旦数据发生变化就去更新界面。
它们的关系:数据绑定是我们的目的,而数据劫持是vue中用来实现数据绑定的方式。
划重点:数据绑定的主角有两位,一是data对象中的属性:
data() { // 注意这里一共是四个属性:name、friends、friends.name、friends.age
return {
name: 'bob'
friends: {
name: 'Mary'
age: 28
}
}
}
二是页面上使用了该属性的节点,更具体的说,是模板中的表达式,类似于:
如上所示,一个属性可能会在模板中被多个表达式使用;另一方面,一个表达式也可能会用到多个属性(如上面的friends.name)——所以属性和表达式之间是多对多的关系。
为了管理属性和表达式之间错综复杂的关系,我们定义两个重要的构造函数:Dep(dependency的简写)和Watcher。
Vue会为data中的每个属性创建一个Dep的实例对象(记作dep),dep包含两个属性:唯一标识符id,以及subs(subsribers的简写)。subs初始时为空数组,随后会往其中添加watcher,表示当前属性在哪些表达式中被使用。
Vue还会为模板中的每个表达式创建一个Watcher的实例对象(记作watcher),watcher的属性有好几个,其中包含一个对象属性depIds,初始时为空对象,随后会往其中添加dep(键为dep.id,值为dep对象),表示当前表达式使用了哪些属性。
一个属性对应一个dep,一个表达式对应一个watcher,它们之间又相互引用,这就是响应式的基本架构,下面来看看具体是如何实现的。
vue文件的解析主要有三步:数据代理、数据劫持、模板解析。当数据代理完成后,Vue会遍历data中的属性,每次将一个属性的key、value,和data对象一起传给defineReactive方法。
defineReactive方法最关键的部分就是新写的get方法和set方法,其中get方法会在对属性执行读操作、且Dep.target有值时生效。这里可以提前透露一下,Dep.target的值就是某个watcher,然而此时此刻watcher一个都还没被创建,所以当前Dep.target的值为null,语句块根本没有机会被执行。而set方法要在数据修改时才生效,一时半会也没法生效。所以等到后面触发了这两个方法我们再来对其进行解读。
数据劫持完成后,下一步是模板解析。在每条指令(如v-text="name")被解析完成后,会为其包含的表达式(如"name")创建一个watcher对象:
其中updateFn是模板解析的核心操作:更新页面,但在这里不做讨论。我们只需要知道创建的watcher对象有三个参数:vm对象、对应的表达式和一个回调函数。显然。若对应的表达式中某个属性被更改,便会调用该回调函数来实现响应式更新页面。下面是Watcher实例化时的内部操作:
watcher实例有五个属性,前三个就是把传入的参数进行了保存;第四个就是前面提到的depIds,在后面它会用来存储相关的dep对象;而第五个属性是——表达式的初始值,那就必须得获取表达式中各个属性的值,就会触发这些属性的get方法,而且此时Dep.target已经指向了当前新创建的watcher,于是让我们再次返回上面defineReactive方法中的get方法。
假设当前watcher对应的表达式是'friends.name',它将要依次和friends和friends.name两个属性建立联系,但此时它的depIds还是空对象,friends和friends.name二者的subs也都是空数组。
读属性时,会触发dep.depend()方法
dep.depend()方法中,执行了当前的watcher对象的addDep方法
为了避免重复添加,addDep方法中先执行了一个判断,如果当前watcher和当前属性之间还未建立联系,便执行两个最关键的操作:给dep添加新的sub(watcher) 、给watcher添加新的dep,礼尚往来。
这两步完成后,当前的watcher.depIds中便加入了某个属性对应的dep,而该属性对应的dep.subs中也加入了当前的watcher。
类似的,接下来又会在当前的watcher和其它相关属性之间建立联系,结果就是watcher.depIds包含了多个属性,而这些属性的dep.subs分别加入当前的watcher。
当前watcher和所有相关属性都建立了联系后,下一个表达式又会创建一个watcher实例(记作watcher02),watcher02会和它的相关属性都建立联系。
这个感觉像是一个两层循环,外层遍历watcher,内层遍历当前watcher对应的dep,最终实现多对多的连接。
至此,我们就完成了数据绑定中的初始化部分。还有一部分操作,发生在数据更改后,比如:执行vm.name = harden。
此时会触发names属性中的set方法:
核心操作是dep.notify()方法
这也很好理解,就是遍历当前属性的subs,通知与之相关的watcher同步更新数据。
update只是个过渡方法,起作用的是run方法,其功能可以用一句话表达:如果修改后的值与原来的值不等,则执行更新界面的回调函数。(还记得watcher实例化时传入的那个回调函数吗!)
至此,vue1.0的数据绑定就基本实现了,总结一下:
在实现数据代理之后,模板解析之前进行数据劫持——为data中每个属性创建一个dep对象,并重写这些属性的get和set方法。在模板解析过程中,为每个表达式创建一个watcher对象,由于创建过程中需要读属性值,所以会触发属性的get方法,在get方法内部watcher和dep之间建立起多对多的联系;当data中某个属性被修改,会触发属性的set方法,该属性对应的dep会通知与之相关联的所有watcher对象,同步更新数据,实现响应式更新。