如何追踪变化
在JS中有两种方法可以侦测到变化:
- Object.defineProperty
- ES6的Proxy
Vue2中使用的是第一种方法,即Object.defineProperty,这是因为当时ES6在浏览器中的支持度并不理想。
利用Object.defineProperty侦测对象
function defineReactive(data, key, val){
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
console.log('get');
return val;
},
set: function(newVal){
console.log('set');
if(val===newVal){
return
}
val = newVal
}
})
}
每当从data的key中读取数据时,get函数被触发,每当往data的key中写入数据时,set函数被触发。
如何收集依赖
把用到数据的地方收集起来,然后等属性发生改变时,把之前收集好的依赖循环触发一遍。
即在getter中收集依赖,在setter触发依赖
依赖收集在哪里
// 收集依赖
class Dep{
constructor(){
this.deps = [];
}
addDeps(dep){
this.deps.push(dep);
}
// 添加依赖
depend(){
if(target){
this.addDeps(target);
}
}
// 移除依赖
removeDep(dep){
if(this.deps.length){
let id = this.deps.indexOf(dep);
if(id>-1){
this.deps.splice(id,1);
}
}
}
// 依赖更新并通知
notify(){
let deps = this.deps.slice();
deps.forEach((dep)=>{
dep.update();
})
}
}
// 侦测变化
function defineReactive(data, key, val){
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get(){
console.log('get');
dep.depend();
return val
},
set(newVal){
console.log('set');
if(val === newVal){
return;
}
val = newVal;
dep.notify()
}
})
}
// 自定义的依赖
function Target(name){
this.name = name,
this.update = function(){
console.log(`${this.name} has been updated!`)
}
}
let target = new Target('watcher')
依赖收集的代码封装成一个Dep类,用于管理依赖,通过这个类,我们可以收集依赖、删除依赖或者向依赖发送通知等。
Watcher
数据变化时,曾经使用了该数据的地方有很多,而且类型可能还不一样,既有可能是模板,也有可能是用户写的一个watch。所以,我们需要抽象出一个能集中处理这些情况的类。
然后,在收集依赖阶段,只收集这个封装好的类的实例进来,通知也只通知它一个,它再负责通知其他地方,这个抽象的东西(类),就叫做Watcher .
// 什么是watcher?watcher抽象类
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// 执行this.getter(),就可以读取data.a.b.c的内容
this.getter = parsePath(expOrFn) // parsePath是一个读取一个字符串keypath的函数,这里不再列举
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
在get方法中先把window.target设置成了this,也就是当前的watcher实例。然后读取数据时,就会触发getter(收集依赖的逻辑),并把依赖添加到Dep中。
依赖注入到Dep中后,每当数据发生变化时,就会让所有的依赖循环触发update方法,而update方法会执行参数中的回调函数,将value和oldValue传到参数中。
递归侦测所有key
class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
}
// 侦测对象的所有属性
walk(obj) {
const keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
// 收集依赖
class Dep{
constructor(){
this.deps = [];
}
addDeps(dep){
this.deps.push(dep);
}
// 添加依赖
depend(){
if(target){
this.addDeps(target);
}
}
// 移除依赖
removeDep(dep){
if(this.deps.length){
let id = this.deps.indexOf(dep);
if(id>-1){
this.deps.splice(id,1);
}
}
}
// 依赖更新并通知
notify(){
let deps = this.deps.slice();
deps.forEach((dep)=>{
dep.update();
})
}
}
// 侦测变化
function defineReactive(data, key, val) {
// 侦测子属性
if(typeof val === 'object'){
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log("get");
dep.depend();
return val;
},
set(newVal) {
console.log("set");
if (val === newVal) {
return;
}
val = newVal;
dep.notify();
},
});
}
// 自定义的依赖
function Target(name){
this.name = name,
this.update = function(){
console.log(`${this.name} has been updated!`)
}
}
let target = new Target('watcher')
let person1={
};
let grade={
Math: 98,
Eng: 90
}
defineReactive(person1,'grade',grade);
上面代码定义的Observer类将一个正常的object转换成被侦测的object(响应式的object)。也就是说,只要将一个对象传到Observer中,那么这个对象就会变成响应式的了。
关于object的问题
Vue.js通过Object.defineProperty将对象的key转换成getter/setter的形式来追踪变化,但Object.defineProperty无法追踪属性的增加和删除,这是因为在ES6之前JS没有提供元编程的能力(Proxy和Reflect)。
为了解决这个问题,Vue.js提供了两个API:
- vm.$set
- vm.$delete
总结
- 初始化数据Data通过Observer转换成了 getter/setter 的形式来追踪变化。
- 当外界通过 Watcher 读取数据时(watcher订阅的数据),会触发getter将watcher添加到依赖中。
- 当订阅的数据发生变化时,会触发setter,向Dep中的依赖(watcher)发送通知。
- Watcher 接收到通知后,会向外界发送通知,触发视图的更新。