前言
vue是通过数据驱动视图
,如何知道数据发生改变,改变后又如何通知视图改变:
最近作者再面试中遇到了一个这样的问题:说一下有关VUE2对数据的变化侦听。在Vue2中对数据变化的侦听主要分成两种,一种是通过Object.defineProperty
方法对Object对象的侦听,一种是对数组方法重写的方式去侦听有关数组的变化。
这篇文章主要是梳理一下,有关vue中对数据侦听的一些内容,当是笔者学习源码的一个方式吧,如果有任何写的不对的地方,麻烦评论区留言。
(这一篇主要是对对象数据的侦听)
主要包括一些知识点:
1. Object.defineProperty
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
注意几个关键的点:
- 可以在对象上定义一个新的属性
- 修改一个对象的现有属性
这个方法接收三个参数
Object.defineProperty(obj, prop, descriptor)
你在哪个对象上使用该方法,你在对象的哪个属性(名称)上进行操作,以及,你操作的属性描述符是什么。最后返回一个对象
这些操作符包括:configurable(是否可配置),enumerable(是否可枚举),value(属性对应的值),writable(是否可写),get(执行访问属性),set(属性值被修改)
- 拥有布尔值(true,flase):
configurable
、enumerable
和writable
的默认值都是false
。 - 属性值和函数:
value
、get
和set
字段的默认值为undefined
。
1.1 使用Object.defineProperty
下面我定义了一个对象jing,它有一个属性age,我们可以对这个属性进行读取,修改。
let jing = {
age:22
}
console.log(jing.age) // 读取数据
jing.age = 18 // 修改数据
console.log(jing.age) // 读取数据
// 22
// 18
但是我们无法知道它上面时候被修改,被读取,而通过Object.defineProperty
,重写这个获取和修改的操作则可以达到
let jing = {}
let val = 22
Object.defineProperty(jing, 'age', {
enumerable: true,
configurable: true,
get(){
console.log('age属性被读取了')
return val
},
set(newVal){
console.log('age属性被修改了')
val = newVal
}
})
console.log(jing.age)
jing.age = 18
console.log(jing.age)
这个jing
的属性被读取或修改时,能够让jing
主动告诉我们,它的属性被修改或者是被获取
2 对Object的侦听和响应
2.1 Object
数据的侦测 Observer
源码在 src>core>observer>index>Observer(类) 这里我贴出的代码和源码有些出入,学习的话请自己拉下来看哦
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 如果是数组 走这里的方法
} else {
this.walk(value) // 如果是对象,通过walk方法转可观测对象
}
}
walk (obj: Object) {
const keys = Object.keys(obj) // 把对象的所有属性 存放到keys里面
for (let i = 0; i < keys.length; i++) { // 遍历这些属性值
defineReactive(obj, keys[i])
}
}
}
上面定义的Observer类可以将一个object转换成一个可被观测的object,通过遍历它里面的属性,对里面的属性进行操作,通过defineReactive
,下面我们来看看这个方法
export function defineReactive (
obj: Object,
key: string,
val: any,
) {
// 如果只传了两个参数,说明只要obj和key
if (arguments.length === 2) {
val = obj[key]
}
// 使用Object.defineProperty 对数据进行处理
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`${key}属性被读取了`)
return val
},
set: function reactiveSetter(newVal) {
if (val === newVal) {
return
}
console.log(`${key}属性被修改了`)
val = newVal
}
})
}
defineReactive
,就是做了我们最开始讲的通过Object.defineProperty
,去实现对象的可观测
比如说我们把之前的哪个jing对象通过Observer变成可观测的,就可以这么写:
let jing = new Observer({
age: 22
})
2.2 依赖收集器 Dep
又回到最开始说的,vue是通过数据驱动视图,现在我们知道的只是数据什么时候发生改变了,那么当数据改变后,又是如何通知视图发送改变的呢?在一个时刻可能有不同的数据发生改变,要做的应该是哪个数据发生了改变,就更新哪个数据对应的视图。
可是数据和视图的对应关系,并不是一对一的,他们可能是这样的:每个数据也可能影响多个地方的视图
就是说每个数据它都可能影响多个视图,假设是一个依赖数组(存放会受影响的视图),一旦数据被使用
,就创建一个依赖数组,让这个依赖数组去收集依赖,如果数据发生了改变
就去通知依赖更新。
之前以及将object变成可观测的数据了,因此我们可以知道它上面时候被使用(被获取时会触发getter
属性,那么我们就可以在getter
中收集这个依赖),什么时候发生了改变(当这个数据变化时会触发setter
属性,那么我们就可以在setter
中通知依赖更新)
当object数据被使用的时候,在getter
中收集这个依赖,那么在vue中如何实现这个依赖收集?(也就是之前说的收集依赖的数组)
在vue里面有个叫依赖管理器Dep
类:
src/core/observer/dep.js
export default class 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 () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有的依赖更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
这个一个Dep类,在里面定义了一个this.subs = []
用来存放依赖,并且有一些增加删除的方法,最后有一个通知所有依赖进行更新的 notify ()
方法
有了这个依赖收集器,再看看如何对我们的数据进行处理:
在之前的将object转换成可观测数据的类里面加上这个Dep
export function defineReactive (
obj: Object,
key: string,
val: any,
) {
const dep = new Dep()
// 如果只传了两个参数,说明只要obj和key
if (arguments.length === 2) {
val = obj[key]
}
// 使用Object.defineProperty 对数据进行处理
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`${key}属性被读取了`)
dep.depend()
return val
},
set: function reactiveSetter(newVal) {
if (val === newVal) {
return
}
console.log(`${key}属性被修改了`)
val = newVal
dep.notify()
}
})
}
2.3 依赖的更新 Watcher
每个数据它都可能影响多个视图,假设是一个依赖数组(存放会受影响的视图),一旦数据
被使用
,就创建一个依赖数组,让这个依赖数组去收集依赖,如果数据发生了改变
就去通知依赖更新。
上面这句话说的数据
我们又是怎么去判断描述是哪个数据被使用了呢?
在vue中如果谁用到了数据,就为他创建一个Watcher实例,在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch
实例,由Watcher
实例去通知真正的视图
2.4 不足的地方
Object.defineProperty
方法可以实现了对object
数据的可观测,但是这个方法仅仅只能观测到object
数据的取值及设置值
,
当我们向object
数据里添加一对新的key/value
Vue 无法探测普通的新增 property (比如 `this.myObject.newProperty = 'hi'`)
或删除一对已有的key/value
时,
Vue 不能检测到 property 被删除的限制
它是无法观测到的,导致当我们对object
数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。
Vue
增加了两个全局API:Vue.set
和Vue.delete
3. 一些实现
3.1 Observer
function mvvm(data, key, val) {
observer(val);
Object.defineProperty(data, key, {
get: function () {
return val;
},
set: function (newVal) {
val = newVal;
}
})
}
function observe(data) {
if (!data || typeof data != 'object') {
return;
}
Object.keys(data).forEach(function (key) {
mvvm(data, key, data[key]);
})
}
3.2 实现一个dep消息订阅器和订阅者
function mvvm(data, key, val) {
observe(val);
var dep = new Dep();
Object.defineProperty(data, key, {
get: function() {
if (Dep.self) { // 判断是否需要添加订阅者
dep.additem(Dep.self); // 在这里添加一个订阅者
}
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify(); // 如果数据变化,通知所有订阅者
}
});
}
// 源码里面是类哦
function Dep () {
// 这里的消息订阅是可以是一个数组
this.items = [];
}
Dep.prototype = {
addItem: function(item) {
this.items.push(item);
},
notify: function() {
this.items.forEach(function(item) {
item.update();
});
}
};
// 订阅者
function Watcher(_this, key, fun) {
this.fun = fun; //订阅者要触发的回调函数
this._this =_this; //订阅者订阅的对象
this.key = key; //订阅者订阅的key
this.value = this.get(); // 将自己添加到订阅器
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this._this.data[this.key];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.fun.call(this._this, value, oldVal);
}
},
get: function() {
Dep.self = this;
var value = this._this.data[this.key]
Dep.self = null;
return value;
}
};
总结
对应官网的话:
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
- 可观测数据(有getter和setter)有对应的依赖管理器Dep
- 谁用到了数据,谁就是依赖,我们就为谁创建一个
Watcher
实例,在创建Watcher
实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中Dep - 这个
Watcher
实例就代表这个依赖 - 当数据变化时,我们就通知
Watcher
实例 - 由
Watcher
实例再去通知真正的依赖
Vue
响应式原理的核心就是Observer
、Dep
、Watcher
。
Observer
中进行响应式的绑定,在数据被读的时候,触发get
方法,执行Dep
来收集依赖,也就是收集Watcher
。
在数据被改的时候,触发set
方法,通过对应的所有依赖(Watcher
),去执行更新。