vue源码学习(4):数据双向绑定的基本实现
Copyes opened this issue · comments
背景
之前已经把数据的单向绑定简单的实现了,能够直接将实例中data中的对应属性绑定到对应的视图中名字一样的属性。但是这个也只是一个简单的数据绑定。那么我们能不能够在input框里面输入的时候自动去更新视图里面对应绑定的属性呢?
答案是肯定,先看一张图:
这张图是网上找的一个双向绑定的基本流程图。
流程解析
首先我们要有一个方法来监听属性的变化。这个方法去递归便利监听每一个属性。在前面的文章中(1)、(2)中有提到如何去监听属性的变化。对应本文的代码是如下:
const observer = (data, vm) => {
// 遍历劫持data下面的所有的属性
Object.keys(data).forEach((key) => {
defineReactive(vm, key, data[key]);
});
}
// 属性劫持封装
const defineReactive = (vm, key, val) => {
// 新建通知者
var dep = new Dep();
// 利用setter 和 getter 访问器来对属性的值监听
Object.defineProperty(vm, key, {
get: () => {
console.log('被访问了');
if(Dep.target){
console.log(val);
dep.addSub(Dep.target);
}
return val;
},
set: (newVal) => {
console.log('被设置了');
if(val === newVal){
return;
}
val = newVal;
// 新的值要赋值给原来实例中data对应的属性
vm.data[key] = val;
// 通知订阅者,我们有数据改变了。
dep.notify();
}
});
}
以上代码大概就是对属性监听的视线,但是里面有使用到Dep(); 这个Dep()又是什么呢?
暂时我们就叫它通知者吧,他的作用是什么呢?简单点来说就是为了通知订阅了data中的属性的地方,我们属性的值发生了改变了。你要做好接下来的操作哦。
通知者的代码实现:
function Dep(){
// 搜集所有订阅了某个属性的订阅者
this.subs = [];
}
Dep.prototype = {
// 添加有用到属性的节点进图观察者队列中
addSub(watcher){
this.subs.push(watcher);
},
// 通知所有的观察者,使相应的数据节点去更新view层的值,model => view;
notify(){
this.subs.forEach((watcher) => {
// 每个观察者对应的更新操作。
watcher.update();
});
}
}
有了一个类似于通知中心的地方后我们在来看看放在通知中心中的那些订阅者watcher是什么。
代码如下:
// 订阅者(为每个节点的数据建立watcher 队列,每次接受更改数据需求哈后,利用数据劫持执行对应的节点的数据更新操作)
function Watcher(vm, node, name){
// 类似一个全局变量吧,用来临时保存下watcher
Dep.target = this;
this.vm = vm;
// 订阅的节点
this.node = node;
// 节点订阅的属性
this.name = name;
this.update();
// 为保证只有一个全局watcher,添加到队列后,清空全局watcher
Dep.target = null;
}
Watcher.prototype = {
// 在通知中心里面遍历的观察者的方法;
update(){
this.get();
if(this.node.nodeName === 'INPUT'){
this.node.value = this.value;
}else{
this.node.nodeValue = this.value;
}
},
// 订阅者拿到最新的属性值,是属性劫持getter拿到的值
get(){
this.value = this.vm[this.name];
}
}
观察车就是做了上面的那些事,但是这些观察者是在哪里被new 出来的呢?接下来看下面的关键步骤:
// 初始化绑定数据
const compile = (node, vm) => {
console.log(node);
// node为元素节点的时候
if(node.nodeType === 1){
// 获取处元素节点上所有属性主要是为了获得v-model
var attrs = node.attributes;
for(let i = 0; i < attrs.length; ++i){
if(attrs[i].nodeName === 'v-model'){
var name = attrs[i].nodeValue;
// 对input绑定了一个keyup事件,没次输入的操作都把新值赋值给对应的属性,
// 因为之前是把这些属性劫持了,所有有新值改变的时候都会触发通知者的notify方法
if(node.nodeName === 'INPUT'){
node.addEventListener('keyup', (e) => {
vm[name] = e.target.value;
console.log(vm[name]);
console.log(vm);
});
}
node.value = vm[name];
node.removeAttribute(attrs[i].nodeName);
}
}
}
// 文本节点
if(node.nodeType === 3){
console.log(node.nodeValue);
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)){
// 这个就是为了去除表达式两边的空格
var name = RegExp.$1.trim();
//node.nodeValue = vm.data[name];
// 这个地方就是添加的订阅者,就是所有的文本节点中订阅了相关data的属性的地方
new Watcher(vm, node, name);
}
}
}
上面就是关键的一步啦。一些其它的代码就没写上来了。完成了了这些操作后就可以实现双向绑定了。
详细代码请看
上面的图片显示不出来
你这两段里,watcher的update的判断是多余的吧,因为你在node.nodeType===3的时候生成的new watcher,都是文本节点你要判断啥nodeName === 'INPUT'。还有,我没有搞懂Dep.target究竟干啥用,作者你能说明白么
Watcher.prototype = {
update(){
this.get();
if(this.node.nodeName === 'INPUT'){
this.node.value = this.value;
}else{
this.node.nodeValue = this.value;
}
}
}
//.............
// 文本节点
if(node.nodeType === 3){
console.log(node.nodeValue);
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)){
// 这个就是为了去除表达式两边的空格
var name = RegExp.$1.trim();
//node.nodeValue = vm.data[name];
// 这个地方就是添加的订阅者,就是所有的文本节点中订阅了相关data的属性的地方
new Watcher(vm, node, name);
}
}
你这两段里,watcher的update的判断是多余的吧,因为你在node.nodeType===3的时候生成的new watcher,都是文本节点你要判断啥nodeName === 'INPUT'。还有,我没有搞懂Dep.target究竟干啥用,作者你能说明白么
上面的问题
(1)是为了在代码里面动态改变数据的时候也会被反绑到input上面去。你就想象下回填input框里面的值,我这里没写好。
(2)你就理解成全局变量就好了。由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除。这里也没有写好。后期我会重新写一个详细版。
最后,这个版本只是一个基础版,便于简单理解双向绑定,双向绑定的真正设计**根本不会这样,这样的性能非常差。