摘要:什么是双向数据绑定是一个框架,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化。
什么是双向数据绑定?Vue是一个MVVM框架,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化。
实现数据绑定的方式大致有以下几种:
- 1、发布者-订阅者模式(backbone.js) - 2、脏值检查(angular.js) - 3、数据劫持(vue.js)发布者-订阅者模式
一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set("property", value),有兴趣可参考这里
我们更希望可以通过 vm.property = value 这种方式进行数据更新,同时自动更新视图。脏值检查
angular是通过脏值检查方式来对比数据是否变化,来决定是否更新视图,最常见的方式是通过setInterval()来监测数据变化,当然,只会在某些指定事件触发时下才进行脏值检查。大致如下:
- DOM事件,譬如用户输入文本,点击按钮等。( ng-click ) - XHR响应事件 ( $http ) - 浏览器Location变更事件 ( $location ) - Timer事件( $timeout , $interval ) - 执行 $digest() 或 $apply()数据劫持
Vue.js则是通过数据劫持以及结合发布者-订阅者来实现的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。
一、实现最基础的数据绑定输入的值为:二、双向数据绑定实现(此处用MVue替代)
上面的只是简单的使用了Object.defineProperty(),并不是我们最终想要的效果,最终想要的效果如下:
输入的值为:{{text}}
实现思路:
1、输入框以及文本节点和data中的数据进行绑定
2、输入框内容变化时,data中的对应数据同步变化,即 view => model
3、data中数据变化时,对应的文本节点内容同步变化 即 model => view
上述流程如图所示:
1、实现一个数据监听器Obverser,对data中的数据进行监听,若有变化,通知相应的订阅者。
2、实现一个指令解析器Compile,对于每个元素上的指令进行解析,根据指令替换数据,更新视图。
3、实现一个Watcher,用来连接Obverser和Compile, 并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。
4、构造函数 (new MVue({}))
在初始化MVue实例时,对data中每个属性劫持监听,同时进行模板编译,指令解析,最后挂载到相应的DOM中。
function MVue (options) { this.$el = options.el; this.$data = options.data; // 初始化操作,后面会说 // ... }
1、实现 view => model
DocumentFragment(文档片段)vue进行编译时,将挂载目标的所有子节点劫持到DocumentFragment中,经过一份解析等处理后,再将DocumentFragment整体挂载到目标节点上。
function nodeToFragment (node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); if (child.firstChild) { var dom = nodeToFragment(child, vm); child.appendChild(dom); } flag.appendChild(child); } return flag; }模板编译(指令解析,事件绑定、初始化数据绑定)
编译过程图
代码如下:
function compile (node, vm) { let reg = /{{(.*)}}/; // 元素节点 if (node.nodeType === 1) { var attrs = node.attributes; for (let attr of attrs) { if (attr.nodeName === "v-model") { // 获取v-model指令绑定的data属性 var name = attr.nodeValue; // 绑定事件 node.addEventListener("input", function(e) { vm.$data[name] = e.target.value; }) // 初始化数据绑定 node.value = vm.$data[name]; // 移除v-model 属性 node.removeAttribute("v-model") } } } // 文本节点 if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1 && (RegExp.$1.trim()); // 绑定数据到文本节点中 node.nodeValue = node.nodeValue.replace(new RegExp("{{s*(" + name + ")s*}}"), vm.$data[name]); } } }
现在,我们修改下MVue构造函数,增加模板编译,如下:
function MVue (options) { this.$el = options.el; this.$data = options.data; // 模板编译 let elem = document.querySelector(this.$el); elem.appendChild(nodeToFragment(elem, this)) }
那么,我们的view => model 已经实现了,包括初始化绑定默认值,只要修改了input中的值,data中对应的值相应变化,并触发了setter, 更新属性值等(可以自行在set方法中打印看效果,或者在控制台手动输入vm.$data.text也会看到效果)。
2、实现 model => view
上面可以看出,虽然我们实现了初始化数据绑定,以及输入框变化时,data中text也会变化,但是文本节点仍然没有任何变化,那么如果做到文本节点也同步变化呢,这里用的是发布者-订阅者模式。
发布者-订阅者模式又称为观察者模式,让多个观察者同时监听某个主题对象,当主题对象发生变化时,会通知所有的观察者对象,即:发布者发出通知给主题对象 => 主题对象接收到通知后推送给所有订阅者 => 订阅者执行相应的操作。
1)首先,定义一个主题对象,用来收集所有的订阅者,并提供notify方法,用来调用订阅者的update方法,从而执行相应的操作。
function Dep () { this.subs = []; } Dep.prototype = { addSub (sub) { this.subs.push(sub); }, notify () { this.subs.forEach(sub => { // 执行订阅者的update方法 sub.update(); }) } }
不难看出,当text属性变化时,会触发set方法,作为发布者,将数据更新消息通过主题对象发送给订阅者, 那么该如何通知呢?
我们知道,在new一个vue时,会执行两个操作,一个事编译模板,一个监听data数据,在监听data时,vue为data的每个属性都生成一个主题对象Dep,而在编译模板时,会为每个与数据绑定的节点生成一个Watcher,那么只要关联了Dep与Watcher,是不是就实现了消息通知呢,关键逻辑是实现二者关联。
已实现:输入框变化 => 触发相应的事件,修改值 => 触发set方法
需要实现:发出通知dep.notify() => 触发订阅者update方法 => 更新视图
我们修改下compile中文本节点内容(只修改部分)
// 文本节点 if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1 && (RegExp.$1.trim()); // 绑定数据到文本节点中 // node.nodeValue = node.nodeValue.replace(new RegExp("{{s*(" + name + ")s*}}"), vm.$data[name]); new Watcher(vm, node, name); } }
2)其次、实现订阅者Watcher
function Watcher (vm, node, name) { // 全局的、唯一 Dep.target = this; this.node = node; this.name = name; this.vm = vm; this.index = index; this.update(); Dep.target = null; } Watcher.prototype = { update () { this.get(); this.node.nodeValue = this.value; }, get () { this.value = this.vm.$data[this.name] } }
首先,定义了一个全局的Dep.target,然后执行了update方法,进而执行了get方法,都去了this.vm的访问器属性, 从而将订阅的消息保存在该属性的主题对象中,并最终将Dep.target设置为空,全局变量,是watcher和dep之间的唯一桥梁,必须保证Dep.target只有一个值。
3)接着、实现一个obverser给data中每个属性添加一个主题对象
遍历data中的所有属性,包括子属性对象的属性
function obverser (obj) { Object.keys(obj).forEach(key => { if (obj.hasOwnProperty(key)) { if (obj[key].constructor === "Object") { obverser(obj[key]) } defineReactive(obj, key); } }) }
使用Object.definePeoperty()来监听属性变动,给属性添加setter和getter
function defineReactive (obj, key) { var _value= obj[key]; // new一个主题对象 var dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, set (newVal) { if (_value= newVal) { return; } _value= newVal; console.log(value) // 作为发布者发出通知给主题对象 dep.notify(); }, get () { // 如果订阅者存在,添加到主题对象中 if (Dep.target) { dep.addSub(Dep.target); } return _value } }) }
最后,我们需要再次修改构造函数MVue
function MVue (options) { this.$el = options.el; this.$data = options.data; // 数据监听 obverser(this.$data); // 模板编译 let elem = document.querySelector(this.$el); elem.appendChild(nodeToFragment(elem, this)) }
现在,已经实现了model => view的变化
当输入框值变化时 => text也会变化 => 文本节点值变化
但如果细心的话,会发现还有一个问题,当我们手动改变text的值时(如在控制台上输入vm.$data.text = "xxx"),会发现,文本节点值已经变化了,但是输入框的值没有变化。
如果给输入框也添加一个Watcher,是不是也就和文本节点一样实现了呢,但需要注意的是,输入框、文本框、下拉框等,是通过value改变值的,而不是nodeValuefa,因为可以做如下修改:
compile中:
// 初始化数据绑定 // node.value = vm.$data[name]; new Watcher(vm, node, name); // 移除v-model 属性 node.removeAttribute("v-model")
wather中:
Watcher.prototype = { update () { this.get(); let _name; if (this.index === 1) { _name = this.name; } else { _name = this.value; } if (this.node.nodeName === "INPUT") { // 可以添加TEXTAREA、SELECT等 this.node.value = this.value; } else { // this.node.nodeValue = this.value; this.node.nodeValue = this.node.nodeValue.replace(new RegExp("{?{?s*(" + _name + ")s*}?}?"), this.value); } ++this.index; }, get () { this.value = this.vm.$data[this.name] } }
OK,基本上完工。
获取完整代码,猛戳这里
个人博客也可以获取完整代码(https://jefferye.github.io)
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/94618.html
摘要:关于双向数据绑定当我们在前端开发中采用的模式时,,指的是模型,也就是数据,,指的是视图,也就是页面展现的部分。参考沉思录一数据绑定双向数据绑定实现数据与视图的绑定与同步,最终体现在对数据的读写处理过程中,也就是定义的数据函数中。 关于双向数据绑定 当我们在前端开发中采用MV*的模式时,M - model,指的是模型,也就是数据,V - view,指的是视图,也就是页面展现的部分。通常,...
摘要:双向数据绑定指的是,将对象属性变化与视图的变化相互绑定。数据双向绑定已经了解到是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过来实现对属性的劫持,达到监听数据变动的目的。和允许观察数据的更改并触发更新。 1 MVVM 双向数据绑定指的是,将对象属性变化与视图的变化相互绑定。换句话说,如果有一个拥有name属性的user对象,与元素的内容绑定,当给user.name赋予一个新...
摘要:执行的时候,会绑定上下文对象为组件实例于是中的就能取到组件实例本身,的代码块顶层作用域就绑定为了组件实例于是内部变量的访问,就会首先访问到组件实例上。其中的获取,就会先从组件实例上获取,相当于。 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得...
摘要:储存订阅器因为属性被监听,这一步会执行监听器里的方法这一步我们把也给弄了出来,到这一步我们已经实现了一个简单的双向绑定了,我们可以尝试把两者结合起来看下效果。总结本文主要是对双向绑定原理的学习与实现。 当今前端天下以 Angular、React、vue 三足鼎立的局面,你不选择一个阵营基本上无法立足于前端,甚至是两个或者三个阵营都要选择,大势所趋。 所以我们要时刻保持好奇心,拥抱变化,...
1:vue 双向数据绑定的原理: Object.defineProperty是ES5新增的一个API,其作用是给对象的属性增加更多的控制Object.defineProperty(obj, prop, descriptor)参数 obj: 需要定义属性的对象(目标对象)prop: 需被定义或修改的属性名(对象上的属性或者方法)对于setter和getter,我的理解是它们是一对勾子(hook...
阅读 1336·2021-11-24 10:20
阅读 3613·2021-11-24 09:38
阅读 2269·2021-09-27 13:37
阅读 2171·2021-09-22 15:25
阅读 2226·2021-09-01 18:33
阅读 3473·2019-08-30 15:55
阅读 1761·2019-08-30 15:54
阅读 2057·2019-08-30 12:50