分析准备
数据绑定
一旦data中某个属性更新,页面上对应使用了该属性的节点都会更新。
数据劫持
数据劫持的基本思想:给data中的所有属性提供set方法,set方法监视data属性中的变化,一旦发生变化就去更新界面。 在Vue中使用数据劫持来实现数据绑定。
数据绑定与数据代理
简单的的理解:
- 通过vm对象来代理data对象中所有属性的操作来实现数据代理。
- 通过给data中的所有属性提供set方法,set方法监视data属性中的变化(数据劫持)来实现数据绑定。
四个重要对象
Observer
- 用来对data所有属性数据进行劫持的构造函数
- 给data中所有属性重新定义属性描述(defineProperty),添加set/get方法。
- 为data中的每个属性创建对应的dep(dependency)对象
Dep
什么时候创建?
初始化时,给data属性进行数据劫持时创建。
Dep的个数?
与data中的属性一一对应,简单理解就是data中有多少个属性就有多少个Dep。
Dep的结构?
- id:标识(从0开始),每个dep都有一个唯一的id
- subs:包含n个对应watcher的数组
Compiler
- 用来解析模板页面的对象的构造函数(一个实例)
- 利用compile对象解析模板页面
- 每解析一个表达式(非事件指令)都会创建一个对应的watcher对象,并建立watcher与dep的关系
Watcher
什么时候创建?
初始化时,解析大括号表达式({{}})/一般指令(v-text)时创建。
Watcher的个数?
与模板中表达式(非事件指令)一一对应。
Watcher的结构?
function Watcher(vm, expOrFn, cb) {
this.cb = cb; // 更新界面的回调函数
this.vm = vm;
this.expOrFn = expOrFn; // 对应的表达式
this.depIds = {}; // 包含所有相关的dep的容器对象
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
// 得到表达式对应的value
this.value = this.get();
}
Dep与Watcher之间的关系
什么关系?
Dep与Watcher之间值多对多的关系(n:n)
- data属性–>Dep–>n个Watcher
<p>{{name}}</p>
<p v-text="name"></p>
- 表达式–>Watcher–>n个Dep
<p v-text="friend.name"></p>
什么时候建立?
初始化时,解析模板中的表达式创建watcher对象时
源码分析
实例
<div id="app">
<p>{{name}}</p>
<p v-text="name"></p>
<p v-text="friend.name"></p>
<button v-on:click="update">更新</button>
</div>
在进行源码分析之前,首先要弄清楚Dep和Watcher分别是什么以及它们俩之间的关系。
Dep看作属性,如上述代码,实例中有name,friend以及friend.name三个Dep,为了好理解分别编号为d0,d1,d2。Watcher看作表达式,第2,3,4行代码,一共有三个表达式,分别编号为w1,w2,w3。
<script type="text/javascript">
new MVVM({
el: "#app",
data: {
name: 'kk',
friend: {
name: 'cc',
age: 20
}
},
methods: {
update() {
this.name = 'xx'
}
}
})
</script>
分析
以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现
- 创建observer对象,开始对data的监视(walk()),遍历data中所有属性,对指定的属性实现响应式的数据绑定。
function Observer(data) {
this.data = data;
this.walk(data);
}
walk: function (data) {
// 保存observer对象
var me = this;
Object.keys(data).forEach(function (key) {
me.convert(key, data[key]);
});
},
convert: function (key, val) {
this.defineReactive(this.data, key, val);
},
- 创建属性对应的dep对象,对data中所有层次属性的数据劫持,给data重新定义属性,添加set/get方法,建立关系。
defineReactive: function (data, key, val) {
var dep = new Dep();
var childObj = observe(val);
// 给data重新定义属性,添加set/get方法
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
// 返回值,建立dep与watcher之间的关系
get: function () {
if (Dep.target) {
// 建立关系
dep.depend();
}
return val;
},
// 监视key属性的变化,更新界面
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知所有相关的订阅者
dep.notify();
}
});
}
};
- 在解析模板的过程中,为表达式创建对应的Watcher,指定更新函数
compile.js
// 为表达式创建一个对应的watcher,实现节点的更新显示
new Watcher(vm, exp, function(value, oldValue) {
//当表达式对应的一个属性值变化时回调
// 更新界面中的指定节点
updaterFn && updaterFn(node, value, oldValue);
});
- 建立Watcher到Dep的关系(添加订阅者),Dep中的subs用于保存n个Watcher。
watcher.js
addDep: function(dep) {
// 判断dep与watcher关系是否已经建立
if (!this.depIds.hasOwnProperty(dep.id)) {
// 将watcher添加到dep 用于更新
dep.addSub(this);
// 将dep添加到watcher中 用于防止重复建立关系
this.depIds[dep.id] = dep;
}
},
// 得表达式的值,建立dep与watcher的关系
get: function() {
// 给dep指定当前的watcher
Dep.target = this;
// 获取表达式的值,内部调用get建立dep与watcher的关系
var value = this.getter.call(this.vm, this.vm);
// 去除dep中指定的当前watcher
Dep.target = null;
return value;
},
以上均为初始化部分,综合分析前准备可以知道,此部分完成Dep、Watcher的创建以及两者关系的建立
当点击更新按钮时
- 负责劫持监听所有属性的Observer中的set方法会调用,把发生的变化遍历通知所有Dep(d0,d1,d2),先讲解d0对应w1和w2这一例子,后面的d1对应w3、d2对应w4操作相似。
- d0会依次遍历所有对应的watcher(w1,w2),通知watcher变化更新。
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
- watcher调用回调函数更新界面
run: function() {
var value = this.get();
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
总结
数据绑定中两个核心技术
- defineProperty
- 给data中所有属性重新定义属性描述,添加set/get方法。
- 消息订阅与发布
- 建立Watcher到Dep的关系,通知watcher变化更新。
v-model(双向数据绑定)
实例
<div id="app">
<input type="text" v-model="msg">
<p>{{msg}}</p>
</div>
new MVVM({
el: "#app",
data: {
msg: 'kk'
}
})
分析
以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现
v-model为普通指令,进行v-model的解析。
compile.js
model: function (node, vm, exp) {
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function (e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
如上述代码,第二行中bind方法做了两件事情:1. 解析表达式显示value;2. 创建对应的watcher。第三行保存当前的的compile,取得表达式所对应的值(‘这里为kk’)。添加对应的input监听(输入时发生)。
当输入框发生变化时,上述代码中第6行先获取输入框的值,判断输入框的值与原来是否相等,如果不相等则将最新的值保存到data上面,由前面数据绑定的知识可以知道,此时会导致data上的set方法调用,如下代码。后续即跟数据绑定是一样的,set方法的调用会通知dep,dep会通知对应的watcher(该实例中有两个watcher),最终实现界面的更新。
// 监视key属性的变化,更新界面
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知所有相关的订阅者
dep.notify();
}
小结
- 双向数据绑定是建立在数据绑定的基础上
- 双向数据绑定的实现流程
- 解析v-model指令,添加input监听
- input的value变化时,将最新的值赋值给当前表达式对应的data
- 进行数据绑定的操作(调用data上的set–>通知dep–>通知对应watcher–>updater)
一道面试题
vue双向数据绑定原理
答:Vue的双向数据绑定实现了View和Model的同步更新。它实际是建立在数据绑定的基础上。而数据绑定主要通过使用数据劫持和发布订阅者模式来实现的。
数据劫持就是通过Object.defineProperty()给data属性添加set方法,为data中的每个属性创建对应的dep对象。对v-model指令进行解析,添加input监听,为表达式创建对应的watcher, 建立Watcher到Dep的关系(添加订阅者)。当input的value发生变化时,将最新的值赋值给当前表达式所对应的data属性。由data更新,会导致data上的set方法调用,set调用会通知dep,dep会通知watcher,watcher收到通知后会更新视图(Updater)。
该篇为学习过程的学习笔记以及对面试题的简单理解。若有不足和错误,欢迎指正!