什么是双向绑定
把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) {//文本节点类型是3let 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;//当前vuethis.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 对数据代理。