学习Vue源码

记一次阅读 Vue 源码的总结.

响应式原理

Vue 通过响应式在修改数据的时候更新视图。

在官网 copy 的介绍响应式原理的图.

流向为: model -> view
可以对表单元素v-model来进行双向数据绑定.

数据劫持 & 将数据变成可观察的(Observable)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//   observer/index.js

/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
// Observer是一个可以附加到每一个被观察的对象的类。一次附加,观察者将会将目标对象的属性键转化为拥有getter/setters的访问器属性,可以用来收集依赖(getter)和在属性有变化时分派通知更新(setter)
export class Observer {
value: any;
dep: Dep; //dep为一个Dep(依赖)类型
vmCount: number; // number of vms that has this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep() // 创建新的Dep实例
this.vmCount = 0

//前面还有一些对传入构造函数的参数的类型的判断,而这里执行的话,value是个对象,从下面的walk方法可以看出
this.walk(value)
}

/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
//尤大这里注释的意思是,Wlak方法遍历传进来的对象的每一个属性,把它们转化为访问器属性(getter/setter),这个方法只在参数是对象类型的时候被调用.
walk (obj: Object) {
//用Object.keys方法获取obj对象的所有属性.
const keys = Object.keys(obj)
//遍历属性,defineReactive方法处理.
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}


/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/

/**
* 尝试为一个value创建一个观察者实例,如果value已经拥有一个存
* 的观察者实例,则返回它。
* 如果成功创建了,则返回新的观察者实例.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
//ob是一个Observer实例
let ob: Observer | void
// ...
// ...
//上面忽略的部分主要是防止为一个data重复创建Observer实例
ob = new Observer()
return ob //返回一个Observer实例
}


/**
* Define a reactive property on an Object.
*/
/** 此方法用来定义一个对象的响应式属性 这里的reactive我理解为响应式.
* defineReactive方法
*/

export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, //这里使用的是flow,一种静态类型检查工具.
shallow?: boolean //shallow 浅的意思,应该是来判断val是一个基本属性还是引用属性吧.
) {
//对obj对象里的key属性描述进行检索,返回一个描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)

//如果该属性描述符对象的configurable(能否修改属性)为false,则返回
if (property && property.configurable === false) {
return
}

//获取该属性已经定义了的 getter/setters 函数
// cater for pre-defined getter/setters
const getter = property && property.get
if (!getter && arguments.length === 2) {
//没有传入val参数且 getter不存在的话,val的值为key的value.
val = obj[key]
}
const setter = property && property.set

// 如果shallow为假,代表说val为一个对象,则递归调用ovserve方法来将子对象里的属性变成可观察的.
let childOb = !shallow && observe(val)

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//如果getter存在,则调用getter获取到值
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() //将Dep.target指的watch实例与当前val的dep实例连接起来。将watch实例push进dep实例维护的订阅者数组.
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
// 主要是实现依赖收集。下面第二节分析Dep.
},
set: function reactiveSetter (newVal) {

//取得旧值value
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
//旧值value与newVal比较,若一样,跳出,不继续执行往下的操作。
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}

if (setter) {
//如果setter存在, 执行setter函数,传入obj的this,和传入参数newVal
setter.call(obj, newVal)
} else {
//不存在就直接把新值赋给val
val = newVal
}

// 如果shallow为假,代表说val为一个对象,则递归调用ovserve方法来将子对象里的属性变成可观察的.
childOb = !shallow && observe(newVal)

//通知在订阅者数组内的订阅者
dep.notify()
}
})
}

小结

initData 源码

1
2
3
4
5
6
function initData(vm: Component) {
//...
//...
// observe data
observe(data, true /* asRootData */)
}

Vue在其初始化时调用observe方法,为data对象的每个属性利用Object.defineProperty()方法给每个属性添加getter, setter,让每个属性都变成observable(可观察的).如果属性的value为一个对象的话,那么内部会递归调用observe方法来给子对象的属性创建观察者实例,使其变为observable的.
(其实还会将 template 上的每个v-指令还有computedprops的数据全部转化为observale)

当 Vue 进行 render 时,需要取数据,就会触发getter,进行依赖收集, (将订阅者(watcher)和观察者观察的(data)绑定起来),修改某一个可观察属性时,就会触发该属性的setter,通知订阅者(watcher)进行更新操作.

依赖(dependence)Dep

这个 dep 对象上可以挂载多个订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
/**
* 这里我理解为,一个dep实例可以挂载多个订阅者.
*/
export default class Dep {
static target: ?Watcher
id: number //用来表示
subs: Array<Watcher>

constructor() {
this.id = uid++
this.subs = []
}

// 将订阅者添加到subs数组
addSub(sub: Watcher) {
this.subs.push(sub)
}

//从subs里移除某个订阅者
removeSub(sub: Watcher) {
remove(this.subs, sub)
}

//用与将watcher与Dep的绑定
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}

