数据监听进阶

嵌套监听


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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,099评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,404评论 0 17
  • 习惯了漂泊 找不到家的感觉 多少次的来与去 多少次的奔与赴 熟悉的声音是开往某地的列车就要进站 风缓缓吹 感受不到...
    迁于乔木阅读 202评论 0 0
  • 当他说,等我挣钱了给你也买一辆这车,我应该看着他说,好!等你挣钱就给我买 不要老提前男友
    六日即为晶晶阅读 143评论 0 0