Vue双向数据绑定概述
Vue采用数据劫持 + 发布者-订阅者模式实现双向数据绑定,实现逻辑图如下所示:
数据劫持
Vue 借助Object.defineProperty()来劫持各个属性,这样一来属性存取过程都会被监听到
发布者-订阅者模式
主要实现三个对象:Observer(观察者),Watcher(订阅者,观察者),Dep(发布者,订阅收集器)。
1、Observer: 数据的观察者,让数据对象的读写操作(数据劫持)都处于自己的监管之下
2、Watcher: 数据的订阅者,数据的变化会通知到Watcher,然后由Watcher进行相应的操作,例如更新视图
3、Dep: Observer与Watcher的纽带,当数据变化时,会被Observer观察到,然后由Dep通知到Watcher
Observer(观察者)
// src/core/util/lang.js
// 这个方法就是对Object.defineProperty的封装,同时加入一些默认配置
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable, // 设置是否可枚举
writable: true,
configurable: true
})
}
// src/core/observer/index.js
export class Observer {
value:any; // 读写需要被监听的数据对象
dep: 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是defineProperty方法的封装
// 为数据对象设置一个__ob__属性,并赋值为当前Observer实例
def(value, '__ob__', this)
if (Array.isArray(value)) {
// hasProto是一个判断对象的__proto__属性是否可用的函数
// protoAugment是一个利用__proto__属性为数组或者对象扩充原型链的方法
// copyAugment是一个实现属性拷贝的方法
const augment = hasProto ? protoAugment : copyAugment
// arrayMethods是继承自数组原型对象(Array.prototype)的对象, arrayKeys是arrayMethods所有属性名的集合
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
}else { // value是对象
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) {
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) {
for (let i =0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
这类定义了三个实例属性:
value:需要被观察的数据对象;
dep:关联的依赖收集器对象(Dep类的实例对象);
vmCount:关联的vue实例对象个数。
接下来我们看一下构造函数constructor,初始化以上三个属性的代码就不多说了,我们简单说一下 def(value, '__ob__', this),这是在需要被观察的数据对象(value)上,增加__ob__属性,作为数据已经被Observer观察的标志。针对不同类型的value,vue做不同的处理。
实现对象的数据监听(value是对象)
value是对象处理过程比较简单,直接调用Observer的walk方法(Observer类的实例方法),而walk方法的内部其实是调用了defineReactive方法,那么我们来看一下walk方法和defineReactive方法:
// src/core/observer/index.js
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i =0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// src/core/observer/index.js
export function defineReactive (
obj: Object, // 被观察的对象
key: string, // 被观察的属性
val:any, // 该属性的值
customSetter?: ?Function, // 自定义的setter
shallow?: boolean // 是否只浅层次观察,类似于浅拷贝
) {
const dep =new Dep()
// 获取属性的配置对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable ===false) {
return
}
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
// 实现数据劫持
Object.defineProperty(obj, key, {
enumerable:true,
configurable:true,
get: function reactiveGetter () {
// 监听数据获取操作
},
set: function reactiveSetter (newVal) {
// 监听数据赋值操作
}
})
}
walk方法实现非常简单,在这里不再赘述。而defineReactive 方法的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。我们来详细说说defineReactive方法的实现过程:
1、定一个Dep类型的对象,用来作为依赖收集器 --- const dep =new Dep()
2、获取key属性的配置对象,如果配置项configurable为false,表示该属性不可配置,直接返回
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable ===false) {
return
}
3、缓存该key属性的get/set函数
const getter = property && property.get
const setter = property && property.set
4、进行shallow参数判断,要不要进行深层次观察(默认是进行深层次观察的),什么叫深层次观察呢?说直白点,就是当value是一个对象或者一个数组时,我们可以继续观察value对象的】每一个属性。而实现这个过程的是observer方法:
/**
* @param value: 任意类型的值
* @param asRootData: 判断是不是根数据
* @returns {Observer|void} 返回一个Observer实例对象或者无返回
*/
export function observe (value:any, asRootData: ?boolean): Observer | void {
// value必须是一个对象或者数组,且不能是vnode
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void // ob可以是Observer类型的对象或者undefined
// 当value的__ob__属性存在,说明该value已经存在Obsever,直接赋值给ob变量
if (hasOwn(value, '__ob__') && value.__ob__instanceof Observer) {
ob = value.__ob__
}else if (
shouldObserve &&
!isServerRendering() && // 判断是不是服务器渲染
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && // 是个可扩展的对象
!value._isVue// 不是Vue实例
) {
ob =new Observer(value)
}
// asRootData 为真,ob表示最外层的Observer实例,用vmCount记录vm实例数量,这里暂时还不知道是做什么用的
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
针对observer函数的实现流程,这里附上一张图方便小伙伴们理解整体的流程:
这里要说明的是这个图并非在下原创,来源于另一篇技术文章:https://segmentfault.com/a/1190000008377887?utm_source=tag-newest
实现数组的数据监听(value是数组)
我们再简单贴一下数组处理的相关代码:
// hasProto是一个判断对象的__proto__属性是否可用的函数
// protoAugment是一个利用__proto__属性为数组或者对象扩充原型链的方法
// copyAugment是一个实现属性拷贝的方法
// arrayMethods是继承自数组原型对象(Array.prototype)的对象,arrayKeys是arrayMethods所有属性名的集合
const augment = hasProto ? protoAugment : copyAugment // 1
augment(value, arrayMethods, arrayKeys) // 2
this.observeArray(value)
// observeArray方法源码
observeArray (items: Array) {
for (let i =0, l = items.length; i < l; i++) {
observe(items[i])
}
}
简单来讲,1和2的实现的东西是当对象存在__proto__属性时,直接将__proto__属性指向一个继承于数组原型的对象;否则就将数组原型里面定义的方法全部赋值到target上,并进行监听。最后调用this.observeArray(value),observeArray方法实际上就是遍历元素,然后依次调用前面提到的observer方法。
⚠️注意:当属性key的值value是数组时,并没有调用defineReactive方法对该属性进行劫持!为什么这么做呢?因为数组的属性其实就是索引,Object.defineProperty 本身做不到对这种属性变化的监听!!!所以我们在开发中有时候会遇到以下情况:即使修改了数据,视图并没有更新
vm.todos[0] = {
name: 'New name',
description: 'New description'
}
// 正确的数据更新方式,当数组元素是个对象时,vue还是会进入对象内部建立监听
vm.todos[0].name = 'New name';
vm.todos[0].description = 'New description';
⚠️注意:当数组调用'push', 'pop','shift','unshift','splice', 'sort','reverse'这些会改变数组自身的方法时,vue才能监听到数组的变化。
到目前为止,我们已经把Observer类和数据劫持过程讲解清楚了。接下来我们将继续分析 Dep 和 Watcher。