前言
Vue的响应式系统是它的一个很重要的特性,数据模型的修改,视图要接收到通知进行变更。在刚接触到Vue框架时,也是对它的这个特性比较好奇,所以带着这个疑问,去了解了下它的整体运行机制。
数据变化如何追踪
当我们把一个对象加入到Vue的data中后,Vue 会遍历该对象的所有Property ,使用 Object.defineProperty 把这些 property 全部转发 getter/setter 。(相信熟悉服务端语言(Java/C#)的同学,看到这里,会很亲切,因为对这些在这两种语言中早已驾轻就熟了)
Object.defineProperty的属性描述符有两种形式: 数据描述符和存取描述符.
Object.defineProperty是实现数据变化的源头,简单看下是怎么创建一个对象的:
var value = 38;
Object.defineProperty(o, "b", {
// 使用了方法名称缩写(ES2015 特性)
// 下面两个缩写等价于:
// get : function() { return bValue; },
// set : function(newValue) { bValue = newValue; },
get() { return value; },
set(newValue) { value = newValue; },
// 可枚举
enumerable : true,
// 该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除
configurable : true
});
console.log(o.b); // 控制台输出 38
从上面代码形式可以知道,在Vue中声明一个属性时,会通过Object.defineProperty来定义这个属性的 getter/setter 方法,在真正访问属性时是调用get方法,更新属性时是调用 set 方法,那是不是可以直接在set 方法中,直接增加一个方法来监听属性的变化,当调用set 时,直接发一个消息,通知已经有监听这个通知的订阅者呢?
在Vue 内部其实也是通过这种方式,整个是通过 Observer 这个模块来实现,内部有 Observer -> Dep -> Watcher 。
首先看下 Observer的实现
- 构造函数
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
value 是要被观察的对象,同时会给这个对象添加 __ob__属性,作为已经被观察的标识。
以普通对象来分析这个构造过程,在walk 方法中, 遍历对象的每个key,对对象上每个key的数据调用defineReactive
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
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
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) {
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 通知
dep.notify()
}
})
}
在 Observer 中会通过构建属性值的变化,通过 Dep 通知各个已经订阅的 Watcher ,在Dep 中实现如下:
subs: Array<Watcher>;
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// 更新
subs[i].update()
}
}
在 Watcher update时,会通知到已经绑定的组件上。
简单勾画下三者关系
结束语
框架只是对基础的封装,就像Java/C# 这种高级语言只是对底层的封装,让使用者可以更加便捷的使用底层的一些特性来为项目服务,所以底层的基础知识还是需要更深入的学习。
了解了Vue的响应式原理,其实底层只是对于 Object.defineProperty的深度定制和扩展,只有深入到源码后,才可以比较好的了解其实质,当然这篇文章仅是作为入口,它的底层实现还是有很多细节,后面的篇幅会继续展开。
最后引用官方的响应式图看下数据流转