参看相关文章以及 vue 源码,自己去实现 vue 的双向绑定,希望如果还对双向绑定比较懵懂的同学也可以照着代码自己打一遍,加深对 vue 双向绑定的理解。动手之前需要了解以下知识点:
- 要有 Vue 项目的实践经历,清楚 Vue 的使用方法。
- es6 中 Class 的使用方法。
参考文档
友情提示:本文可以伴着vue源码一起看,对源码的理解会更加深入
实现思路
vue.js 实现双向绑定就是使用数据劫持结合发布者-订阅者,通过 **Object.defineProperty()**来劫持每个属性的 setter getter ,在数据变动的时候发布消息给订阅者,触发相应的监听回调。
双向绑定实现的核心就是通过
**Object.defineProperty()**,对 data 每个属性进行了 get ,set 的拦截。
只用Object.defineProperty()已经可以实现双向绑定,但是效率非常低,需要结合观察者模式(提升双向绑定的效率),观察者模式是一对多的一种模式,就是修改某一个 data 的值,页面上只要用了这个 data 的地方都进行更新。
主要的实现思路就是
- 实现一个数据监听器 Observer,对 data 的所有属性进行监听,如果有变动就拿到最新的值,然后通知订阅者。数据监听器主要的方法就是
Object.defineProperty监听值的变化,再去维护一个数组收集订阅者,数据变动的时候去调用这些订阅者的update()方法。 - 实现一个指令解析器 Compile,这个就是对元素节点进行扫描解析,去替换数据(本文不实现,只进行模拟)
- 实现一个订阅者 Watcher,订阅并且收到每个属性变动的通知,执行相应回调函数,从而去更新视图。
代码实现
本文主要参考 vue2.x 源码手把手实现观察者 Observer 和订阅者 Watcher,暂不实现 vue 的指令解析,用 JS 代码模拟初始化和数据改变。模拟 vue 实例如下,声明一个data模拟 vue 中的data对象,对data建立观察者,新建Watcher去订阅data.msg的变化去看 vue 的双向绑定是如何运作的。
<!-- index.html -->
<script type="module">
import observe from "./observer.js";
import Watcher from "./watcher.js";
// 模拟 vue 中的 data 对象以及改变
var data = {
msg: "Hello",
};
console.log('begin', data);
// 模拟初始化 Data
observe(data);
// 模拟根据某些规则新建 Wacther
// 比如 v-bind、{{ xxx }} 等等
new Watcher(data, "msg", () => {
console.log("更新视图");
});
// 模拟数据更改
data.msg = "Hi";
</script>
实现一个观察者 Observer
首先是熟悉Object.defineProperty(obj, prop, descriptor),三个参数对象、属性、改变方法(get/set)。当我们用这个方法定义一个值的时候,调用时就使用了里面的 get 方法,赋值的时候就用了里面的 set 方法。具体的使用查看 MDN 文档,这里不详细说明。
<!-- index.html -->
<script type="module">
import observe from "./observer.js";
// 模拟 vue 中的 data 对象以及改变
var data = {
msg: "Hello",
};
console.log('begin', data);
// 模拟初始化 Data
observe(data);
// 模拟数据更改
data.msg = "Hi";
</script>
定义observe函数对data中所有属性进行遍历,并且使用Object.defineProperty对每个属性的get方法和 set方法进行劫持(这里就是所谓的数据劫持)。
// observe.js
export default function observe(data) {
// 这里先不考虑 vue 中 data 为函数的情况
if (!data || typeof data !== "object") {
return;
}
// 对data中所有数据遍历
Object.keys(data).forEach((key) => {
defindReactive(data, key, data[key]);
});
}
function defindReactive(data, key, val) {
observe(val); // 递归遍历子属性
// Object.defineProperty
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
console.log("触发 getter", val);
return val;
},
set: function (newVal) {
console.log("触发 setter", val, "->", newVal);
val = newVal;
},
});
}
执行之后看到效果,我们对 data.msg 进行修改的时候会触发自己定义的set方法

