我们上文写的这个方法 并不能很好的侦测对象所有的属性 或者说 不能比较简介的侦测所有属性
在实际业务中 对象里面套对象 也不是什么很少见的事 例如这样
这种 我们用上一种方法 就很麻烦了 所以 我们需要了解新的方法
要完成完整的属性监听 我们就需要一个工具类 这个类的名字是自己取的 这里建议叫 Observer 字面意思就可以看出 这个类起到一个观察的作用
而这个工具类的作用 就是 将一个object对象中的 每个属性 以及每个层级中的属性 都变成一个侦测的响应式数据
我们打开之前写的项目 将 dataResp.js 代码修改如下
const defineReactive = function(data,key,val) {
if(arguments.length == 2){
val = data[key];
}
let subset = observe(val);
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get() {
console.log(`您正在获取${key}的值`);
return val
},
set(value) {
console.log(`您正在修改${key}的值,更改后的值为${value}`);
if(value == val) {
return
}
val = value;
subset = observe(value);
}
});
}
const def = function(obj,key,value,enumerable) {
Object.defineProperty(obj,key,{
value,
enumerable,
//true 设为可写
writable: true,
//true 设为可被删除
configurable: true
});
}
class Observer{
constructor(value) {
//相当于 给拿到的对象 其中的__ob__绑定 值为thsi,在类中用this 表示取实例本身给__ob__赋值 最后一个enumerable为false 表示属性不参与for遍历
def(value,'__ob__',this,false);
this.walk(value);
}
walk(value) {
for(let key in value){
defineReactive(value,key);
}
}
}
export const observe = function(value) {
if(typeof value != "object") return;
var ob;
if (typeof value.__ob__ !== 'undefined'){
ob = value.__ob__ ;
}else{
ob = new Observer(value);
}
return ob;
}
被改的非常的复杂 不过不要急 看我效果 我们慢慢讲解
将output.js代码更改如下
import { observe } from "./dataResp"
const output = () => {
var obj = {
data: {
data: {
map: {
dom: {
isgin: true
}
},
arg: 13
},
name: "小猫猫"
},
bool: {}
};
observe(obj);
console.log(obj);
obj.data.data.map.dom.isgin = false;
document.getElementById("text").innerHTML = obj.data.name;
}
export default output
我们看看运行效果
首先 是看我们这个obj对象 被console.log输出在控制台 我们发现 他的每一次都被加上了一个 ob 的东西
很多人就会说 啊 我在vue响应式数据中也会看到这个 没错 这就是vue参照的一个实现方式
然后我们看运行效果
很显然 系统监听到了我们对obj.data.name的访问 所以 连着上面的data也被输出了一下 直到name 这一级结束 这就说明 你操作了一个数据 他的父节点也会被触发 比较 最为一个对象 自己的结构 也是他们内容的一部分 子集被访问或变量 也算是他们被访问或修改
然后很清晰的看到 系统捕获到了我们对obj.data.data.map.dom.isgin的修改
说明 这个方法已经实现了全部内容的监听 那么 我们来讲一下工作原理
这个图感兴趣的可以看一下 但老实说 我猜大部分一眼上去是看不懂的 我们从代码上将吧
首先 我们定义了一个 obj 对象 他的内部结构比较复杂 层级比较多 然后引用了dataResp.js 中的observe函数 参数就是我们的obj
然后进到observe中 我们先判断传进来的参数是不是一个对象
如果是对象 就不受影响 继续往下 否则 直接结束语句
这里 值得一提的是 因为判断语句是typeof 所以这个判断 数组和对象都是可以进来的 要的也就是这种效果
在这里存一个 ob 变量 用来存返回值
然后我们会判断 当前这个属性下面的 __ob__属性 是不是未定义
如果定义了 那么 他就不会等于undefined 就直接让ob等于他的__ob__然后返回就好了 这里 就解密 了 确实 __ob__没什么高大上的 就是一个标记 如果__ob__不是未定义 表示 当前的这个对象已经被处理过了 不用继续了 避免递归死循环
如果没有达到条件 说明 元素 __ob__是未定义状态 就需要处理 我们就new Observer 类 将他传进类里面去
然后我们再来看Observer
这里 constructor拿到的这个参数value就是我们new Observer时传进来的对象
然后 我们调用了 def方法 我们来看 def
第一个参数 value 他用在了Object.defineProperty第一个参数 说明 第一个参数是要声明响应式的对象 而这里 我们Observer里传的value 就是我们扔给Observer的对象 而第二个 也作为了Object.defineProperty的第二个参数 要给对象的哪个字段绑定 这里 我们就传了字符串 ob 说明 我们要绑定的是当前对象下的__ob__字段 Object.defineProperty的特性包括 如果对象没有要声明的字段 他会帮你创建 所以 我们没有__ob__他也会帮你创建出来
然后第三个参数 这个字段的值 就是 value对象的__ob__字段的值 我们Observer给了个 this 我们都知道 在类中使用 this 就是拿到当前类的实例 所以 我们将类new出来的实例 赋值给了__ob__
最后一个参数是enumerable 这个我们之前看过 就是 如果这个你设true 你用for遍历这个对象时 就可以看到这个字段 反之 设false 就不会参与for遍历 没人会喜欢这个 __ob__参与遍历吧 里面的信息又没用 他只是个标记
然后回到Observer的下半段代码
调用了声明在类中的walk方法 传入的参数还是value 当前的实例对象
walk 拿到对象的第一件事就是将他遍历开
而 我们目前这个value是之前传到observe 中的 obj 对象 遍历他 就会遍历出他下面的data和bool
然后 for遍历出啦的 对应的是键 而key是键 value 还是obj对象 所以
它遍历两次 传给defineReactive的参数分别是
obj对象, 字符串类型的 “data”
obj对象, 字符串类型的 “bool”
然后 我们来看defineReactive函数
进来先判断了 参数是不是只有两个 因为我知道我只传了两个参数啊
所以判断 如果只有两个参数 说明第三个参数 val是没有值的 所以
val = data[key]
根据上面的参数 我们知道 两次遍历调用 这句话分别代表
obj[“data”]
obj[“bool”]
就会取到当前传进来的对象的值
然后继续
我们让后面的对象继续去调用最开始obj 调用的 observe
obj在这个过程中给 data和bool声明了响应式 那么 继续回去调用这个方法的就还是data和bool
bool因为是个空的 所以 到Observer 的walk方法中 因为没有子集遍历 而停止
而data会继续带出下面data和name继续参与这个过程
data中的data自然没得说 继续重复过程
但 name 因为不是对象或数字 到observe刚开始 就会被判断拦住 并停止
继续看会到obj的这一层
因为 这里是通过遍历对象obj获取的key 所以 声明的分别是 obj的两个子集的响应式 第一个参数 要声明响应式的对象 obj 要声明的字段 就是两次遍历出来的key了
最后 在set中的subset = observe(value); 是保证当改变后 继续保存新值的响应