今天来看Vue的数据响应式原理,也就是watch一个值,改变了这个数据,能够得到通知并且在回调里获得新值和旧值从而进行操作,这一个功能的实现。本篇暂不包含与模板层的双向绑定。
# 数据初始化
// 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
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
通过以上的初始化,成功让配置项里data
的每个值都挂到了vm
实例上。并且vm[key]
的操作会同步到vm._data[key]
上
var a = new Vue3({
data(){
return {
name: 'cky',
age: 18
}
}
})
2
3
4
5
6
7
8
# Observer、Watcher、Dep
接下来要说的三个类Observer
、Watcher
、Dep
,我们先提前搞清楚他们三个的作用。
Observer
类是用来把一个值变得可观测的工具,它会循环传入的值,然后改写它们的getter/setter
。它的实例方法有observeArray
和walk
,分别把数组和对象类型变得可观测。
Watcher
可以设定多个观察对象,然后对于它们的改变做出相应行为。它的实例方法有以下:
get()
。根据传入的表达式或函数,计算出值addDep(dep)
。如果没有将自身传入过该dep
记录依赖,则将自身传入,调用dep.addSub(this)
cleanupDeps()
。清理不再依赖的dep
, 同时在dep
的依赖列表中移除自身update()
。订阅者接口,当订阅的值改变时会触发run()
。对比前后值,如果前后值发生了改变,或者deep
为true
,或者该值为对象,则触发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()
。调用所有的subs
的update()
方法。
上面在initData()
函数中注释了一句
observe(data, true /* asRootData */)
现在来看看这个函数是干什么的。
// /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
})
}
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
总结一下上面干了什么,observe
函数返回的是一个Observer
类的实例,如果传入的value有__ob__
属性,直接返回,如果没有 则传入value
值去构造一个Observer
的实例并返回。
我们再来看看Observer
类的定义
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}`)
}
})
}
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
总结一下上面的操作,Observer
的构造函数给传入的value
值增加__ob__
属性,也就是这个构造出的实例,并且如果value
是对象类型,会循环它的每个属性,调用defineReactive
方法,而这个方法就是改写每个属性的getter/setter
,从而可以在进行值获取和赋值的时候进行某些操作,也就能监听到值的改变。
看看执行上面代码的结果:
var a = new Vue3({
data(){
return {
name: 'cky',
age: 18,
mom: {
name: 'zj',
age: 28
},
friends: ['aa', 'bbb', 'ccc']
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
之前对于vm.age
和vm.name
也有getter
和setter
的重写,和这里的vm._data.age
的getter/setter
是不一样的,在vm.age
的getter
是去读vm._data.age
,从而触发vm._data.age
的getter
,setter
同理。其实真正对于数据改变的监听是在_data
属性上的getter
和setter
上完成的。
# 数组更新检测
上面的操作在数组执行push
这类的会改变数组的方法的时候,却没有任何作用,因为这个并不会触发setter
。那对于数组Vue
又是怎么处理的呢。
// /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
})
})
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
这个array.js
文件export
出了一个 arrayMethods
,arrayMethods
继承了 Array.prototype
,并在自身定义了那些变异方法来拦截原始数组的那些方法调用。
我们知道,当我们访问对象上的一个属性的时候,假如对象自身不存在这个属性,则会延续到它的 __proto__
上去找,找不到就继续。所以上面只需要把数组的 __proto__
指向 vue 自己的 ArrayMethods
就实现了拦截部分属性并继承原始 Array
的其他原型方法,十分巧妙。
官方文档说,不支持直接对数组this.xx[n] = xyz
这样的赋值监听,提供了Vue.set
和this.$set
方法,其实这个方法内部在前一篇也讲了,就是调用了splice
这个变异方法从而实现监听。
# 依赖收集
上面的操作还是仅仅是观察者能够监听到了数组的变化,观察者看到发生变化后,就要去通知那些订阅者(watcher
)。那这个订阅依赖是怎么统计起来的呢。首先,我们需要定义一个Dep
类,这是观察者和订阅者的桥梁,它统计了所有的watcher,然后统一发出通知。
// /demo/dep.js
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub () {...} // 添加订阅者(依赖)
removeSub () {...} // 删除订阅者(依赖)
depend () {...} // 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
notify () {...} // 通知订阅者(依赖)更新
}
2
3
4
5
6
7
8
9
10
11
而什么时候该去添加依赖呢,其实就是你在获取这个值,而且说明自己是个订阅者的时候,就可以把你作为这个值的依赖了。
这一步就在defineReactive
方法里实现,这一步重写了getter
和setter
,所以只需要在getter
里记录依赖,在setter
里通知改变就行了。
// /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()
}
})
}
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
Vue
的订阅者实现是一个Watcher
类,在Vue
的生命周期里,有四个地方会实例化这个类。
Vue
实例化的过程中有watch
选项Vue
实例化的过程中有computed
计算属性选项Vue
原型上有挂载$watch
方法:Vue.prototype.$watch
,可以直接通过实例调用this.$watch
方法Vue
生成了render
函数,更新视图时
// 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 () {...}
}
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
在除了computed
选项外,其他几种实例化watcher
的方式都是在实例化过程中完成求值及依赖的收集工作:this.value = this.lazy ? undefined : this.get()
.在Watcher
的get
方法中:
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在get()
方法中触发了getter
,调用dep.depend()
// demo/dep.js
depend () {
// 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
if (Dep.target) {
// Dep.target为一个watcher
Dep.target.addDep(this)
}
}
2
3
4
5
6
7
8
// 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)
}
}
}
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'],
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)
})
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
其中Dep.id
为1的是对name
值的依赖收集器,它下面有id为1、4、6的watcher
。
Dep.id
为2的是对age
的依赖收集器,它的收集的订阅者有id
为2、3、5、6、7的watcher
id
为6的watcher
因为用到了name
和age
2个值,所以上面都有他,而它的deps
字段也能看出来。