博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
数据监听进阶
阅读量:6705 次
发布时间:2019-06-25

本文共 9456 字,大约阅读时间需要 31 分钟。

嵌套监听


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

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;})()复制代码

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

转载地址:http://wddlo.baihongyu.com/

你可能感兴趣的文章
atitit.taskService 任务管理器的设计 v1
查看>>
编写jquery插件的分享
查看>>
机器学习 —— 概率图模型(学习:对数线性模型)
查看>>
2016百度编程题:蘑菇阵
查看>>
解决教学问题新感悟
查看>>
nyoj 37 回文字符串
查看>>
Lintcode--006(交叉字符串)
查看>>
ASP.NET Core 1.0基础之依赖注入
查看>>
Excel里的单元格提行
查看>>
Matlab最短路径问题记录
查看>>
c语言单链表实现
查看>>
tcpdump非常实用的抓包实例
查看>>
ORACLE 日期函数 MONTHS_BETWEEN
查看>>
struts2.3+spring3.2+hibernate4.2例子
查看>>
进程调度
查看>>
北京地铁新机场线列车亮相调试 设计时速160公里/小时
查看>>
css布局基础总结
查看>>
Koa源码解析
查看>>
webpack系列之一总览
查看>>
乌龙事件之chrome页面部分白屏
查看>>