变化侦测就是追踪状态,亦或者说是数据的变化,一旦发生了变化,就要去更新视图。
变化侦测可不是个新名词,它在目前的前端三大框架中均有涉及。在Angular 中是通过脏值检查流程来实现变化侦测;在React 是通过对比虚拟DOM 来实现变化侦测,而在Vue 中也有自己的一套变化侦测实现机制。
object变化侦测 1.让Object数据变得“可观测” 通过JS提供的Object.defineProperty方法
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 / 源码位置:src/core/observer/index.jsexport class Observer { constructor (value) { this .value = value def(value,'__ob__' ,this ) if (Array .isArray(value)) { } else { this .walk(value) } } walk (obj: Object ) { const keys = Object .keys(obj) for (let i = 0 ; i < keys.length; i++) { defineReactive(obj, keys[i]) } } }function defineReactive (obj,key,val ) { if (arguments .length === 2 ) { val = obj[key] } if (typeof val === 'object' ){ new Observer(val) } Object .defineProperty(obj, key, { enumerable: true , configurable: true , get (){ console .log(`${key} 属性被读取了` ); return val; }, set (newVal){ if (val === newVal){ return } console .log(`${key} 属性被修改了` ); val = newVal; } }) }
我们定义了observer
类,它用来将一个正常的object
转换成可观测的object
。
并且给value
新增一个__ob__
属性,值为该value
的Observer
实例。这个操作相当于为value
打上标记,表示它已经被转化成响应式了,避免重复操作
通过类型判断,如果是object类型,则会调用walk方法,通过循环遍历,调用defineReactive将每一个属性都转换成getter/setter的形式,如果子属性也是一个对象,则通过new Observer(val) 来递归子属性,这样object的所以属性(包括属性)都转化成getter和setting的形式来侦测变化,
1 2 3 4 let car = new Observer({ 'brand' :'BMW' , 'price' :3000 })
这样,car的两个属性就是可观测了。
2.依赖收集 在getter中收集依赖,在setter中通知依赖更新 。
通过实现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 export default class Dep { constructor () { this .subs = [] } addSub (sub) { this .subs.push(sub) } removeSub (sub) { remove(this .subs, sub) } depend () { if (window .target) { this .addSub(window .target) } } notify () { const subs = this .subs.slice() for (let i = 0 , l = subs.length; i < l; i++) { subs[i].update() } } }export function remove (arr, item ) { if (arr.length) { const index = arr.indexOf(item) if (index > -1 ) { return arr.splice(index, 1 ) } } }
在上面的依赖管理器Dep
类中,我们先初始化了一个subs
数组,用来存放依赖,并且定义了几个实例方法用来对依赖进行添加,删除,通知等操作。
有了依赖管理器后,我们就可以在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 function defineReactive (obj,key,val ) { if (arguments .length === 2 ) { val = obj[key] } if (typeof val === 'object' ){ new Observer(val) } const dep = new Dep() Object .defineProperty(obj, key, { enumerable: true , configurable: true , get (){ dep.depend() return val; }, set (newVal){ if (val === newVal){ return } val = newVal; dep.notify() } }) }
在getter
中调用了dep.depend()
方法收集依赖,在setter
中调用dep.notify()
方法通知所有依赖更新
3.依赖到底是谁 实现Watcher类(依赖)订阅者
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 export default class Watcher { constructor (vm,expOrFn,cb) { this .vm = vm; this .cb = cb; this .getter = parsePath(expOrFn) this .value = this .get() } get () { window .target = this ; const vm = this .vm let value = this .getter.call(vm, vm) window .target = undefined ; return value } update () { const oldValue = this .value this .value = this .get() this .cb.call(this .vm, this .value, oldValue) } }const bailRE = /[^\w.$]/ export function parsePath (path ) { if (bailRE.test(path)) { return } const segments = path.split('.' ) return function (obj ) { for (let i = 0 ; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher
实例,在创建Watcher
实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher
实例就代表这个依赖,当数据变化时,我们就通知Watcher
实例,由Watcher
实例再去通知真正的依赖。
那么,在创建Watcher
实例的过程中它是如何的把自己添加到这个数据对应的依赖管理器中呢?
下面我们分析Watcher
类的代码实现逻辑:
当实例化Watcher
类时,会先执行其构造函数;
在构造函数中调用了this.get()
实例方法;
在get()
方法中,首先通过window.target = this
把实例自身赋给了全局的一个唯一对象window.target
上,然后通过let value = this.getter.call(vm, vm)
获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter
,上文我们说过,在getter
里会调用dep.depend()
收集依赖,而在dep.depend()
中取到挂载window.target
上的值并将其存入依赖数组中,在get()
方法最后将window.target
释放掉。
而当数据变化时,会触发数据的setter
,在setter
中调用了dep.notify()
方法,在dep.notify()
方法中,遍历所有依赖(即watcher实例),执行依赖的update()
方法,也就是Watcher
类中的update()
实例方法,在update()
方法中调用数据变化的更新回调函数,从而更新视图。
简单总结一下就是:Watcher
先把自己设置到全局唯一的指定位置(window.target
),然后读取数据。因为读取了数据,所以会触发这个数据的getter
。接着,在getter
中就会从全局唯一的那个位置读取当前正在读取数据的Watcher
,并把这个watcher
收集到Dep
中去。收集好之后,当数据发生变化时,会向Dep
中的每个Watcher
发送通知。通过这样的方式,Watcher
可以主动去订阅任意一个数据的变化。
4.不足之处 向object
数据里添加一对新的key/value
或删除一对已有的key/value
时,它是无法观测到的,导致当我们对object
数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。
Vue增加了两个全局API:
Vue.set和
Vue.delete
5.总结 其整个流程大致如下:
Data
通过observer
转换成了getter/setter
的形式来追踪变化。
当外界通过Watcher
读取数据时,会触发getter
从而将Watcher
添加到依赖中。
当数据发生了变化时,会触发setter
,从而向Dep
中的依赖(即Watcher)发送通知。
Watcher
接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
Array的变化侦测 由于数组无法使用Object .defineProperty来实现数据响应式
Array型数据还是在getter中收集依赖。
1.实现数组方法拦截器 在Vue
中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype
之间,在拦截器内重写了操作数组的一些方法,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype
上的原生方法。
经过整理,Array
原型中可以改变数组自身内容的方法有7个,分别是:push
,pop
,shift
,unshift
,splice
,sort
,reverse
。那么源码中的拦截器代码如下:
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 const arrayProto = Array .prototypeexport const arrayMethods = Object .create(arrayProto)const methodsToPatch = [ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' ] methodsToPatch.forEach(function (method ) { const original = arrayProto[method] Object .defineProperty(arrayMethods, method, { enumerable: false , configurable: true , writable: true , value:function mutator (...args ) { const result = original.apply(this , args) return result } }) })
在上面的代码中,首先创建了继承自Array
原型的空对象arrayMethods
,接着在arrayMethods
上使用object.defineProperty
方法将那些可以改变数组自身的7个方法遍历逐个进行封装。最后,当我们使用push
方法的时候,其实用的是arrayMethods.push
,而arrayMethods.push
就是封装的新函数mutator
,也就后说,实标上执行的是函数mutator
,而mutator
函数内部执行了original
函数,这个original
函数就是Array.prototype
上对应的原生方法。 那么,接下来我们就可以在mutato
r函数中做一些其他的事,比如说发送变化通知
2.使用拦截器 把它挂载到数组实例与Array.prototype
之间,这样拦截器才能够生效。
其实挂载不难,我们只需把数据的__proto__
属性设置为拦截器arrayMethods
即可,源码实现如下:
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 export class Observer { constructor (value) { this .value = value if (Array .isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) } else { this .walk(value) } } }export const hasProto = '__proto__' in {}const arrayKeys = Object .getOwnPropertyNames(arrayMethods)function protoAugment (target, src: Object, keys: any ) { target.__proto__ = src }function copyAugment (target: Object, src: Object, keys: Array<string> ) { for (let i = 0 , l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
上面代码中首先判断了浏览器是否支持__proto__
,如果支持,则调用protoAugment
函数把value.__proto__ = arrayMethods
;如果不支持,则调用copyAugment
函数把拦截器中重写的7个方法循环加入到value
上。这时我们就可以在拦截器中监听到数据变化了。
3.再谈依赖收集 在Observer
类中实例化了一个依赖管理器,用来收集数组依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export class Observer { constructor (value) { this .value = value this .dep = new Dep() if (Array .isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) } else { this .walk(value) } } }
数组的依赖也在getter
中收集,那么在getter
中到底该如何收集呢?这里有一个需要注意的点,那就是依赖管理器定义在Observer
类中,而我们需要在getter
中收集依赖,也就是说我们必须在getter
中能够访问到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 function defineReactive (obj,key,val ) { let childOb = observe(val) Object .defineProperty(obj, key, { enumerable: true , configurable: true , get (){ if (childOb) { childOb.dep.depend() } return val; }, set (newVal){ if (val === newVal){ return } val = newVal; dep.notify() } }) }export function observe (value, asRootData ) { if (!isObject(value) || value instanceof VNode) { return } let ob if (hasOwn(value, '__ob__' ) && value.__ob__ instanceof Observer) { ob = value.__ob__ } else { ob = new Observer(value) } return ob }
在上面代码中,我们首先通过observe
函数为被获取的数据arr
尝试创建一个Observer
实例,在observe
函数内部,先判断当前传入的数据上是否有__ob__
属性,因为在上篇文章中说了,如果数据有__ob__
属性,表示它已经被转化成响应式的了,如果没有则表示该数据还不是响应式的,那么就调用new Observer(value)
将其转化成响应式的,并把数据对应的Observer
实例返回。
4.如何通知依赖 vaule
上的__ob__
就是其对应的Observer
类实例,有了Observer
类实例我们就能访问到它上面的依赖管理器,然后只需调用依赖管理器的dep.notify()
方法,让它去通知依赖更新即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 methodsToPatch.forEach(function (method ) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args ) { const result = original.apply(this , args) const ob = this .__ob__ ob.dep.notify() return result }) }
由于我们的拦截器是挂载到数组数据的原型上的,所以拦截器中的this
就是数据value
,拿到value
上的Observer
类实例,从而你就可以调用Observer
类实例上面依赖管理器的dep.notify()
方法,以达到通知依赖的目的。
5. 深度侦测 所谓深度侦测就是不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化。
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 export class Observer { value: any; dep: Dep; constructor (value: any) { this .value = value this .dep = new Dep() def(value, '__ob__' , this ) if (Array .isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this .observeArray(value) } else { this .walk(value) } } observeArray (items: Array <any>) { for (let i = 0 , l = items.length; i < l; i++) { observe(items[i]) } } }export function observe (value, asRootData ) { if (!isObject(value) || value instanceof VNode) { return } let ob if (hasOwn(value, '__ob__' ) && value.__ob__ instanceof Observer) { ob = value.__ob__ } else { ob = new Observer(value) } return ob }
对于Array
型数据,调用了observeArray()
方法,该方法内部会遍历数组中的每一个元素,然后通过调用observe
函数将每一个元素都转化成可侦测的响应式数据。
而对应object
数据,在上一篇文章中我们已经在defineReactive
函数中进行了递归操作
6.数组新增元素的侦测 只需拿到新增的这个元素,然后调用observe
函数将其转化即可。我们知道,可以向数组内新增元素的方法有3个,分别是:push
、unshift
、splice
。我们只需对这3中方法分别处理,拿到新增的元素,再将其转化即可。
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 methodsToPatch.forEach(function (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) ob.dep.notify() return result }) })
在上面拦截器定义代码中,如果是push
或unshift
方法,那么传入参数就是新增的元素;如果是splice
方法,那么传入参数列表中下标为2的就是新增的元素,拿到新增的元素后,就可以调用observe
函数将新增的元素转化成响应式的了。
7. 不足之处 对于数组变化侦测是通过拦截器实现的,也就是说只要是通过数组原型上的方法对数组进行操作就都可以侦测到,但是别忘了,我们在日常开发中,还可以通过数组的下标来操作数据
Vue也注意到了这个问题, 为了解决这一问题,
Vue增加了两个全局API:
Vue.set和
Vue.delete