一只很酷的蘑菇

vue源码学习笔记--Vue数据响应式原理

阅读量:

学习代码仓库

今天来看Vue的数据响应式原理,也就是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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// demo/index.js
function Vue3(options) {
this._init(options)
}
// 每次执行new Vue()操作都会执行的构造函数
Vue3.prototype._init = function(options) {
const vm = this
vm.$options = options || {}
vm._watchers = []
if (vm.$options.data) initData(vm)
}
function initData(vm) {
let data = vm.$options.data
// 将配置项data的值挂载在vm._data上,如果data是个函数,则调用`getData`方法获取返回值
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}

// ...省略data的校验步骤
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
// ...省略data的key值校验步骤
// 遍历 data 的 key,把 data 上的属性代理到 vm 实例上
proxy(vm, '_data', key)
}
// observe(data, true /* asRootData */)
}

function getData(data, vm) {
try {
return data.call(vm, vm)
} catch (e) {
console.error(e)
return {}
}
}

function noop() {}

function proxy(target, sourceKey, key) {
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
// 获取 vm[key]值的时候,返回vm._data[key]
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
// 设置 vm[key]值的时候,vm._data[key] = val
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
// 将[key]挂载在vm上
Object.defineProperty(target, key, sharedPropertyDefinition)
}

window.Vue3 = Vue3

通过以上的初始化,成功让配置项里data的每个值都挂到了vm实例上。并且vm[key]的操作会同步到vm._data[key]

1
2
3
4
5
6
7
8
var a = new Vue3({
data(){
return {
name: 'cky',
age: 18
}
}
})


Observer、Watcher、Dep

接下来要说的三个类ObserverWatcherDep,我们先提前搞清楚他们三个的作用。
Observer类是用来把一个值变得可观测的工具,它会循环传入的值,然后改写它们的getter/setter。它的实例方法有observeArraywalk,分别把数组和对象类型变得可观测。

Watcher可以设定多个观察对象,然后对于它们的改变做出相应行为。它的实例方法有以下:

  • get()。根据传入的表达式或函数,计算出值
  • addDep(dep)。如果没有将自身传入过该dep记录依赖,则将自身传入,调用dep.addSub(this)
  • cleanupDeps()。清理不再依赖的dep, 同时在dep的依赖列表中移除自身
  • update()。订阅者接口,当订阅的值改变时会触发
  • run()。对比前后值,如果前后值发生了改变,或者deeptrue,或者该值为对象,则触发cb回调。
  • evaluate()。触发this.get() 只会在lazy watcher里被触发
  • depend()。循环触发this.deps的项的depend()方法
  • teardown()。销毁watcher,从所有的this.deps里移除自身,并且将active设为false

Dep类是针对某个值的依赖收集器,比如id为1的Dep实例对象是负责收集_data.name的依赖的,那以后所有对这个值的进行观测的watcher都会被统计到这个实例里。
它的实例方法有如下:

  • addSub(sub: Watcher)。 增加依赖
  • removeSub (sub: Watcher)。 移除依赖
  • depend()。如果有Watcher在Dep.target, 则将这个Watcher加入依赖
  • notify()。调用所有的subsupdate()方法。

上面在initData()函数中注释了一句

1
observe(data, true /* asRootData */)

现在来看看这个函数是干什么的。

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
// /demo/observer.js
import { isObject } from './util'

export function observe(value, asRootData) {
// 如果不是对象,直接返回
if (!isObject(value)) {
return
}
let ob
// 如果value对象上有__ob__属性,而且这个属性是Observer类的一个实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
// 否则ob为一个新的Oserver实例
ob = new Observer(value)
}
// 如果是vm.$data, 那么 ob.vmCount++
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}

export function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

总结一下上面干了什么,observe函数返回的是一个Observer类的实例,如果传入的value有__ob__属性,直接返回,如果没有 则传入value值去构造一个Observer的实例并返回。

我们再来看看Observer类的定义

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
export class Observer {
// value
// dep
// vmCount // number of vms that has this object as root $data
constructor(value) {
this.value = value
this.vmCount = 0
// 给value添加'__ob__'属性,就是这个实例
def(value, '__ob__', this)
this.walk(value)
}
/**
* 循环每个属性,并转换它们的getter/setters,这个方法只能用于Object类型的值
*/
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 第一次初始化时, obj: vm._data keys[i]: key, obj[keys[i]]: value
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
/**
* 在一个对象上定义一个响应式的属性
*/
export function defineReactive(obj, key, val, customSetter, shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

const getter = property && property.get
const setter = property && property.set

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 如果之前这个字段已经定义过getter了就用之前的getter
const value = getter ? getter.call(obj) : val
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
// 如果值没有改变 或者类似 NaN !== NaN这种情况
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
console.log(`我被改变了,新值:${newVal}, 旧值:${value}`)
}
})
}

总结一下上面的操作,Observer的构造函数给传入的value值增加__ob__属性,也就是这个构造出的实例,并且如果value是对象类型,会循环它的每个属性,调用defineReactive方法,而这个方法就是改写每个属性的getter/setter,从而可以在进行值获取和赋值的时候进行某些操作,也就能监听到值的改变。

看看执行上面代码的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = new Vue3({
data(){
return {
name: 'cky',
age: 18,
mom: {
name: 'zj',
age: 28
},
friends: ['aa', 'bbb', 'ccc']
}
}
})



之前对于vm.agevm.name也有gettersetter的重写,和这里的vm._data.agegetter/setter是不一样的,在vm.agegetter是去读vm._data.age,从而触发vm._data.agegettersetter同理。其实真正对于数据改变的监听是在_data属性上的gettersetter上完成的。

