最近因为公司要用到Vue.js
开发,想着看一看源码,知己知彼。
从Vue对象的构造开始说起
本节目标,是能够理解下面这段代码背后发生的观察者系统所做的事情。
const v = new Vue({
data:{
a:1
}
})
v.$watch("a",()=>console.log("Hello,Vue"))
v.a = 4
首先要理解的是Vue在构造时(即 new Vue
)发生了哪些事情,这里直接可以翻看核心代码core文件夹下关于instance,描述Vue实例的相关部分。
在init.js中我们看到Vue初始化的部分。
function initMixin(){
//...
vm._self = vm
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)
}
可以看到的是,初始化的过程依次是 :
初始化生命周期->事件->'beforeCreate'的回调->状态初始化-> 'created'的回调->渲染初始化。
这与官方的实例生命周期图有些不一致,原因不是很清楚。
本节讨论中我们重点观察一下几大初始化语句中关于 initState(vm)
的部分。这里的vm
应该是Vue Component
,即Vue组件的意思。下面为initState(vm)
代码:
export function initState (vm: Component) {
vm._watchers = []
initProps(vm)
initMethods(vm)
initData(vm)
initComputed(vm)
initWatch(vm)
}
这里State
的作用其实很清晰了,initState的作业就可以概括为初始化props、data、computed、method、watched 5个成员,分别对应下面5个示例成员。
var vm = new Vue({
el: '#example',
props: ['message'],
data: {
message: 'Hello'
},
computed: { },
methods: { },
watch: { }
})
如果已本文开头的例子来说,我们这里就只分析一下initData(vm)
部分即可。
const v = new Vue({
data:{
a:1
}
})
我把initData
稍微删除了一部分,看起来清爽一些。
其中第二部分的proxy的作用是使访问自己的属性,就是访问子data的属性。
其作用具体体现为:
var data = { a: 1 }
var vm = new Vue({
data: data
})
vm.a === data.a // -> true 访问自己的属性,就是访问子data的属性。
那么initData
的作用可以概括为获取并代理data成员,并观察data成员
主线其实在于observe(data)
这句话,正是它实现了观察数据。直接进入源码文件Observer文件夹下的index.js找到observe方法,这里我再略作删减,方便查阅。
总结一下上面图片的代码的几个点或疑问:
1.我们可以说observe方法的作用就是遍历data成员并且使其响应式化
2.Observer构造函数对 data的数组成员和非数组成员,用了不同的处理方式
3.defineReactive就是关键,为Vue的data属性定义响应式的代码是使用Object.defineProperty
方法。其中get可以暂时理解为简单的获取值,set可以理解为观察新装,并发送通知。
4.get中Dep.target
因为没有介绍到Dep,暂时先跳过吧
于是我们现在对知识点的理解是这样的:
Object.defineProperty -- 双向绑定
因为我之前对js了解的实在是少,后来了解才知道Object.defineProperty
这么好用,它是前端中实现双向绑定的方法,show me the code 来介绍的话,下面是最直接的。
实现响应式的方式通常会分为Pull 和 Push,而这里无疑是Push的方式,由下往上发送通知,简单的示意图如下所示:
于是现在就了解了Observer中的defineReactive就是使其响应式化。
Dep订阅容器的实现
现在理解了Object.defineProperty
的作用,我们来理解一下观察者对应的订阅者Watcher和订阅容器Dep。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
这部分比较简单,一开始看不容易理解Dep中target的作用,后面会解释。
Watcher订阅者的实现
上一部分Dep只是订阅者的容器,现在来关注一下订阅者Watcher。
对于一个订阅者而言,最关心的功能无非以下几点:
1. Watcher是如何构造的?
2. 接受到观察者的通知时,如何实现 update 更新操作
3. 与Dep订阅容器相关的操作
首先直接看一下Watcher是如何构造的吧:
我们可以说Watcher在构造时的两个部分总结:
第1部分除了基本的成员初始化外,主要是构造Watcher中的第二参数expOrFn当为表达式或函数时进行的分析,因为你必须得知道你监视的是哪一个值,然后解析后赋值给this.getter。
v.$watch("a",()=>console.log("Hello,Vue"))
举例来说,Watcher的构造成员有vm
、ExpOrFn
、cb
,而这句例子中,v就是代码中的vm
,ExpOrFn
就是"a",cb
就是后面的回调。而第一部分就是解析表达式,然后知晓你监视的是"a"。
而重点其实在于第2部分和第三部分,这两部分直接解释了上文中Observer.defineProperty
中为什么要用到Dep.target
。
现在我们现在知道Watcher
和Dep
是包含关系,那么Dep
容器在什么时机增加Watcher
呢?答案就是通过触发 Observer.defineProperty
中关于属性的getter
,并且通过标记一个唯一的Dep.target
标签 , 在Observer.defineProperty.getter
中做if判断,如果这个唯一的标签值被标记,就知道当前访问值的不是其他成员,而是一个订阅者Watcher,于是就把它放入容器。
其中get不仅仅是构造时会调用,在update的时候也会发生调用get方法,get方法一言以蔽之,就是拽取最新的观察值。下图解释了get的流程,以及Dep.target的作用。
这里的第三步addDep和第四步clearupDep,一起的作用就是刷新一轮容器关系。
上面的步骤已经解释了Watcher构造发生了什么,再总结一下就是从初始化Watcher的各个成员之后,把Watcher放入Vue组件对应的Dep容器中,通过get方法获取观察属性的最新值,其中get每次调用都会刷新一轮容器关系
现在我们来看一下Watcher是如何响应通知的的:
于是我们现在可以把订阅者简单概括为:
因此了解了Observer、Watcher、Dep之后,整体的印象就会变成:
而实际执行开头那段代码的话,就会变成以下的步骤: