• 数据监听进阶


    嵌套监听


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

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

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

  • 相关阅读:
    k8s记录-helm部署(九)
    k8s记录-master组件部署(八)
    Hadoop记录-Apache hadoop+spark集群部署
    k8s记录-ubuntu安装docker
    Nginx记录-Proxy_pass多个应用配置(转载)
    Java动态调用脚本语言Groovy
    05 吸收应用-会整理还不够?教你吸收、联想、输出、应用
    02 超级搜索术——资源搜索:全面、快速查找全网你想要的任何信息、情报
    01 超级搜索术——信息搜索:全面、快速查找全网你想要的任何信息、情报
    长赢指数基金投资计划-201807(一补仓)
  • 原文地址:https://www.cnblogs.com/grey-zhou/p/8716801.html
一二三 - 开发者的网上家园