一、什么是双向绑定?
1、前景知识:
(1)单向绑定(MVC模式 Model View Controller):
主要由
- Model(模型):用于封装与应用程序业务逻辑相关的数据以及对数据的处理方法。Model 有对数据直接访问的权力,例如对数据库的访问。
- View(视图) :视图代表模型包含的数据的可视化。
- Controller(控制器):是用户与系统之间的纽带,它接受用户输入,并指示模型和视图基于用户输入执行操作(处理数据、展示数据)
主要实现过程:
控制器接收到视图层的用户请求和操作,然后调用相应的模型针对用户需求进行业务处理,并返回数据给控制器。控制器再调用相应的视图来显示处理的结果,并通过视图呈现给用户。
(2)MVVM模式
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
View 和 Model 之间其实并没有直接的联系,而是通过ViewModel进行交互,它能够监听到数据的变化,然后通知视图进行自动更新,而当用户操作视图时,VM也能监听到视图的变化,然后通知数据做相应改动,这实际上就实现了数据的双向绑定。
2、双向绑定实现原理
Vue 框架是一个典型的 MVVM模式的框架,即数据的双向绑定。主要是实现以下两个过程:
- 视图变化如何更新数据
- 数据变化如何更新视图
(1) 视图变化如何更新数据
通过事件监听的方式实现对视图变化的监测
(2) 数据变化如何更新视图
- 监听器 Observer 用来实现对数据进行监测 new Vue()首先执行初始化,对data执行响应化处理,通过Object.defineProperty方法中的get() 和 set() 实现对数据的监测
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
//添加订阅者watcher到主题对象Dep
if(Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if(newVal === val) return;
val = newVal;
console.log(val);
// 作为发布者发出通知
dep.notify();//通知后dep会循环调用各自的update方法更新视图
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
2.同时指令解析器 Compile扫描和解析每个节点的相关指令,初始化模板数据
Compile对每个节点元素进行扫描和解析,将相关指令通过节点类型对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
function Compile(node, vm) {
if(node) {
this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
console.log([child])
self.compileElement(child, vm);
frag.append(child); // 将所有子节点添加到fragment中
}
return frag;
},
compileElement: function(node, vm) {
var reg = /\{\{(.*)\}\}/;
//节点类型为元素(input元素这里)
if(node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for(var i = 0; i < attr.length; i++ ) {
if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法
vm[name]= e.target.value;
});
new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
}
};
}
//节点类型为text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'nodeValue');
}
}
}
}
3.订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图
class Watcher {
constructor(vm,exp,cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
},
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
},
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
- vm:一个Vue的实例对象;
- exp:是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
- cb:是Watcher绑定的更新函数
这里有个疑问就是wacther如何装入到对应的dep中?
当对应的wacther初始化时会触发wacther中的get(),然后再Dep.target = this缓存当前的数据key通过获取值的方式强行触发对应数据的get()方法触发dep.depend()将自己装入dep中。
4.使用Dep收集对应数据属性的所有依赖watcher
class Dep {
constructor(){
this.subs = []
},
//增加订阅者
addSub(sub){
this.subs.push(sub);
},
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
},
//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
Dep.target = null;
3、总结
- new Vue() 初始化 在监听器 Observer 中通过Object.defineProperty方法,实现对data中数据的可监测化。
- 定义更新函数和Watcher
- 同时使用解析器Compiler对模板执行编译,通过对每个元素节点的指令扫描和解析,将指令模版置换成对应的数据和初始化对应的watcher与更新函数
- 使用Dep依赖收集器,将同个视图中对应某个数据属性所有依赖watcher收集在对应某个数据属性的Dep中
- 当数据发生变化后会触发该数据的set()以及里面的dep.notify(),通过dep.notify()遍历触发dep里的所有watcher的update()实现数据的更新。
差不多了解的就这些啦,如果理解有错欢迎纠正!!!
参考文献:
[1]((31条消息) 通俗易懂了解Vue双向绑定原理及实现_liuyuanyan的博客-CSDN博客_vue数据双向绑定原理 通俗易懂)