参看相关文章以及 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
到这里的双向绑定原理中的订阅者-发布者以及数据劫持的部分已经实现,当然还需要一个解析器,去解析代码中需要双向绑定的地方。