vue observer 源码学习

一、版本:2.5.9

二、建议

      vue最重要的应该就是响应式更新了,刚开始接触vue或多或少都能从官方文档或者其他地方知道vue响应式更新依赖于Object.defineProperty()方法,这个方法在MDN上有详细讲解,不过,如果是初学者的话,直接去看响应式更新源码还有点难度的,最好是先用项目练一遍,对vue有个相对熟悉的了解,然后可以去各大热门讲解的博客上看看人家的讲解,这样汇总一番有点底子了再去看源码实现相对轻松点。
最低级别的监听可以看我这个库:https://github.com/lizhongzhen11/obj
参考:https://segmentfault.com/a/1190000009054946
https://segmentfault.com/a/1190000004384515

三、迎接挑战吧—鬼气·九刀流·阿修罗

      从github上把vueclone下来,或者直接在github上看也行。
      别的先不管,直接去src/core/observer文件夹,这个明显就是vue响应式更新源码精华所在,内部共有array.js,dep.js,index.js,scheduler.js,traverse.js,watcher.js6个文件,先看哪一个呢?第一次看没有头绪的话就先看index.js
      index.js开头import了不少文件,先不用管,往下看需要用到时再去查找不迟。而第一步就用到了arrayMethods,该对象来自array.js,下面同时列出array.js中的相关代码:

1
2
3
4
5
6
7
// index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

      如上所示,arrayMethods其实是一个Array.prototype的实例,只不过中间经过arrayProto过渡,一开始我还在纠结下方的代码(对数组push等方法遍历添加到刚刚创建的实例arrayMethods中,这里没有列出来),因为没看到下方代码有export,感觉很奇怪,而且他代码是下面这样的,[]前有个;,感觉很奇怪,vue作者是不写;的,这里出现一个;感觉很突兀。PS:后来问了前辈,前辈解释说:在js文件合并的时候,防止前一个js文件没有;结尾导致的错误

1
;['push','pop','shift','unshift','splice','sort','reverse']

      接下来,go on!定义了一个“观察状态”变量,内部有一个是否可以覆盖的布尔属性。注释里面说不想强制覆盖冻结数据结构下的嵌套值,以避免优化失败

1
2
3
export const observerState = {
shouldConvert: true
}

      继续往下看,来到了重头戏:Observer类,注释中也说的明白:该类属于每个被观察的对象,observer在目标对象的属性的getter/setters覆盖键同时搜集依赖以及分发更新。

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
import Dep from './dep'
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

      构造函数里面第二步this.dep = new Dep(),这个Dep来自dep.js,这时候,得需要去看看dep.js里面相关的代码了:

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
let uid = 0
/**
* 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 = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 更新 Watcher 数组中的数据
}
}
}

      Dep内部用到了Watcher,而Watcher又来自watcher.js。先说Dep,内部主要对Watcher类型的数组进行增加删除以及更新维护,自己内部没有什么太多复杂的逻辑,主要还是在watcher.js中。接下来列出watcher.js相关代码:

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
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
// 先看构造函数,内部变量不列出来了,太多了
constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this // 直接在vue 页面里打印 this 可以找到_watcher属性
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep // 这里可能是怕万一 options 对象里没有 deep 等属性,所以用了 !! 来强转成布尔型
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set() // es6语法,类似java Set集合,不会添加重复数据
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy ? undefined : this.get()
}

      上面构造函数第一个参数vm是什么?如果一直用vue-cli构建工具开发的话,可能没怎么注意过,其实vm就是vue的一个实例!!!第二个参数expOrFn暂时还不清楚,如果是函数的话直接赋给this.getter,否则this.getter直接指向一个空函数,同时还发出警报,需要传递一个函数。最后,判断this.lazy,为true的话调用this.get()方法:

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
import Dep, { pushTarget, popTarget } from './dep'
/**
* Evaluate the getter, and re-collect dependencies.
* 对 getter 求值,并重新收集依赖
*/
get () {
pushTarget(this) // 相当于 Dep.target = this,
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 求值
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps() // 清理deps,为了依赖收集
}
return value
}
// dep.js
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}

      get()中最终会判断cthis.deep是否为true,如果是调用traverse(value),而traverse()来自traverse.js,其目的是把dep.id加进去;popTarget()是为了将之前pushTarget(this)target移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear() // newDepIds 是Set类型,可以通过clear()清空
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}

      cleanupDeps()方法将旧的依赖编号与新的依赖集合编号进行对比,如果旧依赖数组中存在的编号,而新依赖集合编号中不存在,就需要删除对应编号的依赖;接下来交换新旧依赖集合编号,然后清空this.newDepIds(其实此时该集合内保存的是旧有的依赖集合编号);随后交换新旧依赖数组,然后来了一步骚操作:this.newDeps.length = 0,将this.newDeps清空,比较骚。

      也就是说,利用get()方法求值后会清理依赖收集。
      到了get()可以先暂停回顾一下。这里是在Watcher构造函数中调用的,也就是说,当new Watcher()时就会走遍上述代码,包括调用get()来取值。

