Vue 双向绑定原理及手动实现

527 阅读1分钟

双向绑定原理

Vue 的双向绑定是通过 v-model 指令实现的,它实际上是语法糖,结合了属性绑定和事件监听:

  1. 数据到视图的绑定:通过数据劫持(Object.defineProperty 或 Proxy)和发布-订阅模式实现
  2. 视图到数据的绑定:通过监听表单元素的 input 或 change 事件实现

手动实现双向绑定

1. 使用 Object.defineProperty 实现

// 简易双向绑定实现
function defineReactive(obj, key, val) {
  const dep = new Dep(); // 依赖收集
  
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.addSub(Dep.target); // 收集依赖
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify(); // 通知更新
    }
  });
}

// 依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  
  addSub(sub) {
    this.subs.push(sub);
  }
  
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

// 观察者
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    
    Dep.target = this;
    this.vm[this.key]; // 触发getter,收集依赖
    Dep.target = null;
  }
  
  update() {
    this.cb.call(this.vm, this.vm[this.key]);
  }
}

// 实现v-model
function compile(el, vm) {
  el.querySelectorAll('[v-model]').forEach(node => {
    const key = node.getAttribute('v-model');
    new Watcher(vm, key, val => {
      node.value = val;
    });
    
    node.addEventListener('input', e => {
      vm[key] = e.target.value;
    });
  });
}

// Vue实例
class MyVue {
  constructor(options) {
    this.$data = options.data;
    this._observe(this.$data);
    this._compile(document.querySelector(options.el));
  }
  
  _observe(data) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key]);
    });
  }
  
  _compile(el) {
    compile(el, this);
  }
}

2. 使用 Proxy 实现(Vue3 方式)

class MyVue {
  constructor(options) {
    this.$data = options.data;
    this.$el = document.querySelector(options.el);
    this._proxy(this.$data);
    this._compile(this.$el);
  }
  
  _proxy(data) {
    const handler = {
      get(target, key) {
        return target[key];
      },
      set(target, key, value) {
        target[key] = value;
        // 这里应该通知更新视图
        return true;
      }
    };
    this.$data = new Proxy(data, handler);
  }
  
  _compile(el) {
    el.querySelectorAll('[v-model]').forEach(node => {
      const key = node.getAttribute('v-model');
      node.value = this.$data[key];
      
      node.addEventListener('input', e => {
        this.$data[key] = e.target.value;
      });
    });
  }
}

实际应用示例

<div id="app">
  <input v-model="message">
  <p>{{ message }}</p>
</div>

<script>
// 使用上面实现的MyVue
const app = new MyVue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});
</script>

总结

  1. Vue2 使用 Object.defineProperty 实现数据劫持
  2. Vue3 使用 Proxy 实现数据劫持,性能更好且能监听数组变化
  3. 双向绑定的核心是数据劫持 + 发布订阅模式
  4. v-model 本质上是语法糖,结合了 :value 和 @input

在实际面试中,可以结合自己的理解,从响应式原理、依赖收集、派发更新等方面展开说明。