我们在使用Vue时,只需要修改数据,视图就会自动更新,这就是数据响应
今天来学习Vue实现数据响应式的原理~
源码链接 https://gitee.com/ykang2020/vue_learn
前置知识 Object.defineProperty()
可以参考之前的博文
【JS】JavaScript对象属性-属性类型-数据属性-访问器属性-Object.defineProperty()方法-get方法-set方法
用getter
和setter
方法可以对数据进行监听,访问和设置都会被监听捕获
读取数据的时候会触发getter,而修改数据的时候会触发setter
我们要进行数据劫持,先想到的就是Object.defineProperty()
中给属性添加getter
和setter
方法,但是这么做有点问题~
defineProperty()
方法需要临时的全局变量周转getter
和setter
我们来看下面这个例子
let obj = {
};
let temp;
Object.defineProperty(obj, "a", {
get() {
console.log("getter试图访问obj的a属性");
return temp;
},
set(newValue) {
console.log("setter试图改变obj的a属性", newValue);
temp = newValue;
},
});
console.log(obj.a);
obj.a = 5
console.log(obj.a);
所以我们就自己定义一个函数,对defineProperty
进行封装,来实现数据劫持
使用defineReactive
函数不需要设置临时变量了,而是用闭包
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
// 可枚举 可以for-in
enumerable: true,
// 可被配置,比如可以被delete
configurable: true,
// getter
get() {
console.log(`getter试图访问obj的${
key}属性`);
return value;
},
// setter
set(newValue) {
console.log(`setter试图改变obj的${
key}属性`, newValue);
if (value === newValue) return;
value = newValue;
},
});
}
let obj = {
};
// 初始化
defineReactive(obj, "a", 10);
console.log(obj.a);
obj.a = 5;
console.log(obj.a);
上面定义的defineProperty()
函数,不能监听到对象嵌套的形式
也就是对象嵌套对象
function defineReactive(data, key, value) {
if (arguments.length === 2) {
value = data[key];
}
Object.defineProperty(data, key, {
// 可枚举 可以for-in
enumerable: true,
// 可被配置,比如可以被delete
configurable: true,
// getter
get() {
console.log(`getter试图访问obj的${
key}属性`);
return value;
},
// setter
set(newValue) {
console.log(`setter试图改变obj的${
key}属性`, newValue);
if (value === newValue) return;
value = newValue;
},
});
}
let obj = {
b: {
c: {
d: 4,
},
},
};
// 初始化
defineReactive(obj, "b");
console.log(obj.b.c.d);
所以我们要创建一个Observer
类 ——> 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
遍历对象
监听 value
尝试创建Observer实例,如果value已经是响应式数据,就不需要再创建Observer实例,直接返回已经创建的Observer实例即可,避免重复侦测value变化的问题
import Observer from "./Observer";
/**
* 监听 value
* @param {*} value
* @returns
*/
export default function observe(value) {
// 如果value不是对象,就什么都不做
if (typeof value != "object") return;
let ob;
if (typeof value.__ob__ !== "undefined") {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
Observer 类会附加到每一个被侦测的object上
一旦被附加,Observer会将object所有属性转换成getter/setter的形式
__ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例
import def from "./def";
import defineReactive from "./defineReactive";
/**
* 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
*/
export default class Observer {
// 构造器
constructor(value) {
// 给实例添加__ob__属性,值是当前Observer的实例,不可枚举
def(value, "__ob__", this, false);
console.log("Observer构造器", value);
// 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
this.walk(value);
}
// 遍历value的每一个key
walk(value) {
for (let key in value) {
defineReactive(value, key);
}
}
}
工具方法 定义一个对象属性
/**
* 定义一个对象属性
* @param {*} obj
* @param {*} key
* @param {*} value
* @param {*} enumerable
*/
export default function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true,
});
}
给对象data的属性key定义监听
import observe from "./observe";
/**
* 给对象data的属性key定义监听
* @param {*} data 传入的数据
* @param {*} key 监听的属性
* @param {*} value 闭包环境提供的周转变量
*/
export default function defineReactive(data, key, value) {
console.log('defineReactive()', data,key,value)
if (arguments.length === 2) {
value = data[key];
}
// 子元素要进行observe,形成递归
let childOb = observe(value)
Object.defineProperty(data, key, {
// 可枚举 可以for-in
enumerable: true,
// 可被配置,比如可以被delete
configurable: true,
// getter
get() {
console.log(`getter试图访问${
key}属性`);
return value;
},
// setter
set(newValue) {
console.log(`setter试图改变${
key}属性`, newValue);
if (value === newValue) return;
value = newValue;
// 当设置了新值,新值也要被observe
childOb = observe(newValue)
},
});
}
处理完对象,如果嵌套的是数组怎么办,下面我们来看看数组是怎么处理的
import observe from "./observe";
let obj = {
a: 1,
b: {
c: {
d: 4,
},
},
};
observe(obj);
obj.a = 5;
obj.b.c.d = 10;
正因为我们可以通过Array原型上的方法来改变数组的内容,所以ojbect那种通过getter/setter的实现方式就行不通了。
ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法。
Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理
这些方法分别是:push
、pop
、shift
、unshift
、splice
、sort
、reverse
这七个方法都是定义在Array.prototype
上,要保留方法的功能,同时增加数据劫持的代码
思路就是 以Array.prototype为
原型,创建一个新对象arrayMthods
然后在新对象arrayMthods
上定义(改写)这些方法
定义 数组 的原型指向 arrayMthods
这就相当于用一个拦截器覆盖Array.prototype
,每当使用Array
原型上的方法操作数组时,其实执行的是拦截器中提供的方法。在拦截器中使用原生Array
的原型方法去操作数组。
Object.setPrototypeOf()
修改对象原型Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null
Object.setPrototypeOf(obj, prototype)
obj
要设置其原型的对象
prototype
该对象的新原型(一个对象 或 null)
Object.create()
创建一个新对象Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
Object.create(proto,[propertiesObject])
proto
新创建对象的原型对象。
propertiesObject
可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。
返回
一个新对象,带着指定的原型对象和属性。
import def from "./def";
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法
const methodsNeedChange = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
// 备份原来的方法
const original = arrayPrototype[methodName];
// 定义新的方法
def(
arrayMethods,
methodName,
function () {
console.log("array数据已经被劫持");
// 恢复原来的功能(数组方法)
const result = original.apply(this, arguments);
// 把类数组对象变成数组
const args = [...arguments];
// 把这个数组身上的__ob__取出来
// 在拦截器中获取Observer的实例
const ob = this.__ob__;
// 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
let inserted = [];
switch (methodName) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
// 查看有没有新插入的项inserted,有的话就劫持
if (inserted) {
ob.observeArray(inserted);
}
return result;
},
false
);
});
__ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了
而且可以通过value.__ob__来访问Observer的实例
import def from "./def";
import defineReactive from "./defineReactive";
import observe from "./observe";
import {
arrayMethods} from './array'
/**
* 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
* Observer 类会附加到每一个被侦测的object上
* 一旦被附加,Observer会将object所有属性转换成getter/setter的形式
* 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
*/
export default class Observer {
// 构造器
constructor(value) {
// 给实例添加__ob__属性,值是当前Observer的实例,不可枚举
def(value, "__ob__", this, false);
// __ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例
// console.log("Observer构造器", value);
// 判断是数组还是对象
if (Array.isArray(value)) {
// 是数组,就将这个数组的原型指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
// 早期实现是这样
// value.__proto__ = arrayMethods;
// observe数组
this.observeArray(value);
} else {
this.walk(value);
}
}
// 对象的遍历方式 遍历value的每一个key
walk(value) {
for (let key in value) {
defineReactive(value, key);
}
}
// 数组的遍历方式
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行observe
observe(arr[i]);
}
}
}
之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方。
先收集依赖,把用到数据的地方收集起来,等属性改变,在之前收集好的依赖中循环触发一遍就好了~
总结起来就是
对象是
在getter中收集依赖,在setter中触发依赖
而数组是
在getter中收集依赖,在拦截器中触发依赖
目标明确,我们要在 getter
中收集依赖,那 依赖收集到哪里?
将依赖收集封装成一个类 Dep
这个类帮我们管理依赖
可以收集依赖、删除依赖、向依赖发送通知
需要用到数据的地方,成为依赖,就是Watcher
只有Watcher
触发的 getter
才会收集依赖,哪个Watcher
触发了getter
,就把哪个Watcher
收集到Dep
中
Dep
使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher
都通知一遍。
Watcher
是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
代码实现的巧妙之处:Watcher
把自己设置到全局的一个指定位置,然后读取数据。
因为读取了数据,所以会触发这个数据的getter
。在getter
中就能得到当前正在读取数据的Watcher
,并把这个Watcher
收集到Dep
中。
需要用一个数组来存watcher
watcher实例需要订阅 依赖(数据),也就是获取依赖或者收集依赖
watcher的依赖发生时,触发watcher的回调函数,也就是派发更新
Dep
使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher
都通知一遍。
Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向依赖发送通知等
let uid = 0;
/**
* Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向依赖发送通知等
*/
export default class Dep {
constructor() {
console.log("Dep");
this.id = uid++;
// 用数组存储自己的订阅者,放的是Watcher的实例
this.subs = [];
}
// 添加订阅
addSub(sub) {
this.subs.push(sub);
}
// 删除订阅
removeSub(sub) {
remove(this.subs, sub);
}
// 添加依赖
depend() {
// Dep.target 是一个我们指定的全局的位置,用window.target也行,只要是全局唯一,没有歧义就行
if (Dep.target) {
this.addSub(Dep.target);
}
}
// 通知更新
notify() {
console.log("notify");
// 浅拷贝一份
const subs = this.subs.slice();
// 遍历
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
/**
* 从arr数组中删除元素item
* @param {*} arr
* @param {*} item
* @returns
*/
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
import Dep from "./Dep";
let uid = 0;
/**
* Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
*/
export default class Watcher {
constructor(target, expression, callback) {
console.log("Watcher");
this.id = uid++;
this.target = target;
// 按点拆分 执行this.getter()就可以读取data.a.b.c的内容
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
get() {
// 进入依赖收集阶段。
// 让全局的Dep.target设置为Watcher本身
Dep.target = this;
const obj = this.target;
var value;
// 只要能找就一直找
try {
value = this.getter(obj);
} finally {
Dep.target = null;
}
return value;
}
update() {
this.run();
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(callback) {
const value = this.get();
if (value !== this.value || typeof value === "object") {
const oldValue = this.value;
this.value = value;
callback.call(this.target, value, oldValue);
}
}
}
/**
* 将str用.分割成数组segments,然后循环数组,一层一层去读取数据,最后拿到的obj就是str中想要读的数据
* @param {*} str
* @returns
*/
function parsePath(str) {
let segments = str.split(".");
return function (obj) {
for (let key of segments) {
if (!obj) return;
obj = obj[key];
}
return obj;
};
}
对象在getter中收集依赖,在setter中触发依赖
数组在getter中收集依赖,在拦截器中触发依赖
import Dep from "./Dep";
import observe from "./observe";
/**
* 给对象data的属性key定义监听
* @param {*} data 传入的数据
* @param {*} key 监听的属性
* @param {*} value 闭包环境提供的周转变量
*/
export default function defineReactive(data, key, value) {
// 每个数据都要维护一个属于自己的数组,用来存放依赖自己的watcher
const dep = new Dep();
// console.log('defineReactive()', data,key,value)
if (arguments.length === 2) {
value = data[key];
}
// 子元素要进行observe,形成递归
let childOb = observe(value);
Object.defineProperty(data, key, {
// 可枚举 可以for-in
enumerable: true,
// 可被配置,比如可以被delete
configurable: true,
// getter 收集依赖
get() {
console.log(`getter试图访问${
key}属性`);
// 收集依赖
if (Dep.target) {
dep.depend();
// 判断有没有子元素
if (childOb) {
// 数组收集依赖
childOb.dep.depend();
}
}
return value;
},
// setter 触发依赖
set(newValue) {
console.log(`setter试图改变${
key}属性`, newValue);
if (value === newValue) return;
value = newValue;
// 当设置了新值,新值也要被observe
childOb = observe(newValue);
// 触发依赖
// 发布订阅模式,通知dep
dep.notify();
},
});
}
数组在getter中收集依赖,在拦截器中触发依赖
import def from "./def";
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法
const methodsNeedChange = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
// 备份原来的方法
const original = arrayPrototype[methodName];
// 定义新的方法
def(
arrayMethods,
methodName,
function () {
console.log("array数据已经被劫持");
// 恢复原来的功能(数组方法)
const result = original.apply(this, arguments);
// 把类数组对象变成数组
const args = [...arguments];
// 把这个数组身上的__ob__取出来
// 在拦截器中获取Observer的实例
const ob = this.__ob__;
// 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
let inserted = [];
switch (methodName) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
// 查看有没有新插入的项inserted,有的话就劫持
if (inserted) {
ob.observeArray(inserted);
}
// 发布订阅模式,通知dep
// 向依赖发送消息
ob.dep.notify();
return result;
},
false
);
});
let obj = {
a: 1,
b: {
c: {
d: 4,
},
},
e: [22, 33, 44, 55],
};
console.log(obj);
observe(obj);
observe(obj);
console.log(obj);
observe(obj);
obj.a = 5;
observe(obj);
obj.a++
observe(obj);
console.log(obj.b.c)
observe(obj);
obj.b.c.d = 10;
observe(obj);
obj.e.push(66, 77, 88);
console.log(obj.e);
observe(obj);
obj.e.splice(2, 1, [13, 14]);
console.log(obj.e);
observe(obj);
new Watcher(obj, "b.c.d", (val) => {
console.log("watcher监听", val);
});