这时候如果继续强行看完Watcher下面的源码,会发现没什么头绪,所以依然回到index.js中。继续研究Observer类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}

      构造函数中紧跟着调用了def(value, '__ob__', this),这个方法是干嘛的?在哪里?
      通过查找发现def方法位于util/lang.js内,下面贴出源码:

1
2
3
4
5
6
7
8
9
10
11
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

      def内部调用了Object.defineProperty(),结合Observer构造函数的传参,可知这里给每个对象定义了一个__ob__属性,在日常开发中,当我们打印输出时经常能看到__ob__
      接下来进一步判断value是不是数组,如果不是的话调用walk(),当然要确保参数是Object,然后遍历对象的key并且每个调用defineReactive(obj, keys[i], obj[keys[i]])

看看defineReactive()方法内部实现:

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
export function defineReactive (obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key) // 返回指定对象上一个自有属性对应的属性描述符。
if (property && property.configurable === false) { // 这一步其实是判断对象改属性能不能被修改,如果不能就返回
return
}
// cater for pre-defined getter/setters
const getter = property && property.get // 缓存对象属性内的get方法
const setter = property && property.set // 缓存对象属性内的set方法
let childOb = !shallow && observe(val) // observe(val)尝试返回一个 observer实例,如果 !shallow === true 那么 childOb === ob
// 其实也可以理解为, childOb === val.__ob__
Object.defineProperty(obj, key, { // 这里开始是真正的核心所在,其实就是重新对象的get、set方法,方便监听
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // getter 存在的话就调用原生的 get 方法取值,否则用传进来的值
if (Dep.target) {
dep.depend() // 增加依赖
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value) // 递归调用收集数组依赖
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) // childOb === newVal.__ob__
dep.notify() // 内部调用了 watcher.js 里面的 uodate(),内部又调用了 run(),run()里面设置值,其中还用到了watcher队列
}
})
}

      响应式更新的重中之重就是首先得监听到对象属性值的改变,vue通过defineReactive()内部重写传入的对象属性中的set以及get方法,其中,js原生的call()也有很大的功劳。

总结

      再一次看vue源码明显比第一次看好多了,但是不断地调用其它方法,理解上还是有一定的难度,这一次阅读源码更多的就是做个笔记,写得并不好,但是留个印象,方便下次再看。

Newer Post

js event loop 学习

一、参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoophttps://www.w3.org/TR/html5/webappapis.html#event-loopshttps://juejin.im/post/5a6 …

继续阅读
Older Post

spring boot 踩坑

入门学习自: http://www.ityouknow.com/springboot/2016/01/06/springboot(%E4%B8%80)-%E5%85%A5%E9%97%A8%E7%AF%87.html踩坑 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;照着链 …

继续阅读