前端开发中数据绑定要解决的核心问题是:当 JavaScript 对象里的数据发生变化时,页面上引用该数据的位置能够自动更新;反过来,当用户在表单里输入内容时,对应的 JavaScript 数据也能同步修改。这种双向的同步机制大幅减少了手动操作 DOM 的代码量,但实现它需要解决两个关键点:一是如何知道数据变了,二是数据变化后如何通知所有依赖方。

目前主流的实现思路有发布订阅模式、脏检查机制和数据劫持三种。发布订阅需要显式地触发事件,脏检查则要不断比较新旧值,而数据劫持是在属性被访问或修改时直接插入自定义逻辑,不需要额外的轮询或手动通知。正因为这种自动触发的特性,数据劫持成为 Vue、MobX 等响应式框架的底层基础。
手动实现一个简单数据绑定
在深入响应式原理之前,可以先从一个最简单的例子入手。假设页面上有一个用于展示姓名的元素,当 JavaScript 中的 name 变量发生变化时,希望这个元素的内容自动更新。如果使用 Object.defineProperty 拦截属性的读写操作,就能在 set 方法里顺带更新 DOM。
// 使用 Object.defineProperty 实现数据拦截
const data = { name: '张三' };
let currentName = data.name;
Object.defineProperty(data, 'name', {
get() {
console.log('读取 name');
return currentName;
},
set(newValue) {
console.log('设置 name 为', newValue);
currentName = newValue;
document.querySelector('#output').textContent = newValue;
}
});
// 初始化视图
document.querySelector('#output').textContent = data.name;
// 修改数据后视图自动更新
data.name = '李四';
这段代码完成了最简单的单向数据绑定:从数据到视图。但它的局限性也非常明显,更新逻辑被写死在 set 里,如果页面上有多个位置依赖同一个数据,就需要在 set 中逐个更新。当应用规模变大时,这种硬编码方式几乎不可维护。因此需要引入依赖收集和派发更新的机制,把数据与依赖它的视图之间建立一种可扩展的关联。
其实数据绑定的本质可以理解为:数据是被观察者,视图或计算逻辑是观察者。当数据变化时,所有依赖该数据的观察者都要收到通知并执行更新。下面要进一步讨论如何自动收集这些依赖关系,而不是靠开发者在 set 里手动列出所有要更新的位置。
依赖收集与派发更新
完整的响应式系统通常包含三个角色:Dep(依赖容器)、Watcher(观察者)和响应式数据。核心思路是当某个计算过程读取了响应式属性时,就把当前正在执行的 Watcher 记录到该属性对应的 Dep 中;当属性被修改时,Dep 通知所有记录过的 Watcher 重新执行。这样依赖关系就是自动建立的,不再需要手动维护更新列表。
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeWatcher) {
this.subscribers.add(activeWatcher);
}
}
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
let activeWatcher = null;
class Watcher {
constructor(getter, callback) {
this.getter = getter;
this.callback = callback;
this.value = this.get();
}
get() {
activeWatcher = this;
const value = this.getter();
activeWatcher = null;
return value;
}
update() {
const newValue = this.getter();
this.callback(newValue, this.value);
this.value = newValue;
}
}
上面代码中的 activeWatcher 是一个全局变量,它的作用是标记当前正在求值的 Watcher。当响应式属性的 get 被调用时,Dep.depend() 会把这个 Watcher 加入到订阅者集合中。一旦属性被修改,Dep.notify() 就会遍历所有订阅者并触发它们的 update 方法。这样就形成了一个闭环:读取时收集依赖,修改时派发更新。
接下来要把普通的对象属性改造成响应式属性。下面是一个简化版的 defineReactive 函数,它给每个属性单独创建一个 Dep 实例。
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
dep.depend();
return val;
},
set(newVal) {
if (val === newVal) return;
val = newVal;
dep.notify();
}
});
}
通过这个函数,任意对象的指定属性都可以变成响应式数据。在读取属性时,依赖会被自动收集;在修改属性时,所有依赖会收到通知。这也是 Vue 2.x 响应式系统的核心思想。不过,这种基于 Object.defineProperty 的实现存在一些天然缺陷,比如新增属性和删除属性不会触发 set,数组索引的修改也无法被监听到。Vue 2.x 只能通过重写数组方法以及 Vue.set 等方式来弥补。
Proxy 带来的改进
ES6 提供了 Proxy 对象,它可以直接代理整个目标对象的多种内部方法,而不是像 Object.defineProperty 那样只能定义属性的 getter 和 setter。有了 Proxy,属性新增、删除、数组索引变化、甚至 in 操作符都可以被拦截,响应式覆盖范围更加完整。
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 在这里进行依赖收集
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 触发更新
}
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
// 触发更新
return result;
}
};
const proxyData = new Proxy({ count: 0 }, handler);
proxyData.count = 1; // set 触发
delete proxyData.count; // deleteProperty 触发
上面这段代码展示了 Proxy 的三个常用拦截点。与 defineProperty 相比,Proxy 的拦截粒度更细,而且不需要在初始化时递归遍历对象的所有属性,可以做到惰性响应,也就是只有在真正访问某个嵌套对象时才去创建它的代理。这种设计减少了初始化开销,也避免了动态新增属性无法响应的问题。Vue 3 选择 Proxy 作为响应式引擎的基础,正是因为这些优势。
不过 Proxy 也不是没有代价。它无法被完全 polyfill,对旧版浏览器的兼容性不如 defineProperty。另外,代理对象和原始对象不是同一个引用,在某些场景下可能会引起身份判断的问题。好在现代主流环境对 Proxy 的支持已经足够稳定,因此它逐渐成为响应式实现的首选方案。
双向绑定的完整实现
数据绑定最常见的使用场景是表单输入与数据状态的同步。下面是一个基于 Proxy 的完整双向绑定示例:当用户在输入框中打字时,数据对象会更新;当数据对象被程序修改时,展示区域也会自动更新。
const input = document.querySelector('#name-input');
const output = document.querySelector('#name-output');
const state = new Proxy({ name: '' }, {
set(target, key, value) {
target[key] = value;
output.textContent = value;
return true;
}
});
input.addEventListener('input', (event) => {
state.name = event.target.value;
});
state.name = '初始值';
这个例子省略了依赖收集的部分,直接把视图更新写在了 set 拦截器里,目的是更直观地展示双向绑定的链路。实际框架中的实现会结合前面提到的 Dep 和 Watcher 机制,让任何依赖该数据的计算属性和组件都能自动更新,而不是只更新某一个 DOM 元素。
从手动绑定到自动依赖收集,再到 Proxy 的全面拦截,JavaScript 数据绑定的演进方向是减少模板代码、扩大拦截范围,同时保持运行时的高效。理解这套响应式原理,有助于在使用 Vue、MobX 等框架时快速定位数据不更新的问题,也能为自研轻量级状态管理库打下基础。
JavaScript数据绑定响应式原理数据劫持修改时间:2026-09-27 09:46:08