嵌套监听


简单的数据监听我们已经了解怎么做了,但如果属性也是个对象,我们希望它也能被监听呢?显然我们需要做循环判断了。

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函数内部都能拿到pathjudge函数也能正确调用

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;
})()

当然,代码还有继续优化的空间,不过目前已经能实现了我们所有的需求,至此,一个监听对象才算真正建立起来。

版权声明:本文为grey-zhou原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/grey-zhou/p/8716801.html