数据监听进阶
嵌套监听
简单的数据监听我们已经了解怎么做了,但如果属性也是个对象,我们希望它也能被监听呢?显然我们需要做循环判断了。
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;
})()
当然,代码还有继续优化的空间,不过目前已经能实现了我们所有的需求,至此,一个监听对象才算真正建立起来。