Skip to content

vue2实现数据双向绑定的数据挟持

约 932 字大约 3 分钟

vue2数据挟持Object.defineProperty

2025-05-22

vue2数据双向绑定介绍

双向数据绑定:数据劫持 + 发布-订阅模式

本篇主要讲vue2的数据挟持

  • 数据挟持

监听数据的变化 数据劫持是实现双向绑定的底层技术,其核心在于拦截对数据的读写操作,从而触发更新机制。

vue2数据挟持的原理

主要利用的是Object.Object.defineProperty (ES5)

通过定义属性的 gettersetter,在数据被访问或修改时执行回调函数。

  • Object.defineProperty

    一、核心功能

    1. 定义/修改属性 通过该方法可以直接在对象上添加新属性,或调整已有属性的特性(如是否可写、可枚举等)。与普通属性赋值(如 obj.prop = value)不同,它允许开发者对属性行为进行细粒度控制

    2. 控制属性特性 通过​​属性描述符​​(descriptor)参数,可以设置以下特性:

      数据描述符

      数据描述符(直接操作值)

      value

      属性的默认值(如数值、对象等)

      writable:若为 false,属性值不可被赋值运算符修改(但可通过 defineProperty 修改)

      configurable:若为 false,属性无法被删除,且除 valuewritable 外的特性不可修

      // 定义只读属性
      const obj = {};
      Object.defineProperty(obj, 'id', {
        value: 123,
        writable: false,
        configurable: false
      });
      obj.id = 456; // 修改无效(严格模式下报错)
      console.log(obj.id); // 123
      
      // 使用存取器实现响应式
      let _value = 0;
      Object.defineProperty(obj, 'count', {
        get() { return _value; },
        set(newVal) {
          _value = newVal;
          console.log('值已更新:', newVal);
        },
        enumerable: true
      });
      obj.count = 10; // 输出 "值已更新: 10"

    此方法需要配合深度监听实现

接下来看代码


Object.defineProperty('挟持数据''key','相关操作')

具体代码示例

const obj = {name:'张三'}

Object.defineProperty(obj,key,{
    get() { // 当obj.name是调用
        // 可以执行访问是的相关操作
        return obj[key]
    },
    set(newValue) { // obj.name = '李四' // 赋值是执行
        // 执行赋值操作
        if(obj[name] !== newValue) {
            // 此处如果不是单纯的对象,则需要深度遍历每一个来监听
            observer(newVal); 
           obj[name] = newValue
        // 此处
        upadtaView() //视图跟新函数
        }
    }
})

数组需要单独监听

  • 劫持数组的原有方法进行改造

    // 绑定数组监听
    const arrProto = Object.create(Array.prototype); // 获取Array原型里面的方法
    // 将数组方法复写挂载到新对象中
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
        
        // 改造数组
        arrProto[methodName] = function() {
            // 更新视图
            updateView();
            // 执行原型链的方法
            oldArrayProperty[methodName].call(this, ...arguments);
        }
    })

接下来看完整事例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button class="but">改变</button>
  <div class="content"></div>
  <script>
    const data = {
      name: 'Tom',
      age: 18,
      children: [
        {
          name: 'Jerry',
          age: 16
        }
      ]
    }

    // 视图更新函数
    const updateView = () => {
        // 这里面就可以利用diff进行比对dom
      console.log('视图更新了')
    }

    // 数组单独监听
    const oldFn = Array.prototype
        // 劫持数组的方法
    const arr = Object.create(oldFn)
    // 增加数组方法
   const fnArr =  ['push','pop','shift','unshift','splice']
   fnArr.forEach(fnName => {
    arr[fnName] = function() {
      updateView()// 更新视图
      oldFn[fnName].call(this,...arguments)
    }
   })


    // 深度监视
    const observer = (obj) => {
      if(typeof obj !== 'object')return obj;

      if(Array.isArray(obj)) {
        obj.__proto__ = arr
      }
      for(let key in obj) {
        defineReactive(obj,key,obj[key])
      }
    }

    // 执行对象监听
    const defineReactive = (obj,key, value) => {
      observer(value)
      Object.defineProperty(obj,key,{
        get() {
          return value
        },
        set(newValue) {
          if(newValue !== value) {
 // 深度监听,避免重新设置对象,无法监听
          observer(newValue)
          value = newValue
          updateView()
          }
         
        }
      })
    }


    observer(data)
const content = document.querySelector('.content')
content.innerHTML = data.children[0].name
    const but = document.querySelector('.but')
    but.addEventListener('click', () => {
      // data.children[0].name = '张三'
      data.or = '123'
      
    })
  </script>
</body>
</html>

优缺点

  • 可以兼容其他浏览器
  • 深度监听需要一次性递归
  • 无法监听 新增 \ 删除 属性
  • 无法原生监听数组,需要特殊处理