动态数据绑定之监听对象变化

---恢复内容开始---

动态数据绑定是MVVM框架中最基础的的一个功能,简单描述就是:将数据和视图进行绑定,当数据发生改变时,视图随之改变,更深层次一点,数据绑定包括单向数据绑定和双向数据绑定。

本文从数据绑定中的问题出发,一步一步的来实现这个功能。

本文的所有的源代码地址: 点击此处查看源代码

问题一

给定任意一个对象,如何监听其属性的读取与变化?也就是说,如何知道程序访问了对象的哪个属性,又改变了哪个属性?

举个例子:

let app = new Observer({
    name: 'liujianhuan',
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

//要实现的结果如下
app.data.name //你访问了name
app.data.company //你访问了company
app.data.address = 'Beijing' //你设置了address, 新的值为 Beijing

实现这样的一个Observer并不难,在此我们暂且不考虑数组的情况,只针对传入的参数为对象。如果对ES6和ES5都熟悉的话,可以立刻想到针对上述场景,可以有两种的实现方式:

  1. 采用ES6中的proxy,对目标对象的属性进行拦截处理
  2. 采用ES5中的defineProperty,为目标对象的属性添加setter和getter

接下来首先采用ES6中的proxy方法实现上述场景,首先从阮一峰老师的《es6入门标准中》摘录:

Proxy可以理解成在目标对象前架设一“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以称为“代理器”。

上边的话读完后应该和没读一样,放出来也只是用来装一下的。下边直接用简单的例子来说明:

ES6原生提供Proxy构造函数,用于生成Proxy实例。

var proxy = new Proxy(target,handler);
var proxy = new Proxy(target,handler);

Proxy对象的所有用法都是上面的形式,不同的只是handler参数的写法。其中new Proxy()表示生成的一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

下面来一个拦截读取属性行为的例子:

var proxy = new Proxy({},{
        get:function(target,property){
        return 35;
    }
});

proxy.time;//35
proxy.name;//35
proxy.title;//35

上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。


听话分割线出来了,以上内容摘自《ES6标准入门(第二版)》,特此声明!

看了上述的代码之后,我想也应该不用再太多的介绍了,直接上针对问题一的代码:

function Observer(data){
  return new Proxy(data, {
    get: function(target, key){
          if(key in target){
            console.log('你访问了' + key);
            return target[key];
          }else{
            throw new Error('key does not exist')
         }
    },
    set: function(target, key, newVal){
          console.log('你设置了' + key);
          console.log('新的' + key + '=' + newVal);
          target[key] = newVal;
    }
  })
}

let app = new Observer({
    name: 'liujianhuan',
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

测试结果如下图:

如上图结果所示,上述代码完美的实现了问题一中所提到的监听对象属性变化,但是深入思考就会发现,上述代码还是有问题的,因此,引出来问题二。

问题二

如果传入的参数对象是一个“比较深”的对象(也就是其属性值也可能是对象),那该怎么办?

举个例子:

let app = new Observer({
    basicInfo: {
        name: 'liujianhuan',
        age: 25
    },
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

//要实现的结果如下
app.data.basicInfo.name //你访问了basicInfo,你访问了name

首先利用问题一中的代码进行测试:

从结果可以看到并不能解决问题,到这里也许有人觉得只要在代码中加上这样的一段代码即可:

for(let key in data){
    if(data.hasOwnProperty(key) && typeof data[key] === 'object'){
          new Observer(data[key]);
    }
}

事实上是这种方式是无效的,读者可自行测试。究其原因,ES6中的proxy方式是通过Proxy构造函数来new一个实例,此实例代理拦截目标对象的操作,所以对于深层递归new出来的子对象实例我们是无法操作的,所以这种方法无效。

一步一步写的现在是不是觉得人生好无趣,好不容易写了这么多却发现行不通啊。这时候先上一碗热鸡汤,人生的每一步都是我们应该走的,因为它会给我们不同的经历,让我们更坚韧、更强大。 此路不通,那就只能再回首走开篇提到的第二条路了,不过这时候也应该上一碗毒鸡汤,所谓,码农之路就是,山重水复疑无路,柳暗花明又一坑。

第二种方法是采用ES5中的defineProperty,为目标对象的属性添加setter和getter。关于defineProperty的基本知识这里不再赘述,有不清楚的地方可以自行翻阅权威书籍,比如红宝书。直接上代码,下边的代码涵盖了问题一和二。

function Observer (data) {
  //暂不考虑数组
  this.data = data;
  this.makeObserver(data);
}
Observer.prototype.setterAndGetter = function (key, val) {
  //此为问题一的要点
  Object.defineProperty(this.data, key, {
    enumerable: true,
    configurable: true,
    get: function(){
      console.log('你访问了' + key);
      return val;
    },
    set: function(newVal){
      console.log('你设置了' + key);
      console.log('新的' + key + '=' + newVal);
      val = newVal;
    }
  })
}
Observer.prototype.makeObserver = function (obj) {
  let val;
  //此为问题二的要点
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      val = obj[key];
      //深度遍历
      if(typeof val === 'object'){
        new Observer(val);
      }
    }
    this.setterAndGetter(key, val);
  }
}

//测试
let app = new Observer({
basicInfo: {
    name: 'liujianhuan',
    age: 25
},
company: 'Qihoo 360',
address: 'Chaoyang, Beijing'

})

测试结果如下图:

看到这样的结果是不是很开心呢,同时解决了问题一和问题二,perfect。如果你这样想了,那就得回想一下毒鸡汤了,生活中不是缺少坑,是缺少发现坑的眼睛,直到被你柳暗花明之后踩到。。请继续看问题三。

问题三

如果设置新的值是一个对象的话,新设置的对象的属性是否能继续响应getter和setter呢?

举个例子:

let app = new Observer({
    basicInfo: {
        name: 'liujianhuan',
        age: 25
    },
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

//要实现的结果如下
app.data.basicInfo = {like: 'NBA'}//你设置了basicInfo,新的basicInfo为{like: 'NBA'}
app.data.basicInfo.like //你访问了basicInfo,你访问了like

采用问题二中的代码进行测试:

看到了吧,如果设置新的值是一个对象的话,新设置的对象的属性不能继续响应getter和setter。不过代码写到这里,这个问题应该是非常容易的就可以解决了,那就是直接在setter中添加如下代码:

//如果newval是对象的话
if(typeof newVal === 'object'){
    new Observer(val);
}

测试结果如下:

至此,我们已经较为完整的了实现了针对对象变化的数据监听,由于数组的操作方法比较多,所以针对数组的变化监听待后续完善,接下来我们针对上述代码继续增强完善。

完善点一

考虑传递回调函数。在实际应用中,当特定数据发生改变的时候,我们是希望做一些特定的事情,而不是每一次只能打印出来一些信息,所以,我们需要支持传入回调函数的功能。

举个例子:

let app = new Observer({
    name: 'liujianhuan',
    age: 25,
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

app.$watch('age', function(age){
    console.log(`我的年龄变了,现在是:${age}岁了`);
})

app.data.basicInfo.age = 20;//输出:'我的年龄变了,现在已经是20岁了'

针对上述场景,我们需要实现$watch这个API,每当年龄发生改变的时候触发相应的回调函数。这个API的实现可以很有多种方式,在此我们采用事件的方式来实现,通俗的讲就是实现一个通用的事件模型,每次$watch一个属性相当于注册了一个监听事件,当属性发生改变的则触发对应的事件,这样做的优势是可以为同一个属性通过事件模型来注册多个回调函数。

下边是一个不完整的简易事件模型:

//实现一个事件
function Event(){
  this.events = {};
}

Event.prototype.on = function(attr, callback){
  if(this.events[attr]){
    this.events[attr].push(callback);
  }else{
    this.events[attr] = [callback];
  }
}

Event.prototype.off = function(attr){
  for(let key in this.events){
    if(this.events.hasOwnProperty(key) && key === attr){
      delete this.events[key];
    }
  }
}

Event.prototype.emit = function(attr, ...arg){
  this.events[attr] && this.events[attr].forEach(function(item){
    item(...arg);
  })
}

有了上述事件模型后,每次new一个Observer的实例时,就new一个Event实例出来用来管理Observer实例中的所有事件;然后通过$watch API来为Observer实例注册属性的监听事件,每次当属性改变的触发相应的事件队列。

function Observer (data) {
  //暂不考虑数组
  this.data = data;
  this.makeObserver(data);
  this.eventsBus = new Event();
}

Observer.prototype.setterAndGetter = function (key, val) {
  let _this = this;
  Object.defineProperty(this.data, key, {
    enumerable: true,
    configurable: true,
    get: function(){
      console.log('你访问了' + key);
      return val;
    },
    set: function(newVal){
      console.log('你设置了' + key);
      console.log('新的' + key + '=' + newVal);
      //触发$watch函数
      _this.eventsBus.emit(key, val, newVal);
      val = newVal;
      //如果newval是对象的话
      if(typeof newVal === 'object'){
        new Observer(val);
      }
    }
  })
}

Observer.prototype.makeObserver = function (obj) {
  let val;
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      val = obj[key];
      //深度遍历
      if(typeof val === 'object'){
        new Observer(val);
      }
    }
    this.setterAndGetter(key, val);
  }
}

Observer.prototype.$watch = function(attr, callback){
  this.eventsBus.on(attr, callback);
}

let app = new Observer({
    name: 'liujianhuan',
    age: 25,
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

app.$watch('age', function(oldVal, newVal){
    console.log(`我的年龄变了,原来是: ${oldVal}岁,现在是:${newVal}岁了`)
})

app.$watch('age', function(oldVal, newVal){
    console.log(`我的年龄真的变了诶,竟然年轻了${oldVal - newVal}岁`)
})

app.data.basicInfo.age = 20;

测试结果如下:

测试结果显示上述代码触发了所注册的两个回调,但是上述代码也还是有着问题,比如目前只可注册监听对象的第一层的属性,对于对象的深层属性并不能有效监听,比如:

let app = new Observer({
    basicInfo: {
        name: 'liujianhuan',
        age: 25
    },
    company: 'Qihoo 360',
    address: 'Chaoyang, Beijing'
})

app.$watch('age', function(age){
    console.log(`我的年龄变了,现在是:${age}岁了`);
})

app.data.basicInfo.age = 20;

这段代码中的回调并不会触发,这个问题留下来在后续中完善补充。

总结一下本文中针对“动态数据绑定”还未解决掉的问题:

  1. 当传入的参数为数组时,如何监听数组对象的变化
  2. 深层对象属性的事件回调监听,或者描述为:对象的深层属性值发生变化后如何向上传递到顶层
  3. 动态数据与视图的绑定,如何绑定,当数据变化后如何触发视图的自动刷新。

另外附上两个最近使用Vue实现的 Vue在SKU组合查询中应用 和 基于Vue的后台管理模板

 

---恢复内容结束---

posted @ 2017-03-05 20:18  changning  阅读(2100)  评论(0编辑  收藏  举报