自己动手模拟MVVM之二实践数据劫持

所谓的数据劫持,说白了就是在数据变动的时候,可以执行一些自定义的操作,而通过Object.defineProperty来设置对象属性的setter和getter方法刚好可以做到这一点,所以只要学会的Object.defineProperty的使用,数据劫持是很简单的。

代码实践

  • 先准备一个对象

    1
    2
    3
    4
    var data={
    name:"daihaoxin",
    job:"coding"
    }
  • 获取对象的属性描述符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    console.log(Object.getOwnPropertyDescriptors(data));

    // 输出
    {
    "name": {
    "value": "daihaoxin",
    "writable": true,
    "enumerable": true,
    "configurable": true
    },
    "job": {
    "value": "coding",
    "writable": true,
    "enumerable": true,
    "configurable": true
    }
    }
  • 下面我们将data的属性转换为存取描述符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Object.keys(data).map((item,index)=>{
    var initData = data[item];
    Object.defineProperty(data, item, {
    enumerable: true,
    configurable: true,
    set: function(val) {
    console.log(`修改属性${item}值为${val}`);
    // 注意这里不能直接使用 data[item]=val,这样会引发重复设置属性值的死循环
    initData = val;
    },
    get: function() {
    console.log(`获取属性${item}的值${initData}`);
    // 注意这里不能使用data[item],同样会引发重复获取属性值的死循环
    return initData;
    }
    });
    });
    // 尝试修改属性的值试一试
    data.name="xingmu"; // 修改属性name值为xingmu
    data.name; // 获取属性name的值xingmu
    data.job="jser"; // 修改属性job值为jser
    data.job; // 获取属性job的值jser

通过Object.getOwnPropertyDescriptors对比前后两次的属性描述符,可以看到已经成功转换了

  • 封装为函数,有两点需要注意
    • 对参数做为空和类型验证
    • 对复杂属性进行递归处理
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      function dataIntercept(data) {
      if(!data || typeof data !== 'object'){
      console.log(`参数必须为对象`);
      return false;
      }
      Object.keys(data).map((item,index)=>{
      var initData = data[item];
      Object.defineProperty(data, item, {
      enumerable: true,
      configurable: true,
      set: function(val) {
      console.log(`修改属性${item}值为${val}`);
      initData = val;
      },
      get: function() {
      console.log(`获取属性${item}的值${initData}`);
      return initData;
      }
      });
      if(typeof initData === 'object'){
      dataIntercept(initData)
      }
      });
      }
      var data = {
      name: "daihaoxin",
      job: "coding"
      }
      dataIntercept(data);
      // 尝试修改属性的值试一试
      data.name="xingmu"; // 修改属性name值为xingmu
      data.name; // 获取属性name的值xingmu
      data.job="jser"; // 修改属性job值为jser
      data.job; // 获取属性job的值jser

上面的 dataIntercept 函数实现了一个数据监听,当监听某个对象后,我们可以在用户读取或者设置属性值的时候做个拦截,做我们想做的事

总结

这个dataIntercept能够实现预定的基本功能,但是对于属性值为数组类型的处理上还有些问题,无法监控数组的变化原因:

  • 当你修改数组的长度时,例如:arr.length = newLength
  • 当你利用索引直接设置一个项时,例如:arr[index] = newValue
  • 无法监控通过Array.prototype上的push、pop、shift、unshift、splice、sort、reverse、fill和copyWithin对数组的改变

参考

为什么defineProperty不能检测到数组长度的“变化”