简单的数据监听咱们已经了解怎么作了,但若是属性也是个对象,咱们但愿它也能被监听呢?显然咱们须要作循环判断了。数组
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=>{ ... }) } })
把重复操做部分抽离出来变成递归函数app
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
方法来添加功能函数
因此大概样子应该是这样的this
function Observer(obj){ this.data = obj; //存监听对象 this.func_list = []; //存功能函数 } Observer.prototype.$watch = function(){} //添加功能函数属于公共方法
正好咱们前面抽离封装成了函数,只要组合一下便可prototype
function Observer(obj){ this.data = obj; //存监听对象 this.func_list = []; //存功能函数 watch(this.data); } Observer.prototype.$watch = function(){} //添加功能函数属于公共方法
接下来咱们考虑一下$watch
函数怎么写,正常的监听大概是这样的,字面理解就是当属性age
发生变化时,执行回调函数code
var app = new Observer({ age:25, name:'big white' }) app.$watch('age',function(newVal){ console.log(`年龄已经更新,如今是${newVal}岁`) })
那对咱们内部实现来讲,咱们须要维护一个跟属性相关的回调数组,而且在对应属性发生变化时,挨个调用这个数组内的函数。server
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; })()
固然,代码还有继续优化的空间,不过目前已经能实现了咱们全部的需求,至此,一个监听对象才算真正创建起来。