//通知, 通知subs里的所有订阅者(watcher实例)执行update操作
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 这里定义了个全局变量 Dep.target,用于确定当前的watcher实例.任何时候只有一个watcher实例可以给评估.
// 因为在watcher内部,当它要把自己加进某个data的订阅者数组里时,会将其自身赋给Dep.target,然后触发data的getter,这时就会将其push进subs数组,然后watch再把Dep.target置空.
Dep.target = null

//一个target栈,用以评估watcher实例
const targetStack = []

//这两个方法暴露给watcher使用.用于来给Dep.target指示一个watcher.
export function pushTarget(_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

export function popTarget() {
Dep.target = targetStack.pop()
}

小结

读取数据,触发observable data的 getter,通过Dep将 data 与订阅者添加一个依赖关系。一个 data 对应一个Dep实例,其有一个 uniqueID,用来标识,其内部维护一个subs订阅者数组。当 data 改变,触发 setter,会通过Dep实例来给 subs 里的所有订阅者通知更新。

Watcher 订阅者

订阅者主要作用:

  1. 通过 Dep(依赖)来为每个数据添加订阅者,当数据变化时,会通过Dep来通知订阅者进行update操作.
  2. 创建订阅者实例会传入一个 cb 函数,当 update 执行完后会执行这个 cb 函数。cb 函数是用来执行 re-render 操作的,用于将新数据重新渲染到 view 上.

这里就贴我自己写的watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import Dep from './dep'

export default class Watcher {
constructor(vm, exp, cb) {
this.vm = vm //在vue源码里,这里是一个component
this.exp = exp //某个属性
this.cb = cb
this.value = this.get()
}

/**
* 调度者接口,当订阅数据更新时执行
*/
update() {
console.log('订阅者里的update操作.')
this.run()
}

//用来触发getter获取到最新的新值.
get() {
Dep.target = this
let value = this.vm[this.exp] //触发key的getter,将自己添加为订阅者.
//添加完后
Dep.target = null

return value
}

/**
* 调度者工作接口,由调度者接口调用
*/
// vue源码里 run执行后执行cb函数,视图重新渲染
run() {
let value = this.get()
console.log('run: ', value)
let oldVal = this.value

if (oldVal !== value) {
this.value = value
this.cb.call(this.vm, value, oldVal)
console.log('执行完毕回调')
}
}
}

对三者的一个小结

首先是组件渲染函数,进行一次 render 操作就会取到所有的数据,这时就会触发 getter,(这里只取到了视图渲染需要的数据触发 getter),从而进行依赖收集,Data可以看成与Watcher绑定在一起,但是我们知道他们中间是有个Dep在维护的他们的关系的,当触发 getter 时,将 watcher 添加到 Dep 的subs数组。数据改变时,触发 setter,Dep通知subs里的所有 watcher【你订阅的数据改变了】,watcher 会触发 update,而实际工作是由run来执行,判断新旧数据是否一致,如果数据真的改变了,则会触发 watcher 实例的 cb 函数,继续将工作交给 compiler 来做,最终结果会重新渲染视图(一般是渲染部分视图),因为 vue 也采用了 virtual DOM 技术,(用 JS 对象来模仿 DOM 节点,避免多次直接 DOM 操作,提高性能)监听到 VNode 变化时,会先通过 diff 算法,判断初始虚拟 DOM 树和改变后的虚拟 DOM 树是否有差别,具体哪里发生改变,思想是修改尽量少的 DOM,进行尽量少的 DOM 操作,最后把发生改变的虚拟 DOM 元素应用到真实 DOM 上.(patch操作)

再看上图. touch

在源码watcher.js也看到touch,关于这个我自己是这样理解的.

1
2
3
4
5
6
7
8
9
//observer/watcher.js

// ...

// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}

“touch” every property so they are all tracked as dependencies for deep watching

这里应该是来对值为object or array的进一步处理, 触发每个深层对象的依赖.

图中的touch也可以理解为访问’touch’ every needed property,多了个 needed,就是访问所有需要用到的属性.

自己的实现

模仿 Vue 完成小作业, 因为还没有去看 Vue 的模版渲染,所以就只实现了 Observer、Dep、Watcher 这三者的逻辑关系,最终结果一个简单的观察订阅者模式的 demo。

demo in github

题外话

浅尝辄止,这个词,来形容过去的学习生活,可以说是很形象很贴切了。

最近也是在忙春招,也是会有点紧张和压抑,原因,浅尝辄止

反正,加油啦。自我安慰(学东西,慢一点没关系啦)。

也是自己第一篇源码相关的总结,学习到不少,这篇主要还是给源码根据自己的理解添加中文注释,可能由很多地方有误。

Vue - github

柒陌 - github

segmentfault