从上面的思路中可以知道,我们需要在set方法执行(也就是值发生改变的)的时候,去通知所有订阅者去执行一些更新动作。那么如何去管理订阅者,我们可以通过一个数组,将所有订阅者都放在这个数组里,当需要更新的时候,遍历这个数组里面的订阅者,依次去执行自己的更新动作。
// dep.js
// 维护一个数组来收集订阅者,数据变动的时候触发 notify() 通知各个订阅者,再调用订阅者的 update() 方法去更新数据
export default class Dep {
constructor() {
this.subs = []; // 此数组用来收集订阅者
}
// 将 Watcher 添加到订阅者列表中
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在getter里面将订阅者添加到数组里面,用Dep.target来指向当前正在执行操作的Watcher。
// observe.js
import Dep from "./dep.js"; // 用数组维护订阅者
// ...
function defindReactive(data, key, val) {
var dep = new Dep(); // 用数组维护订阅者
observe(val); // 递归遍历子属性
// Object.defineProperty
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再 define
get: function () {
console.log("触发 getter", val);
Dep.target && dep.addSub(Dep.target)
// console.log(dep)
return val;
},
set: function (newVal) {
console.log("触发 setter", val, "->", newVal);
val = newVal;
dep.notify(); // 通知所有订阅者
},
});
}
数据变化的时候通知订阅者更新这一步已经完成了,但是订阅者是什么?怎么添加订阅者?这两个问题我们还没有实现。接下来就要先实现我们一直说的订阅者
实现订阅者 Watcher
在index.html中直接去创建一个Watcher去模拟 vue 中初始化的时候根据某些规则去构造不同的Watcher(比如 v-bind、{{ xxx }}等等,这里不进行实现)。
// index.html
<script type="module">
import observe from "./observer.js";
import Watcher from "./watcher.js";
// 模拟 vue 中的 data 对象以及改变
var data = {
msg: "Hello",
};
console.log('begin', data);
// 模拟初始化 Data
observe(data);
// 模拟根据某些规则新建 Wacther
// 比如 v-bind、{{ xxx }} 等等
new Watcher(data, "msg", () => {
console.log("更新视图");
});
// 模拟数据更改
data.msg = "Hi";
</script>
实现 Observer 的时候我们知道,订阅者需要满足的条件:
- 要有一个
update方法去做更新动作,参数有一个回调函数,不同的Watcher可以在更新的时候做不同的动作(比如更新视图、用户自己定义的 watch 的回调函数等) - 需要记录当前的值
- 需要将自己添加到
dep里面- 初始化的时候要触发
getter - 将
Dep.target指向自己,触犯完getter之后把Dep.target清空
- 初始化的时候要触发
// watcher.js
import Dep from "./dep.js";
// 在自身实例化的时候往 dep 中添加自己;当属性变动 dep.notice() 通知时,调用自身的 update 方法,触发更新视图
export default class Watcher {
constructor(vm, exp, cb) {
this.cb = cb // 构建 Wacher 的时候可以传入不同回调函数,在更新时执行不同的操作
this.vm = vm // 实例(在这里就是 index.html 的 data 对象)
this.exp = exp // 相当于 key 值
this.value = this.get() // 实例化的时候触发 Observer 的 get,往 dep 添加自己
}
update() {
this.run()
}
run(){
const value = this.get() // 取得最新值
const oldValue = this.value
if(value !== oldValue) {
this.value = value
console.log('获得最新值:', value, ',去更新视图啦')
this.cb()
}
}
get() {
Dep.target = this // 将 Dep.target 指向自己
const value = this.vm[this.exp] // 触发数据的 getter,使得可以往数据里添加订阅者
Dep.target = null // 添加到数组后将指向清空
return value
}
}
执行一下代码就能看到在new Watcher的时候触发了一次 getter,改变数据执行 run()的时候又调用了一次getter,但是每一次调用getter的时候都将同一个Watcher放到了订阅者数组dep中,明显这是可以进行优化的地方。

所以我们需要给每一个dep标注一个唯一索引id,每个Watcher都记录下自己所在dep的id,并且判断我要加入的dep是否重复。代码优化后如下
// watcher.js
export default class Watcher {
constructor(vm, exp, cb) {
// ...
this.depIds = {} // 记录 depId
this.value = this.get() // 实例化的时候触发 Observer 的 get,往 dep 添加自己
}
// 需要判断 dep.id 是否已经在当前 Watcher 的 depIds 里面
// 如果在 oberser 的 getter 里面直接调用 addSub,会将 Watcher 重复添加到 dep 中
addDep(dep) {
if(!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this)
this.depIds[dep.id] = dep
}
}
}
// dep.js
let uid = 0;
export default class Dep {
constructor() {
this.id = uid++
// ...
}
// ...
// 调用目标 Watcher 的 addDep 方法
depend() {
Dep.target.addDep(this)
}
}
// observe.js
function defindReactive(data, key, val) {
var dep = new Dep(); // S2 用数组维护订阅者
observe(val); // 递归遍历子属性
Object.defineProperty(data, key, {
// ...
get: function () {
console.log("触发 getter", val);
// 不能直接调用 addSub,不然每次触发 getter 都会重复添加 Watcher
// Dep.target && dep.addSub(Dep.target)
if(Dep.target) {
dep.depend()
}
return val;
},
// ...
});
}
执行代码可以看到dep下面只有一个 Watcher](https://i-blog.csdnimg.cn/blog_migrate/5d199e6193cf93248d8fae2d8ba297cf.png)
到这里的双向绑定原理中的订阅者-发布者以及数据劫持的部分已经实现,当然还需要一个解析器,去解析代码中需要双向绑定的地方。
本文详细介绍了Vue双向绑定的实现原理,通过数据劫持和订阅者模式,利用`Object.defineProperty()`进行get和set拦截,结合Observer、Watcher和Compile三大核心组件,实现数据变化驱动视图更新。文中通过JavaScript代码模拟了Observer和Watcher的创建,展示了如何在数据变化时通知订阅者进行视图更新,帮助读者深入理解Vue的响应式系统。
432

被折叠的 条评论
为什么被折叠?



