前言
双向绑定是Vue这类MVVM框架最基础的功能,Vue使用了劫持Object.defineProperty属性的方法去实现,本文将用一个简单版的双向绑定去解读Vue的源码。
简单版的源码放在我的仓库,目录:/example/双向绑定
解析
Object.defineProperty劫持
根据传入对象,属性,对get和set方法劫持。当
- 进行数据取值,收集依赖(dep.depend())
- 进行数据赋值,触发更新(dep.notify())
下面会详细讲解dep
function defineReactive(obj, key, val) {
let dep = new Dep();
dep.obj = obj;
dep.key = key;
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
}
return value;
},
set: function(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
dep.notify()
}
});
}
Dep和Watcher
Vue为了实现双向绑定,其中有两个类扮演什么重要的角色:
- Dep
Dep负责管理Vue实例里每个属性的依赖关系。方法depend负责收集依赖,把目标节点的渲染所依赖的属性关联起来;方法notify负责通知依赖,当某个属性变更,通知该属性关联的所有视图执行更新
class Dep {
subs = [];
notify() {
this.subs.forEach((sub) => {
sub.update(this.obj, this.key);
});
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub (sub) {
this.subs.push(sub)
}
}
Dep.target = undefined;
- Watcher
addDep方法用于把Wacher实例watcher和Dep实例dep关联起来,使得dep可以通过notify()通知对应的watcher更新视图;
每当Watcher初始化的时候,主要做了两个事:
定义update方法,update方法用于更新视图。
触发get方法,把Dep.target设置为undefined,结束掉依赖的收集
class Watcher {
node = null;
constructor(getTpl, parentNode, vm) {
this.get(this);
this.node = document.createElement('div');
parentNode.append(this.node);
this.update = (obj, key) => {
const node = parseDom(getTpl.call(vm));
node.addEventListener('change', (event) => {
debugger
obj[key] = event.target.value;
})
parentNode.replaceChild(node, this.node);
this.node = node;
}
this.update();
this.get();
}
get(target) {
Dep.target = target;
}
update() {
}
addDep(dep) {
dep.addSub(this);
}
}
这里有点需要强调的是收集依赖的depend函数。简单版里面执行过程比较简单,而Vue里面实际是很绕的
// watcher.js
Watcher {
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
}
Dep {
depend() {
Dep.target.addDep(this);
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
Dep.target是一个全局对象,在每个Watcher实例的构造函数里开始时设置,结束时删除。当触发了Wacher的构造函数,
- 会先设置Dep.target
- 执行depend,此时会把调用depend的对象dep作为参数,执行Dep.target.addDep。然后该方法内部判断depId是否重复,假如重复,就放弃这个依赖
渲染时创建Watcher
上面提到了,Wacher构造函数会收集依赖,那么什么时候执行Watcher的构造函数的?在视图的渲染的时候,即render函数。(无论我们写的是Template,还是函数,Vue最终都会转化为render函数),而render在Vue实例创造的时候执行
class Vue {
rootNode = null;
data = {};
constructor(params) {
if (params.el) {
this.rootNode = document.querySelector(params.el);
}
if (params.data) {
this.data = params.data();
Object.keys(this.data).forEach(key => {
defineReactive(this.data, key, this.data[key]);
});
}
this.render(params.render);
return this;
}
render(getTpl) {
const watcher = new Watcher(getTpl, this.rootNode, this);
watcherList.push(watcher);
}
}
前言
双向绑定是Vue这类MVVM框架最基础的功能,Vue使用了劫持Object.defineProperty属性的方法去实现,本文将用一个简单版的双向绑定去解读Vue的源码。
简单版的源码放在我的仓库,目录:/example/双向绑定
解析
Object.defineProperty劫持
根据传入对象,属性,对get和set方法劫持。当
下面会详细讲解dep
Dep和Watcher
Vue为了实现双向绑定,其中有两个类扮演什么重要的角色:
Dep负责管理Vue实例里每个属性的依赖关系。方法depend负责收集依赖,把目标节点的渲染所依赖的属性关联起来;方法notify负责通知依赖,当某个属性变更,通知该属性关联的所有视图执行更新
addDep方法用于把Wacher实例watcher和Dep实例dep关联起来,使得dep可以通过notify()通知对应的watcher更新视图;
每当Watcher初始化的时候,主要做了两个事:
定义update方法,update方法用于更新视图。
触发get方法,把Dep.target设置为undefined,结束掉依赖的收集
这里有点需要强调的是收集依赖的depend函数。简单版里面执行过程比较简单,而Vue里面实际是很绕的
Dep.target是一个全局对象,在每个Watcher实例的构造函数里开始时设置,结束时删除。当触发了Wacher的构造函数,
渲染时创建Watcher
上面提到了,Wacher构造函数会收集依赖,那么什么时候执行Watcher的构造函数的?在视图的渲染的时候,即render函数。(无论我们写的是Template,还是函数,Vue最终都会转化为render函数),而render在Vue实例创造的时候执行