前言
- observer部分完整的源码注释放在github上了,有兴趣的可以去看看,如果发现有误情不吝赐教!observer
- 这篇文很长长长长长长长长,而且比较费脑,我也整理了很久,如果对这篇文感兴趣,请自带☕️和无限的耐心~
- 这篇文只是Vue的响应式原理的一部分,后面还有很多很多很多的知识本文没有涉及到。
Vue的双向数据绑定和Angular很不一样。
Angular采用的是“脏检查”的方法,当我们触发了某些事件(定时,异步请求,事件触发等),执行完事件之后,Angular会对所有“注册”过的值进行一遍“全面检查”,也就是遍历所有的值,判断是否和之前的一致。这种方法效率不高,因为我们修改一个小地方都会带来两次以上的全面检查,如果我们绑定的view比较多,就可能会存在比较明显的性能问题了。
而Vue的处理方式则不同,它结合观察者模式和发布-订阅模式,当我们改变了一个数值,它会主动通知与它相关的订阅者,告诉他们可以进行相关的操作了,这种方法和“脏检查”相比,更加优雅,效率会更高。
1. 响应式的基石:Object.defineProperty(obj, prop, descriptor)
MDN : Object.defineProperty(obj, prop, descriptor)
我们都知道对象有两种属性,一种是数据属性,一种是访问器属性。
数据属性有4个特性:configurable, enumerable, value, writable
访问器属性也有4个特性:configurable, enumerable, get, set
平时我们通过普通赋值的方法(比如:obj.a - 'a'
)添加的属性都是数据属性,且默认configurable
,和enumerable
都为true
。
而用Object.defineProperty()
可以添加数据属性或者访问器属性,默认configurable
,enumerable
都为false
。
关于getter/setter
访问器属性是没有value
的,但是他们可以用来劫持对另一个数据的访问,举个例子:
var log = console.log.bind(console);
var obj = {
_year: 2017
};
Object.defineProperty(obj, 'year', {
get: function getter () {
return this._year;
},
set: function setter (value) {
this._year = value;
}
});
log(obj.year); // 2017
obj.year = 2018;
log(obj._year); // 2018
这个例子中我们访问obj.year
,会返回obj._year
的值,我们修改obj.year
,会修改obj._year
的值。
根据这个特性,我们可以实现视图-数据双向绑定:
<body>
<p id="test-p">lalal</p>
</body>
<script>
var log = console.log.bind(console);
var obj = {}
Object.defineProperty(obj, 'test-p', {
get: function getter () {
return document.getElementById('test-p').innerHTML;
},
set: function setter (value) {
document.getElementById('test-p').innerHTML = value;
}
});
log(obj['test-p']);
setTimeout(function changeData () {
obj['test-p'] = 'hahah';
}, 3000);
</script>
是不是特别好玩?
2. 观察者模式
在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。
一图胜千言,我画了张简单的流程图,应该很容易看懂:
详细一点讲,流程大概是这样的:
目标对象有这么几个方法:
- setState:设置对象的状态,该函数调用了NotifyObserver方法
- getState:取得对象当前的状态
- addObserver:添加观察者
- removeObserver:删除观察者
- NotifyObserver:通知观察者:我的状态改变了,该方法会调用各个观察者的Notify方法
观察者对象有个方法:
Notify:该方法会会调用目标对象的getState方法,然后对目标对象的新值作出一些反应,比如说,打印出来之类的。
如果我们写一个最简单的观察者模式,那可能是这样的:
var log = console.log.bind(console);
function Oberser (target, cb) {
(function(){ // 添加到目标对象
target.addOberser && target.addOberser(this);
console.log('oberser added')
}).call(this);
this.notify = cb;
}
var target = {
_value: 2017,
obersers: [],
addOberser: function (oberser) { // 添加观察者
this.obersers.push(oberser);
},
removeOberser: function (oberser) { // 删除观察者
// ...
},
notifyOberser: function () { // 通知观察者
this.obersers.map(oberser => oberser.notify && oberser.notify());
}
}
Object.defineProperties(target, {
value: {
get: function () {
return this._value;
},
set: function (newValue) {
this._value = newValue;
this.notifyOberser(); // 调用notifyOberser
}
}
});
var oberser1 = new Oberser(target, function () {
log(`I'm observer1, the value of my target is ${target.value}`);
});
var oberser2 = new Oberser(target, function () {
log(`I'm observer2, the value of my target is ${target.value}`);
});
target.value = 2018;
/*
oberser added
oberser added
I'm observer1, the value of my target is 2018
I'm observer2, the value of my target is 2018
*/
当然了如果我们要观察同一个对象中的多个属性,就不能用这种方法了,因为我们总不能一个属性更新,所有观察者都全部调用一遍吧?最好是每一个属性都能有自己的观察者。
3. 正题
怎么给每个对象都维护一个观察者的列表呢?Vue是这样做的:
Vue在观察者模式中结合发布-订阅模式,其中涉及到了三个重要的对象:Observer
, Dep
, Watcher
。
Observer
:负责观察目标数据的变化,如果数据变化了,那么通知Dep。
Dep
:负责维护一个订阅者列表(收集依赖),当接收到Observer
的通知时,他就通知所有订阅者:目标数据更新了。
Watcher
:维护一个回调函数,当接收到Dep
的通知时,执行回调函数。
可以这么理解:Observer
是教师,Dep
是教学在线,Watcher
是学生。教师不必维护自己的学生列表,教务处帮他维护。学生不必维护自己的课表,因为教务处也会帮他维护。每次教师布置了新作业等(比喻不是很恰当),他只需要跟教学在线说一声就可以了,教学在线就发邮件告诉每一个上了这门课的学生:有新作业了。学生就可以分别对这个新作业作出不同的反应。
原理已经了解得差不多了,接下来看一下源码吧。
先看一下 Observer
类,Vue会给每一个响应式的数据添加一个observer,这个observer就负责观察这个数据有没有发生变化。
中文注释是我加的,英文注释是作者加的,不要漏了英文注释,很重要!
export class Observer {
value: any; // 被观察的对象,比如vue的根属性data,在vue实例初始化的时候,vue会为data属性添加一个observer对象,介时observer对象的value属性指向data,而data的__ob__属性指向observer对象
dep: Dep; // 每一个observer对象都有一个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) // value的__ob__属性指向这个observer对象本身,比如L38注释中说到的data属性
if (Array.isArray(value)) { // 如果value为数组,那么增强这个数组
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 往下递归数组,如果数组中有元素为对象或者数组,也会给其添加observer
} else { // 如果value为对象,那么往下递归对象,如果对象中有属性为对象或者数组,也会给其添加observer
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) { // 遍历对象,把对象中的属性都转化为getter/setter对
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 很重要!
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) { // 遍历数组,如果数组中有元素为对象或者数组,也会给其添加observer
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
在Observer
类的constructor
函数中,可以看到对于数组和对象,Vue的处理是不一样的,为什么呢?
数组是没有Object.defineProperty(obj, prop, descriptor)
这个方法的,这就意味着我们没有办法监听数组中属性的添加,删除。
如果你在Vue中处理过数组,你应该知道,在Vue中,数组只有7个常用方法可以触发视图的更新:push(), pop(), shift(), unshift(), splice(), sort(), reverse()
。这是因为Vue对这些方法进行了增强,原理很简单,类似于这样(当然实际上要严谨一些,这里只是帮助理解):
var log = console.log.bind(console);
var arrayMethods = Object.create(Array.prototype); // 继承自Array.prototype,保留了数组原本的特性
arrayMethods.unshift = function (value) { // 重写方法
Array.prototype.unshift.call(this, value); // 调用原来的方法
notify(); // 并进行通知
}
function notify () {
console.log('unshift');
}
var arr = [1, 2, 3];
arr.__proto__ = arrayMethods;
arr.unshift(0);
log(arr)
/*
[ 1, 2, 3 ]
unshift
[ 0, 1, 2, 3 ]
*/
上面的代码截断了数组的原型链,我们新创建了一个对象arrayMethods
,这个对象继承自Array.prototype
,然后改写里面的unshift()
方法。这样既保证我们保留了数组的length
等属性,有可能使用自己定义的unshift()
方法,我们在unshift()
方法中调用了notify()
函数。
Vue是怎么给每一个对象都加上一个Observer对象的?上面代码中,在处理数组的函数observeArray
里,可以看到Vue遍历了一遍数组,并对每一个元素调用了observe()
函数。
而在处理对象的函数walk
中,对每一个属性都调用了defineReactive
函数(这个函数非常重要,后面再说),这个defineReactive
函数内部也对属性都调用了一遍observe()
函数。
也就是说,Vue是通过observe()
函数来给对象添加observer的。看一下observe()
函数:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 只有对象或数组才会进入这个函数
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // value已经有了自己的observer
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // value为对象且没有自己的observer,那么为他新建一个observer,注意这里说明了Vue对每一层的属性或元素递归添加了observer
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
注意到里面有一行代码:ob = new Observer(value)
,也就是说,Vue递归遍历了每一层的属性或元素,如果这个元素/属性的类型为对象/数组,那么它也会有一个自己的observer。
好,现在我们已经明白了Vue怎么遍历数组来把数组转化为响应式的了,那接下来再看看Vue如何处理对象属性:
高能预警!
export function defineReactive ( // 每个属性都转化为getter/setter,并且每个类型为对象(包括数组)的属性都会拥有自己的observer
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean // shallow为true的话,属性不会有自己的observer,也就是该属性将不具备响应性
) {
const dep = new Dep() // 注意这个函数将会出现两个dep,这里第一个dep,将会被闭包进getter/setter函数中
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key] // 注意,这个val也会被闭包进getter/setter方法中,我之前还疑惑把属性都转化为getter/setter值是怎么存储数据的,就是把这个val闭包进去的
}
let childOb = !shallow && observe(val) // 每一个observer会有一个dep属性,所以这里有了第二个dep,这个dep会在该属性的属性被增删的时候通知订阅者
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 执行属性原本自己有的getter
if (Dep.target) { // 如果存在 Dep.target 这个全局变量不为空,表示是在新建 Watcher 的时候调用的
dep.depend() // 这里是第一个dep,当Dep.target依赖于这个属性的时候,他会调用该属性的getter,这是dep.depend()就会把Dep.target添加进自己的订阅列表,这样在属性的setter被调用的时候,这个dep就可以通知Dep.target了
if (childOb) {
childOb.dep.depend() // 第二个dep也会收集依赖,那么该属性的属性被添加或者删除的时候,这个dep就可以通知这个属性的订阅者了
if (Array.isArray(value)) { // 如果value为一个数组,那么是无法通过getters来窃听对数组元素的访问的,所以要向下遍历数组,给里面的元素都收集依赖
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)) { // 没有变化/newVal为NaN/value为NaN
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)
dep.notify()
}
})
}
这个函数逻辑比较复杂,让我们好好来捋一下思路:
1. 闭包的妙用
如果你好好观察一下属性的getter/setter方法,你会发现他们闭包了这几个变量:
getter, setter, val, dep,childOb
其中,getter, setter
两个变量可能是我们自己定义的getter/setter方法,因为我们有时候也会有需要访问器属性的时候。
val
是我们原本使用自己的getter/setter想要访问的值,比如这篇文章第一个代码块的_year
属性。
我之前还在疑惑,因为访问器属性是没有自己的值的,Vue把对象的属性转化为访问器属性之后,要怎么维护之前的值,原来是闭包进来了!
看一下源码,当我们调用属性的setter方法的时候,最后修改的是这个val
的值,而我们调用getter方法的时候,返回的也是这个val
的值。
dep, childOb
在第3小节一起讲。
2.Dep
类
是时候介绍一下Dep
类了,不然后面的讲不下去。
Dep
的结构很简单,大概长这样:
export default class Dep { // dep是dependence的缩写,他负责收集依赖,以及通知订阅者。每一个Observer对象有其自己的的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 () { // 添加依赖,也就是把当前Dep.target添加到这个dep实例的subs列表中
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () { // 通知watcher,执行所有watcher的.update()方法,更新watcher的数据
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
可以看到,一个Dep
类会有一个自己的id,维护自己的一个订阅者列表,并切可以添加,删除,通知订阅者。
Dep
类中有一个静态属性Dep.target
,学过C++的同学应该知道,静态属性也就是类属性,是所有实例共享的。这个target是干什么用的呢?
Vue在处理一个watcher的时候,就会把Dep.target
的值设为当前的watcher,举个例子,这是我们的Vue实例(该例子从官网中复制过来的):
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
现在假设处理到了reversedMessage
,先把Dep.target
指向它。很明显reversedMessage
依赖了message
,我们是不是要在message
对应的dep的订阅者列表中加上reversedMessage
?问题来了,我们要怎么添加这个依赖?
首先因为reversedMessage
会访问到message
,也就是会调用message
的getter方法,那我们可以在getter方法中进行依赖收集,但是getter方法是没办法传参的,所以它也没办法知道谁订阅了它。
这时候Dep.target
就起作用了,我们前面已经说过,Vue处理到了哪个watcher,就会把Dep.target
指向它,那么此时的Dep.target
肯定就是reversedMessage
,我们只需要在getter函数中把Dep.target
添加进订阅者列表就可以了!
那么这时,当我们改变message
的值时,会调用其setter函数,setter函数中dep就会调用dep.notify()方法,通知reversedMessage
:我更新了!
真的太妙了!
3. 两个dep
源码中我在注释中也提醒过了,这个函数中出现了两个dep,一个是在函数开头就新建的dep,另一个是属性自己的observer中的dep。
dep的作用是什么?收集依赖并在适当的时候通知订阅者:目标数据更新了。
在源码中,属性的getter方法中,给dep, childOb
都添加了依赖,为什么在setter方法中,只通知了dep
?或者说,childOb
的意义在哪里呢?
先看一个例子:
var obj = {
_a: { aa: 1}
};
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get: function () {
log('get a:' + this._a);
return this._a;
},
set: function (newV) {
log('set a:' + newV);
this._a = newV;
}
});
obj.a; // get a:[object Object]
obj.a.bb = 2; // get a:[object Object]
delete obj.a;
// 删除该属性的时候,没有调用getter/setter函数!
可以很明显看出getter/setter的缺陷:只能监听到属性的更改,不能监听到属性的删除与添加。
我们都知道Vue提供了内置的Vue.set()
, Vue.delete()
方法来让我们响应式的添加和删除数组的元素或对象的属性。
官方文档
官方文档是这么说的:
Vue.set()这个方法主要用于避开 Vue 不能检测属性被添加的限制。
Vue.delete()这个方法主要用于避开 Vue 不能检测到属性被删除的限制。
我们前面已经证明了,setter是不会在属性被删除或者添加的时候调用的,那么Vue是怎么在删除和添加的时候通知watcher的?其实跟数组方法的增强事同一个套路,把Vue.delete()
的源码简化简化再简化之后:
function del () {
delete obj.a;
childOb.notify(); // 通知obj的watcher:我有一个属性删除了。
}
所以为什么在getter方法中要添加childOb
的依赖,就是为了在删除或者添加属性的时候进行通知。
4. 如何向下收集依赖
是这样的,假设数据是这样的let data = {a: {b: {c: {d: {e: 1}}}}}
,有一个模板引用了{{a.b.c}}
,那么我们修改a.b.c.d.e
,这时watcher会被通知到吗?
答案是不会。为什么?一步一步来看。
首先我们知道每一层的属性,也就是
a, b, c, d, e
,都有自己的observer,而且watcher订阅observer是通过getter方法来实现的,没有getter方法就没法订阅。它调用了
c
的getter方法,因此c
更改了(整个对象被替换),会有通知,c
删除了,也会有通知。我们修改了
a.b.c.d.e
模板引用的是
{{a.b.c}}
,那么它没有调用到e
的getter方法。因此我们修改了
e
,watcher就没办法知道了。
Vue是怎么解决这个问题的呢?
它是这样做的:当模板引用了{{a.b.c}}
时,此时Dep.target
是这个模板,然后,Vue从c
开始往下遍历,对每个属性都"touch"一下,也就是强行调用一下getter方法,这样,模板就加入了所有属性的订阅者列表中。
有兴趣的同学可以自己去看一下Vue源码中的traverse.js
5. dependArray函数
终于要进入尾声了🙂️,看源码真的很费心神,但是收获真的超级大呀!
在defineReactive
函数中的getter方法中,对数组有一个额外的处理过程:如果value
为数组,那么对其执行dependArray
函数。
想了好久才想明白为什么要进一步的处理。
回到最开始,我们给一个对象添加一个observer,那么他会遍历所有的属性,把属性都转化为getter / setter。
但是给数组添加一个observer,他只是添加了8个具有响应性的方法。(当然他也会给子对象添加observer)
这时我们push,pop数组,是响应式的,数组的dep知道他要通知订阅者们。
但是如果我们改变的是数组的元素,比如,对于一个数组var arr = [1, 2, 3, {a: 4}],现在我们这样操作arr[0] = 0
,数组是不会有响应的。
这也是为什么vue给数组加了两个方法Vue.set
, Vue.delete
来添加和删除元素的原因。
再回到这个问题上,我们已经有了Vue.set
, Vue.delete
两个方法,那我们操作基本类型的元素基本没啥问题了,但是如果是像arr[3].a = 5
这种呢?Vue的解决方法就是递归遍历数组,遇到类型为object的元素,就把当前的Dep.target添加到它的订阅者列表中,这时它的变化就可以被监听了。
这一切的根本原因,就是数组没法通过getter/setter对象来监听元素的变化。
最后附上dependArray
的源码。
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend() // //如果数组元素也是对象,那么他们observe过程也生成了ob实例,那么就让ob的dep也收集依赖
if (Array.isArray(e)) {
dependArray(e)
}
}
}