嵌套监听
简单的数据监听我们已经了解怎么做了,但如果属性也是个对象,我们希望它也能被监听呢?显然我们需要做循环判断了。
let test = { a:1, b:2, c:{ d:3, e:4, }, };Object.keys(obj).forEach(key=>{ let val = obj[key]; Object.defineProperty(obj,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) } }) if(typeof val === 'object'){ let newObj = val; Object.keys(newObj).forEach(key=>{ ... }) }})复制代码
把重复操作部分抽离出来变成递归函数
function define(obj,key,val){ Object.defineProperty(obj,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) } })}function watch(obj){ Object.keys(obj).forEach(key=>{ let val = obj[key]; define(obj,key,val); if(typeof val === 'object'){ watch(val); } })}watch(test);复制代码
现在我们已经可以做到监听深层对象了,但是如果我们修改某个属性为对象,比如test.a = {a:7,b:9}
,我们可以监听到test.a
的改变,但我们没法监听{a:7,b:9}
,所以上面的代码还需要强化一下。 我们只需要在set
时,进行一次判断即可。
function define(obj,key,val){ Object.defineProperty(obj,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) //添加了下面的代码 if(typeof val === 'object'){ watch(val); } } })}复制代码
到了这一步,一个对象的完整监听算是建立起来了。 接下来,我们需要解决一个核心的问题,监听对象变化,触发回调函数 这个才是我们监听对象的根本目的,我们要能添加自己的功能函数进去,而不是写死的console
$watch的实现
如果想要塞功能函数进去,显然我们还需要继续封装,因为至少我们要有存储功能函数的位置,还要有存储监听对象的位置,还得提供一个$watch
方法来添加功能函数 所以大概样子应该是这样的
function Observer(obj){ this.data = obj; //存监听对象 this.func_list = []; //存功能函数}Observer.prototype.$watch = function(){} //添加功能函数属于公共方法复制代码
正好我们前面抽离封装成了函数,只要组合一下即可
function Observer(obj){ this.data = obj; //存监听对象 this.func_list = []; //存功能函数 watch(this.data);}Observer.prototype.$watch = function(){} //添加功能函数属于公共方法复制代码
接下来我们考虑一下$watch
函数怎么写,正常的监听大概是这样的,字面理解就是当属性age
发生变化时,执行回调函数
var app = new Observer({ age:25, name:'big white'})app.$watch('age',function(newVal){ console.log(`年龄已经更新,现在是${newVal}岁`)})复制代码
那对我们内部实现来说,我们需要维护一个跟属性相关的回调数组,并且在对应属性发生变化时,挨个调用这个数组内的函数。
function Observer(obj){ this.data = obj; //存监听对象 this.func = {}; //这里改动了 watch.call(this); //后面解释为什么使用call}Observer.prototype.$watch = function(key,func){ //添加功能函数属于公共方法 let arr = this.func[key] || (this.func[key] = []); //没有对应数组就创建个空的 arr.push(func);} function execute(arr){ //执行功能函数数组 for(let fun of arr){ fun(); }}复制代码
那我们现在把所有的代码整合一下
function judge(val){ //监听判断 if(typeof val === 'object'){ new Observer(val); }}function execute(arr,val){ //执行功能函数数组 arr = arr || []; for(let fun of arr){ fun(val); }}function watch(){ //监听对象 Object.keys(this.data).forEach(key=>{ let val = this.data[key]; define.call(this,key,val); judge(val); })}function define(key,val){ let Fun = this.func; //拿到回调对象 Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) judge(val); execute(Fun[key],val); } })}function Observer(obj){ this.data = obj; //存监听对象 this.func = {}; //这里改动了 watch.call(this); //后面解释为什么使用call}Observer.prototype.$watch = function(key,func){ //添加功能函数属于公共方法 let arr = this.func[key] || (this.func[key] = []); //没有对应数组就创建个空的 arr.push(func);} 复制代码
现在代码已经跑通,我们可以任意添加监听的回调了,不过有几个点还是要单独说一下。 首先解释下为什么watch
这个监听函数要使用call
来调用,原因很简单,因为watch
函数内部是要访问对象实例的,虽说放到私有方法或者原型上也能访问到对象实例,但是我们其实并不希望暴露一个内部实现的方法,所以使用call
既可以绑定到对象实例,又能避免被暴露出去。define
函数也是同理。 然后第二个需要解释的是define
函数内的这一句 let Fun = this.func;
。其实最早我写的时候时候是直接let arr = this.func[key]
,流程一切正常,但是无法执行回调数组。后来我意识到,define
函数很早就执行了,且只执行一次,那个时候我们没有调用过$watch
,理所当然的arr
当然为undefined,且永远为undefined。所以外部必须获取引用类型的this.func
,即let Fun = this.func;
数组的获取只能放到set函数
内部,这样可以保证,每次execute
我们都做了一次回调数组的获取。
ok,简单监听已经实现完毕,我们调整下代码结构和名称,比如this.func
改为this.events
const Observer = (function(){ function judge(val){ //监听判断 if(typeof val === 'object'){ new Observer(val); } } function execute(arr,val){ //执行功能函数数组 arr = arr || []; for(let fun of arr){ fun(val); } } function _watch(){ //监听对象 Object.keys(this.data).forEach(key=>{ let val = this.data[key]; _define.call(this,key,val); judge(val); }) } function _define(key,val){ let Event = this.events; //拿到回调对象 Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ val = newVal; //console.log(`你修改了 ${key}`) judge(val); execute(Event[key],val); } }) } var constructor = function(obj){ this.data = obj; //存监听对象 this.events = {}; //回调函数对象 _watch.call(this); } constructor.prototype.$watch = function(key,func){ //注册监听事件 let arr = this.events[key] || (this.events[key] = []); //没有对应数组就创建个空的 arr.push(func); } return constructor;})()复制代码
再进一步
上面我们实现了$watch
,但是也仅仅是监听简单属性,如a
,面对如a.b
这种形式则毫无办法。 同理,假如a
属性是个对象,当a.b
发生变化时,也不会触发a
变化的回调函数。 也就是说我们的$watch
还停留在简单的一层对象上,数据的变化没有办法传递。
通过观察其实我们可以发现,无论是正向监听a.b
,还是a.b
的改变要触发a
的监听回调函数,逃不过去的东西就是一个层级,或者我们换个词path
我们的judge
函数是监听深层对象的关键
function judge(val){ //监听判断 if(typeof val === 'object'){ new Observer(val); }}复制代码
显然,目前虽然完成了监听,却没有和外层对象产生联系,当我们new Observer()
的时候,我们并不清楚这个新造的对象是根对象还是子对象,所以新建对象的时候应该把子对象在根对象的路径**path
** 传进去。 如果是根对象,那说明没有path
const Observer = (function(){ ... var constructor = function(obj,path){ this.data = obj; this.events = {}; this.path = path; //将path存在对象内部 _watch.call(this); } return constructor;})()复制代码
这样_define
和_watch
函数内部都能拿到path
,judge
函数也能正确调用
function judge(val,path){ //监听判断 if(typeof val === 'object'){ new Observer(val,path); }}复制代码
既然有了路径,当a.b
改变时,我们除了可以拿到b
这个属性名(key
),还能拿到a
这个path
,而我们注册事件的属性名就是a.b
,换句话说当触发更改时,我们只要execute(Event[path + '.' + key],val)
即可。 那么接下来只有一个问题:Event
不是同一个。 解决这个问题也很简单,让所有子对象跟根对象共用一个Event
对象即可
const Observer = (function(){ function judge(val,path,Event){ //又多了个参数 Event if(typeof val === 'object'){ new Observer(val,path,Event); } } function _define(key,val){ let Event = this.events; let Path = this.path? this.path+'.'+key : key; Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ if(newVal === val){ return; } val = newVal; judge(val,_this.path,Event); execute(Event[Path],val); } }) } ... var constructor = function(obj,path,Event){ //又多了个参数 Event this.data = obj; this.events = Event?Event:{}; //大家共用根组件的Event对象 this.path = path; //将path存在对象内部 _watch.call(this); } return constructor;})()复制代码
以上,我们就解决了第一个问题,$watch
可以监听a.b.c
的值了。 仔细一想,第二个问题其实也已经解决了,因为我们现在共用一个Event
对象,a.b.c
改变了,我们只要依次触发a.b
的回调函数,a
的回调函数即可。而a.b.c
这个path
,已经在我们手上了,所以只要改造下execute
函数,就能满足所有需求
function execute(Event,path,val){ //参数改变 let path_arr = path.split('.'); path_arr = path_arr.reduce((arr,key,index)=>{ //获得 a a.b a.b.c 数组 let val = arr[index -1]? arr[index-1]+'.'+key : key; arr.push(val); return arr; },[]); for(let i = path_arr.length-1;i>=0;i--){ //倒序调用 先触发a.b.c 再触发a.b let funs = Event[path_arr[i]] || []; if(i == path_arr.length-1){ for(let fun of funs){ fun(val); //直接被改变的属性可以拿到新值 } }else{ for(let fun of funs){ fun(); } } }}复制代码
完整代码如下:
const Observer = (function(){ function judge(val,path,Event){ //监听判断 if(typeof val === 'object'){ new Observer(val,path,Event); } } function execute(Event,path,val){ //执行监听回调 let path_arr = path.split('.'); path_arr = path_arr.reduce((arr,key,index)=>{ //获得 a a.b a.b.c 数组 let val = arr[index -1]? arr[index-1]+'.'+key : key; arr.push(val); return arr; },[]); for(let i = path_arr.length-1;i>=0;i--){ //倒序调用 先触发a.b.c 再触发a.b let funs = Event[path_arr[i]] || []; if(i == path_arr.length-1){ for(let fun of funs){ fun(val); //直接被改变的属性可以拿到新值 } }else{ for(let fun of funs){ fun(); } } } } function _watch(){ //监听对象 Object.keys(this.data).forEach(key=>{ let val = this.data[key]; let Path = this.path? this.path+'.'+key : key; _define.call(this,key,val,Path); judge(val,Path,this.events); }) } function _define(key,val,Path){ let Event = this.events; Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ if(newVal === val){ return; } val = newVal; judge(val,Path,Event); execute(Event,Path,val); } }) } var constructor = function(obj,path,Event){ this.data = obj; this.events = Event?Event:{}; //大家共用根组件的Event对象 this.path = path; //将path存在对象内部 _watch.call(this); } constructor.prototype.$watch = function(key,func){ //注册监听事件 let arr = this.events[key] || (this.events[key] = []); //没有对应数组就创建个空的 arr.push(func); } return constructor;})()复制代码
当然,代码还有继续优化的空间,不过目前已经能实现了我们所有的需求,至此,一个监听对象才算真正建立起来。