数组更新检测

上面的操作在数组执行push这类的会改变数组的方法的时候,却没有任何作用,因为这个并不会触发setter。那对于数组Vue又是怎么处理的呢。

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
// /demo/observer.js
import { arrayMethods } from './array'
export class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
this.walk(value)
}
}
// walk()...
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

// /demo/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
console.log(`我是数组,被${method}方法改变了`)
return result
})
})

这个array.js文件export出了一个 arrayMethodsarrayMethods 继承了 Array.prototype,并在自身定义了那些变异方法来拦截原始数组的那些方法调用。
我们知道,当我们访问对象上的一个属性的时候,假如对象自身不存在这个属性,则会延续到它的 __proto__ 上去找,找不到就继续。所以上面只需要把数组的 __proto__ 指向 vue 自己的 ArrayMethods 就实现了拦截部分属性并继承原始 Array 的其他原型方法,十分巧妙。

官方文档说,不支持直接对数组this.xx[n] = xyz这样的赋值监听,提供了Vue.setthis.$set 方法,其实这个方法内部在前一篇也讲了,就是调用了splice这个变异方法从而实现监听。

依赖收集


上面的操作还是仅仅是观察者能够监听到了数组的变化,观察者看到发生变化后,就要去通知那些订阅者(watcher)。那这个订阅依赖是怎么统计起来的呢。首先,我们需要定义一个Dep类,这是观察者和订阅者的桥梁,它统计了所有的watcher,然后统一发出通知。

1
2
3
4
5
6
7
8
9
10
11
// /demo/dep.js
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub () {...} // 添加订阅者(依赖)
removeSub () {...} // 删除订阅者(依赖)
depend () {...} // 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
notify () {...} // 通知订阅者(依赖)更新
}

而什么时候该去添加依赖呢,其实就是你在获取这个值,而且说明自己是个订阅者的时候,就可以把你作为这个值的依赖了。
这一步就在defineReactive方法里实现,这一步重写了gettersetter,所以只需要在getter里记录依赖,在setter里通知改变就行了。

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
// /demo/observer.js
export function defineReactive(obj, key, val, customSetter, shallow) {
const dep = new Dep() // 该值的依赖收集器
//...
let childOb = !shallow && observe(val) // 返回的是一个Observer实例

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 当订阅者存在的时候,才进行依赖收集
dep.depend() // 依赖收集,
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
// ...
dep.notify()
}
})
}

Vue的订阅者实现是一个Watcher类,在Vue的生命周期里,有四个地方会实例化这个类。

  • Vue实例化的过程中有watch选项
  • Vue实例化的过程中有computed计算属性选项
  • Vue原型上有挂载$watch方法: Vue.prototype.$watch,可以直接通过实例调用this.$watch方法
  • Vue生成了render函数,更新视图时
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
// demo/watcher.js
export default class Watcher {
constructor (vm, expOrFn, cb, options) {
// 缓存这个实例vm
this.vm = vm
// vm实例中的_watchers中添加这个watcher
vm._watchers.push(this)
// options
if (options) {
this.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
....
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// eg: 'a.b.c',parsePath方法返回了一个函数,接收obj参数,然后返回obj[a][b][c]的值
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
// 通过get方法去获取最新的值
// 如果lazy为true, 初始化的时候为undefined
this.value = this.lazy
? undefined
: this.get()
}
get () {...}
addDep () {...}
update () {...}
run () {...}
evaluate () {...}
run () {...}
}

在除了computed选项外,其他几种实例化watcher的方式都是在实例化过程中完成求值及依赖的收集工作:this.value = this.lazy ? undefined : this.get().在Watcherget方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get () {
// pushTarget即设置当前的需要被执行的watcher
pushTarget(this)
let value
const vm = this.vm
try {
// $watch(function () {})
// 调用this.getter的时候,触发了属性的getter函数
// 在getter中进行了依赖的管理
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
// traverse方法其实就是遍历读取了value的值,从而遍历触发了下面的`getter`从而进行了依赖收集
if (this.deep) {
traverse(value)
}
// 完成了依赖收集
popTarget()
// 清理和删除老旧依赖
this.cleanupDeps()
return value
}

get()方法中触发了getter,调用dep.depend()

1
2
3
4
5
6
7
8
// demo/dep.js
depend () {
// 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
if (Dep.target) {
// Dep.target为一个watcher
Dep.target.addDep(this)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// demo/watcher.js
// 添加依赖
addDep(dep) {
// 某值的依赖收集器实例,如果这个依赖没有被收集过
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

其他的细节代码就不细说了,现在完善代码, 并执行以下代码后

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
var a = new Vue3({
data(){
return {
name: 'cky',
age: 18,
mom: {
name: 'zj',
age: 28
},
friends: ['aa', 'bbb', 'ccc'],
classmate: ['dd', 'ee', 'gg']
}
}
})
a.$watch('name', function(newValue, oldValue) {
console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
console.log(newValue, oldValue)
})
a.$watch('name', function(newValue, oldValue) {
console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
console.log(newValue, oldValue)
})
a.$watch(function(){
return this.name + this.age
}, function(newValue, oldValue) {
console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
console.log(newValue, oldValue)
})




其中Dep.id为1的是对name值的依赖收集器,它下面有id为1、4、6的watcher
Dep.id为2的是对age的依赖收集器,它的收集的订阅者有id为2、3、5、6、7的watcher
id为6的watcher因为用到了nameage2个值,所以上面都有他,而它的deps字段也能看出来。

参考

Vue的数据依赖实现原理简析

陈柯伊

梦想一克拉 热血一卡车 眼泪一酒瓶 熬夜一光年

评论

文章目录