什么是数据双向绑定
数据双向绑定(Two-Way Data Binding) 是一种机制,用于实现视图(UI)和数据模型(Model)之间的双向同步更新。在双向绑定中,当数据发生变化时,视图会自动更新;同样,当视图(例如用户输入)发生变化时,数据模型也会自动更新。
Vue数据双向绑定原理

- 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter这样的 话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
- compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对 应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:在自身实例化时往属性订阅器(dep)里面添加自己自身必须有一个 update()方法待属性变动 dep.notify()通知时,能调用自身的update()方法,并触发 Compile 中绑定的回调,则功成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听 自己的 model 数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新;视图交互变化(input)->数据model变更的双向绑定效果。
js
class Vue {
constructor(options) {
this.$data = options.data;
// 调用数据劫持的方法
Observe(this.$data);
// 属性代理,将 data 属性代理到 Vue 实例上
······
// 调用模板编译的函数
Compile(options.el, this);
}
}
// 定义数据劫持的方法
function Observe(obj) {
Object.defineProperty(obj, key, {
get() {
// Watcher 实例添加到dep.subs中,
Dep.target && dep.addSub(Dep.target);
},
set(newVal) {
Observe(value);
// 通知每一个订阅者更新自己的文本
dep.notify();
},
});
}
// 对 HTML 结构进行模板编译
function Compile(el, vm) {
// 初始化视图
.......
new Watcher(vm, execResult[1], (newValue) => {
// 更新视图
});
}
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 所有的 watcher 都要存到这个数组中
this.subs = [];
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher);
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
// 订阅者的类
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 把创建的 Watcher 实例存到 Dep 实例的 subs 数组中
Dep.target = this;
// 进行一次取值操作出发getter函数,在getter函数中将watcher实例添加到dep收集依赖数组中
key.split(".").reduce((newObj, k) => newObj[k], vm);
Dep.target = null;
}
update() {
......
}
}
Vue数据双向绑定原理代码拆解
1.框架入口(new MVVM())
- 接收 options 配置对象,包含 data 和 el 属性
- 对 data 对象进行数据劫持(Observe)
- 将 data 属性代理到 Vue 实例上,可以直接通过
vm.name
访问而不是vm.$data.name
- 编译模板(Compile)
js
class Vue {
constructor(options) {
this.$data = options.data;
// 调用数据劫持的方法
Observe(this.$data);
// 属性代理
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key];
},
set(newValue) {
this.$data[key] = newValue;
},
});
});
// 调用模板编译的函数
Compile(options.el, this);
}
}
2.数据劫持(Observer)
核心作用:
1.使用 Object.defineProperty
为每个属性添加 getter/setter
2.getter 中进行依赖收集(Dep.target && dep.addSub
)
3.setter 中触发更新(dep.notify
)
4.递归处理嵌套对象
缺点:
1.无法劫持新增属性(需用 Vue.set
)。
2.对数组需重写方法(如 push
)。
js
function Observe(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== "object") return;
const dep = new Dep();
// 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
Object.keys(obj).forEach((key) => {
// 当前被循环的 key 所对应的属性值
let value = obj[key];
// 把 value 这个子节点,进行递归
Observe(value);
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
// 就被放到了 dep.subs 这个数组中了
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
value = newVal;
Observe(value);
// 通知每一个订阅者更新自己的文本
dep.notify();
},
});
});
}
3.模板编译(Compile)
- 模板编译和节点替换
将模板中的文本节点(比如{{ name }}
)提取出来,并替换成对应的动态数据(如vm.name
)。这部分用到了正则表达式来查找模板中的插值表达式并替换它们。 - 响应式绑定
对于文本节点和input
元素,它通过Watcher
类来实现响应式更新。Watcher
会监听数据变化,当数据发生变化时,自动更新对应的 DOM 元素。 - v-model 双向绑定
对于input
元素,代码实现了v-model
的基本功能。通过获取v-model
属性值(例如v-model="name"
),将input
元素的值与vm.name
绑定,并在用户输入时更新vm.name
。 - 性能优化
使用document.createDocumentFragment()
提高 DOM 操作的性能,因为fragment
是内存中的一个轻量级 DOM 对象,不会触发浏览器的重排和重绘。
js
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el);
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment();
// 将根元素的子节点全部移入文档片段(避免频繁操作真实 DOM)
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode);
}
// 进行模板编译
replace(fragment);
vm.$el.appendChild(fragment);
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 用正则 /\{\{\s*(\S+)\s*\}\}/ 提取表达式(如 {{ name }} 中的 name)
const regMustache = /\{\{\s*(\S+)\s*\}\}/;
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent;
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text);
if (execResult) {
// 通过 reduce 链式访问嵌套属性(如 info.address)
const value = execResult[1]
.split(".")
.reduce((newObj, k) => newObj[k], vm);
node.textContent = text.replace(regMustache, value);
// 在这个时候,创建 Watcher 类的实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue);
});
}
// 终止递归的条件
return;
}
// 判断当前的 node 节点是否为 input 输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === "INPUT") {
// 得到当前元素的所有属性节点
const attrs = Array.from(node.attributes);
const findResult = attrs.find((x) => x.name === "v-model");
if (findResult) {
// 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
const expStr = findResult.value;
const value = expStr.split(".").reduce((newObj, k) => newObj[k], vm);
node.value = value;
// 创建 Watcher 的实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue;
});
// 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
node.addEventListener("input", (e) => {
const keyArr = expStr.split(".");
const obj = keyArr
.slice(0, keyArr.length - 1)
.reduce((newObj, k) => newObj[k], vm);
const leafKey = keyArr[keyArr.length - 1];
obj[leafKey] = e.target.value;
});
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child));
}
}
4.发布者-订阅者模式(Dep、Watcher)
Dep
(依赖)类 负责管理订阅者(Watcher
实例),通过addSub
添加订阅者,并通过notify
通知所有订阅者更新。Watcher
(观察者)类 存储一个回调函数,当被Dep
通知时,执行该回调。Dep
作为调度中心,统一管理和触发依赖更新
js
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存到这个数组中
this.subs = [];
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher);
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
// 订阅者的类
class Watcher {
// cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据,
// 因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
// 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
// 因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
// Dep.target作用是一个静态属性,表示当前正在执行的 watcher 实例,确保在执行 getter 函数时,能
够把 watcher 实例添加到 dep 的 subs 数组中
Dep.target = this;
// 这段代码的意义就是进行一次取值操作出发getter函数,在getter函数中将watcher实例添加到dep收集依
// 赖数组中
key.split(".").reduce((newObj, k) => newObj[k], vm);
Dep.target = null;
}
// watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
update() {
const value = this.key.split(".").reduce((newObj, k) => newObj[k], this.vm);
this.cb(value);
}
}