什么是双向绑定
把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定
如:
当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定关系图如下
vue2双向绑定原理:
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
ViewModel
主要职责:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(Observer):对所有数据的属性进行监听
解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。通过Object.defineProperty()来实现数据劫持的。
流程图如下:
- new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中
- 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
- 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
- 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
1.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。(link:v-model)
依赖收集:
2.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
(Object.defineProperty(),该方法有get()和set()方法,set方法,set方法可以劫持到具体更新的那个数据,从而在所有订阅者中找出现在要更新的某一个订阅者
3.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数(watcher),从而更新视图。
class Vue {
constructor(options) {
//options.beforeCreate.call(this) //生命周期:创建前
this.obj = {};
this.$data = options.data; //$data接收数据
this.proxyData(); //将this.$data数据暴露给vue
//双向绑定第一步:劫持并监听所有属性
this.observer()
//options.created.bind(this)() ///生命周期:创建后
this.$el = document.querySelector(options.el); //绑定,挂载vue根节点
//options.beforeMount.bind(this)() //生命周期:挂载前
this.compile(this.$el);//负责模板解析编译,初始化 ==》view [视图]
//options.mounted.bind(this)() //生命周期:挂载后
console.log(this)
}
proxyData() {
Object.keys(this.$data).forEach(key => {
//this,当前的,需要暴露
Object.defineProperty(this, key, {
get() {
//第一次给值,触发改变,那么给监听$data的值就行
return this.$data[key];
},
set() {
}
})
})
}
observer() {
//监听所有$data的属性
let vueThis = this;
Object.keys(this.$data).forEach(key => {
let item = this.$data[key];
Object.defineProperty(this.$data, key, {
get() {
return item;
},
set(val) {
//当该属性发生变化,this.obj[key]就是那个修改了变化
//那一个发生变化,触发这个函数
//双向绑定第三步:从所有订阅者中,筛选出要更新的订阅者。
item = val;//数据发生改变值,再给get,允许改变
// new Watcher(this, key, nodes[i], "textContent");
vueThis.obj[key].forEach(itemWatcher => {
itemWatcher.update();
})
}
})
})
}
compile(parent) {
let nodes = parent.childNodes//获取所有的$el内的节点
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].nodeType == 1) {//元素节点类型是1
//判断元素节点上,有没有是双向绑定v-model.
if (nodes[i].hasAttribute("v-model")) {
let key = nodes[i].getAttribute("v-model");
nodes[i].value = this[key];//第一次放值
nodes[i].addEventListener('input', () => {
//改数据
this.$data[key] = nodes[i].value;
this[key] = nodes[i].value;
})
}
this.compile(nodes[i])//元素节点里面,可能也有订阅者
}
if (nodes[i].nodeType == 3) {//文本节点类型是3
let text = nodes[i].textContent;
let newText = text.replace(/{{(.*)}}/g, (firstStr, regStr) => {
/*
依赖收集:
视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知
双向绑定第二步:添加所有订阅者
所有的订阅者:obj格式为
{
str:[{},{},{}],//订阅者,在页面3次
a:[{}],
b:[{}]
}
*/
let key = regStr.trim(),
watcher = new Watcher(this, key, nodes[i], "textContent");
//判断订阅者,是否多次出现页面
if (this.obj[key]) {
//有订阅者
this.obj[key].push(watcher)
} else {
//没有订阅者
this.obj[key] = []
this.obj[key].push(watcher)
}
return this[key];
})
nodes[i].textContent = newText; // ==》view [视图]
}
}
}
beforeCreate() { }
created() { }
beforeMount() { }
mounted() { }
}
class Watcher {
// new Watcher(this, key, nodes[i], "textContent");
constructor(vm, key, node, attr) {
this.vm = vm;//当前vue
this.key = key;//vue哪个属性
this.node = node;//哪个节点
this.attr = attr;//对节点,哪个属性操作
}
update() {
//更新视图
// new Watcher(this, key, nodes[i], "textContent");
this.node[this.attr] = this.vm[this.key];
}
}
vue2 的双向数据绑定是利用ES5 的一个 API Object.definePropert()对数据进行劫持 结合 发布订阅模式的方式来实现的。
vue3 中使用了 es6 的 ProxyAPI 对数据代理。