前言
上一篇《前端 mvvm 框架底层学习(七、双向绑定优化)》介绍了我们如何解决了双向绑定中的局部更新的问题,或者解决更小颗粒度更新的问题,但代码比较混乱,所有的 Dom 操作及数据绑定操作都耦合到了一起。这里我们来学习一下 Vue 是如何结合合经典的发布/订阅模式进行双向绑定优化的。
Vue的observer依赖关系图[本站原创]
如果直接去刨 Vue 的 observer
的源码,整个流程其实是非常复杂的,下面就把流程图画一下便于全局理解
由图上可以看到一处很巧妙的地方 Observer
通过 observe(val)
递归遍历实现了对对象进行深度 observe
/ watch
。这里面其实出现了双向的互相依赖并不是十分完美,理想的状态是不使用双向的互相依赖。立个flag,后续将尝试按自己的方式实现 Vue 的 Observer
。:)
来自于简书用户 “流动码文” 的简化版本 Vue 双向绑定源码
本来想自己去把作者源码中的 flow 代码精简,后面发现网上有牛人流动码文已经整理出非常精辟的代码,而且注释非常详尽;在这里,我参照 Vue 源码对注释修改了一下,下面一起来学习学习
引用自:https://www.jianshu.com/p/2df6dcddb0d7
const Vue = (function() {
let uid = 0;
// 用于储存订阅者并发布消息
/**[新增]
* A dep is an observable that can have multiple 一个Dep就是一个可被监听的对象(依赖)
* directives subscribing to it. 并且可以供多个指令进行订阅(发布/订阅模式)
*/
class Dep {
constructor() {
// 设置id,用于区分新Watcher和只改变属性值后新产生的Watcher
this.id = uid++; // [新增]uid for batching,保证所有依赖的id都是唯一的
// 储存订阅者的数组
this.subs = [];
}
// 触发target上的Watcher中的addDep方法,参数为dep的实例本身
depend() {
Dep.target.addDep(this);
}
// 添加订阅者
addSub(sub) {
this.subs.push(sub);
}
notify() {
// 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
this.subs.forEach(sub => sub.update());
}
}
// 为Dep类设置一个静态属性,默认为null,工作时指向当前的Watcher
// [新增] The current target watcher being evaluated. 当前的目标watcher被访问
// This is globally unique because only one watcher 每次全局一次只能有一个watcher在被访问
// can be evaluated at a time.
Dep.target = null;
// 监听者,监听对象属性值的变化
/**[新增]
* Observer class that is attached to each observed 这里表达得很清楚,Observer类是绑定到每个被观察的对象
* object. Once attached, the observer converts the target 一旦被绑定上,observer就会给目标对象的属性值keys添加
* object's property keys into getter/setters that 上用于收集依赖集dependencies的getter/setters的勾子并且分发
* collect dependencies and dispatch updates. 更新事件
*/
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
// 遍历属性值并监听
/**[新增]
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object. 这里限定value只能是对象时才会触发walk函数
*/
walk(value) {
Object.keys(value).forEach(key => this.convert(key, value[key]));
}
// 执行监听的具体方法
convert(key, val) {
defineReactive(this.value, key, val);
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
// 给当前属性的值添加监听
let chlidOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 如果Dep类存在target属性,将其添加到dep实例的subs数组中
// target指向一个Watcher实例,每个Watcher都是一个订阅者
// Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
if (Dep.target) {
dep.depend();
}
return val;
},
set: newVal => {
if (val === newVal) return;
val = newVal;
// 对新值进行监听
chlidOb = observe(newVal);
// 通知所有订阅者,数值被改变了
dep.notify();
},
});
}
/** [新增]尝试为一个值创建一个观察者实例,如果成功就返回一个Observer,或者如果value已经有就返回已有的
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe(value) {
// 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
/** [新增]
* A watcher parses an expression, collects dependencies, 一个watcher会解释一个表达式,收集所有依赖
* and fires callback when the expression value changes. 接着当表达式的值发生改变时会触发回调。
* This is used for both the $watch() api and directives. 会在$watch的api和directives指令中用到
*/
class Watcher {
constructor(vm, expOrFn, cb) {
this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者
this.vm = vm; // 被订阅的数据一定来自于当前Vue实例
this.cb = cb; // 当数据更新时想要做的事情
this.expOrFn = expOrFn; // 被订阅的数据
this.val = this.get(); // 维护更新之前的数据
}
// 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用,
update() {
this.run();
}
/**[新增]
* Add a dependency to this directive.给这个指令添加一个依赖dep
*/
addDep(dep) {
// 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存
// 此判断是避免同id的Watcher被多次储存
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this); // [新增]给dep增加本watcher为订阅者
this.depIds[dep.id] = dep; // [新增]本watcher的依赖id数据depIds相应加上dep
}
}
run() {
const val = this.get();
console.log(val);
if (val !== this.val) {
this.val = val;
this.cb.call(this.vm, val); // [新增]数据变化,执行相应的回调
}
}
/**[新增]
* Evaluate the getter, and re-collect dependencies.取新的getter里的值,和重新收集相关的依赖
*/
get() {
// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
Dep.target = this; // [新增]给全局的Dep的target赋值,因为每次只能有一个watcher被访问
const val = this.vm._data[this.expOrFn];
// 置空,用于下一个Watcher使用
// [新增]之前说过一次只能有一个target被访问
Dep.target = null;
console.log(Dep.target, 2);
return val;
}
}
class Vue {
constructor(options = {}) {
// 简化了$options的处理
this.$options = options;
// 简化了对data的处理
let data = (this._data = this.$options.data);
// 将所有data最外层属性代理到Vue实例上
Object.keys(data).forEach(key => this._proxy(key));
// 监听数据
observe(data);
}
// 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
$watch(expOrFn, cb) {
new Watcher(this, expOrFn, cb);
}
_proxy(key) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: () => this._data[key],
set: val => {
this._data[key] = val;
},
});
}
}
return Vue;
})();
let demo = new Vue({
data: {
text: '',
},
});
const p = document.getElementById('p');
const input = document.getElementById('input');
input.addEventListener('keyup', function(e) {
demo.text = e.target.value;
});
demo.$watch('text', str => p.innerHTML = str);
注:Dep
是 dependence 的缩写,中文就是 “依赖” 的意思。因为 Watcher
订阅者需要依赖 Dep
才能了解数据的变化,没有 Dep
,Watcher
根本不可能知道数据发生了变化,当有数据变化发生时,Dep
会通知 Watcher
,Dep
相当于是杂志社,Watcher
作为订阅者,首先需要向杂志社订阅杂志,这样当有新的杂志(消息)产生时,Dep
才会通知 Watcher
,所以 Watcher
强烈依赖 Dep
,他们之间是这样的一种关系。
总结
其实完成双向绑定的本质就对组件中 state
的 data
数据进行深度 watch
,而完成深度 watch
要使用的解决方案可以是使用 ES5 中的 Object.defineProperty
递归遍历 data
对象数据进行深度 watch
,也可以使用 ES6 的Proxy 结合
Reflect 进行同样的处理。
D3.js貌似在画2D图时很厉害,在3D图没有多少例子。
呵呵,那可是svg界的jQuery,当它是一个svg和canvas库来用就好