JavaScript实现简单的双向数据绑定
什么是双向数据绑定
双向数据绑定简单来说就是UI视图(View)与数据(Model)相互绑定在一起,当数据改变之后相应的UI视图也同步改变。反之,当UI视图改变之后相应的数据也同步改变。
双向数据绑定最常见的应用场景就是表单输入和提交。一般情况下,表单中各个字段都对应着某个对象的属性,这样当我们在表单输入数据的时候相应的就改变对应的对象属性值,反之对象属性值改变之后也反映到表单中。
目前流行的 MVVM 框架(Angular、Vue)都实现了双向数据绑定,这样也就实现了视图层和数据层的分离。相信使用过 jQuery 的人都知道,往往我们在获取到数据之后就直接操作 DOM ,这样数据操作和 DOM 操作就高度耦合在一起了。
实现方式
发布者-订阅者模式
这种实现方式就是使用自定义的 data 属性在 HTML 代码中指明绑定。所有绑定起来的 JavaScript 对象以及 DOM 元素都将 “订阅” 一个发布者对象。任何时候如果 JavaScript 对象或者一个 HTML 输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。具体实现可看这篇文章:http://www.html-js.com/article/Study-of-twoway-data-binding-JavaScript-talk-about-JavaScript-every-day
脏值检查
Angularjs(这里特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)双向数据绑定的技术实现是脏值检查。原理就是:Angularjs内部会维护一个序列,将所有需要监控的属性放在这个序列中,当发生某些特定事件时(并不是定时的而是由某些特殊事件触发的,比如:DOM事件、XHR事件等等),Angularjs会调用 $digest 方法,这个方法内部做的逻辑就是遍历所有的 watcher,对被监控的属性做对比,对比其在方法调用前后属性值有没有发生变化,如果发生变化,则调用对应的 handler。
这种方式的缺点很明显,遍历轮训 watcher 是非常消耗性能的,特别是当单页的监控数量达到一个数量级的时候。
访问器监听
vue.js 实现数据双向绑定的原理就是访问器监听。它使用了 ECMAScript5.1(ECMA-262)中定义的标准属性 Object.defineProperty 方法。通过 Object.defineProperty 设置各个属性的 setter,getter,在数据变动时更新UI视图。
实现
本文将采用 访问器监听
这种方式来实现一个简单的双向数据绑定,主要实现:
- **_obverse**:对数据进行处理,重写相应的 set 和 get 函数
- **_complie**:解析指令(e-bind、e-model、e-click)等,并在这个过程中对 view 与 model 进行绑定
- Watcher:作为连接 _obverse 和 _complie 的桥梁,用来绑定更新函数,实现对视图的更新
首先看下我们的视图代码:
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta name="author" content="赖祥燃, laixiangran@163.com, http://www.laixiangran.cn"/>
<title>实现简单的双向数据绑定</title>
<style>
#app {
text-align: center;
}
</style>
<script src="eBind.js"></script>
<script>
window.onload = function () {
new EBind({
el: '#app',
data: {
number: 0,
person: {
age: 0
}
},
methods: {
increment: function () {
this.number++;
},
addAge: function () {
this.person.age++;
}
}
});
};
</script>
</head>
<body>
<div id="app">
<form>
<input type="text" e-model="number">
<button type="button" e-click="increment">增加</button>
</form>
<h3 e-bind="number"></h3>
<form>
<input type="text" e-model="person.age">
<button type="button" e-click="addAge">增加</button>
</form>
<h3 e-bind="person.age"></h3>
</div>
</body>
从视图代码可以看出,在 <div id="app">
的子元素中我们应用了三个自定义指令
e-bind
、e-model
、e-click
, 然后我们通过 new EBind({***})
应用双向数据绑定。
## 分析
### EBind
EBind
构造函数接收应用根元素、数据、方法来初始化双向数据绑定:
```javascript
/**
- EBind构造函数
- @param options
- @constructor
*/
function EBind(options) {
this._init(options);
}
/**
- 初始化构造函数
- @param options
@private
*/
EBind.prototype._init = function (options) {// options 为上面使用时传入的结构体,包括 el, data, methods
this.$options = options;// el 是 #app, this.$el 是 id 为 app 的 Element 元素
this.$el = document.querySelector(options.el);// this.$data = {number: 0}
this.$data = options.data;// this.$methods = {increment: function () { this.number++; }}
this.$methods = options.methods;// _binding 保存着 model 与 view 的映射关系,也就是我们定义的 Watcher 的实例。当 model 改变时,我们会触发其中的指令类更新,保证 view 也能实时更新
this._binding = {};// 重写 this.$data 的 set 和 get 方法
this._obverse(this.$data);// 解析指令
this._complie(this.$el);
};
```
### _obverse
_obverse
的关键是使用 Object.defineProperty 来定义传入数据对象的 getter 及 setter,通过 setter 来监听对象属性的变化从而触发 Watcher 中的更新方法。
```javascript
/**
- 对data进行处理,重写相应的set和get函数
- @param currentObj 当前对象
- @param completeKey
@private
*/
EBind.prototype._obverse = function (currentObj, completeKey) {
var _this = this;
Object.keys(currentObj).forEach(function (key) {
if (currentObj.hasOwnProperty(key)) {// 按照前面的数据,_binding = {number: _directives: [], preson: _directives: [], preson.age: _directives: []} var completeTempKey = completeKey ? completeKey + '.' + key : key; _this._binding[completeTempKey] = { _directives: [] }; var value = currentObj[key]; // 如果值还是对象,则遍历处理 if (typeof value === 'object') { _this._obverse(value, completeTempKey); } var binding = _this._binding[completeTempKey]; // 双向数据绑定的关键 Object.defineProperty(currentObj, key, { enumerable: true, configurable: true, get: function () { console.log(key + '获取' + JSON.stringify(value)); return value; }, set: function (newVal) { if (value !== newVal) { console.log(key + '更新' + JSON.stringify(newVal)); value = newVal; // 当 number 改变时,触发 _binding[number]._directives 中的绑定的 Watcher 类的更新 binding._directives.forEach(function (item) { item.update(); }); } } }); }
})
};
```
### _complie
_complie
的关键是简析自定义指令,根据不同的自定义指令实现不同的功能。如 e-click
就解析为将对应 node 绑定 onclick 事件,e-model
必须绑定在 INPUT 和 TEXTAREA 上,然后监听 input 事件,更改 model 的值,e-bind
就直接将绑定的变量值输出到DOM元素中。
```javascript
/**
- 解析指令(e-bind、e-model、e-click)等,并在这个过程中对 view 与 model 进行绑定
- @param root root 为 id 为 app 的 Element 元素,也就是我们的根元素
@private
*/
EBind.prototype._complie = function (root) {
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];// 对所有元素进行遍历,并进行处理 if (node.children.length) { this._complie(node); } // 如果有 e-click 属性,我们监听它的 onclick 事件,触发 increment 事件,即 number++ if (node.hasAttribute('e-click')) { node.onclick = (function () { var attrVal = node.getAttribute('e-click'); // bind 是使 data 的作用域与 method 函数的作用域保持一致 return _this.$methods[attrVal].bind(_this.$data); })(); } // 如果有 e-model 属性且元素是 INPUT 和 TEXTAREA,我们监听它的 input 事件,更改 model 的值 if (node.hasAttribute('e-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) { node.addEventListener('input', (function (index) { var attrVal = node.getAttribute('e-model'); // 添加指令类 Watcher _this._binding[attrVal]._directives.push(new Watcher({ name: 'input', el: node, eb: _this, exp: attrVal, attr: 'value' })); return function () { var keys = attrVal.split('.'); var lastKey = keys[keys.length - 1]; var model = keys.reduce(function (value, key) { if (typeof value[key] !== 'object') { return value; } return value[key]; }, _this.$data); model[lastKey] = nodes[index].value; } })(i)); } // 如果有 e-bind 属性 if (node.hasAttribute('e-bind')) { var attrVal = node.getAttribute('e-bind'); // 添加指令类 Watcher _this._binding[attrVal]._directives.push(new Watcher({ name: 'text', el: node, eb: _this, exp: attrVal, attr: 'innerHTML' })); }
}
};
```
Watcher
作为连接 _obverse 和 _complie 的桥梁,用来绑定更新函数,通过 update 实现对视图的更新。
/**
* 指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
* @param options Watcher 类属性:
* name 指令名称,例如文本节点,该值设为"text"
* el 指令对应的DOM元素
* eb 指令所属EBind实例
* exp 指令对应的值,本例如"number"
* attr 绑定的属性值,本例为"innerHTML"
* @constructor
*/
function Watcher(options) {
this.$options = options;
this.update();
}
/**
* 根据 model 更新 view
*/
Watcher.prototype.update = function () {
var _this = this;
var keys = this.$options.exp.split('.');
// 比如 H3.innerHTML = this.data.number; 当 number 改变时,会触发这个 update 函数,保证对应的 DOM 内容进行了更新。
this.$options.el[this.$options.attr] = keys.reduce(function (value, key) {
return value[key];
}, _this.$options.eb.$data);
};
总结
这样我们就使用原生 JavaScript 实现了简单的双向数据绑定。
源码:https://github.com/laixiangran/e-bind
JavaScript实现简单的双向数据绑定的更多相关文章
- 五十行javascript代码实现简单的双向数据绑定
五十行javascript代码实现简单的双向数据绑定 Vue框架想必从事前端开发的同学都使用过,它的双向数据绑定机制能给我们带来很大的方便.今天闲着没事,尝试着实现一下双向数据绑定,接下来给大家分享一 ...
- 利用ES6中的Proxy和Reflect 实现简单的双向数据绑定
利用ES6中的Proxy (代理) 和 Reflect 实现一个简单的双向数据绑定demo. 好像vue3也把 obj.defineProperty() 换成了Proxy+Reflect. 话不多说 ...
- JavaScript实现简单的双向绑定
很多的前端框架都支持数据双向绑定了,最近正好在看双向绑定的实现,就用Javascript写了几个简单的例子. 几个例子中尝试使用了下面的方式实现双向绑定: 发布/订阅模式 属性劫持 脏数据检测 发布/ ...
- React简单实现双向数据绑定
import React, { Component } from 'react' import ReactDOM from 'react-dom' class App extends Componen ...
- 自己手动实现简单的双向数据绑定 mvvm
数据绑定 数据绑定一般就是指的 将数据 展示到 视图上.目前前端的框架都是使用的mvvm模式实现双绑的.大体上有以下几种方式: 发布订阅 ng的脏检查 数据劫持 vue的话采用的是数据劫持和发布订阅相 ...
- 原生js简单实现双向数据绑定原理
根据对象的访问器属性去监听对象属性的变化,访问器属性不能直接在对象中设置,而必须通过 defineProperty() 方法单独定义. 访问器属性的"值"比较特殊,读取或设置访问器 ...
- 简单实现双向数据绑定mvvm。
- angularJs初体验,实现双向数据绑定!使用体会:比较爽
使用初体验:ng 双向数据绑定: 最简单的双向数据绑定:(使用默认模块控制) <body ng-app> <input type="text" ng-model= ...
- 深入vue源码,了解vue的双向数据绑定原理
大家都知道vue是一种MVVM开发模式,数据驱动视图的前端框架,并且内部已经实现了双向数据绑定,那么双向数据绑定是怎么实现的呢? 先手动撸一个最最最简单的双向数据绑定 <div> < ...
随机推荐
- sublime的使用技巧
ctr+shift+d是复制当前行当下一行2.使用Sublime text 3 编写代码是一种享受,使用Sublime text 3 格式化HTML代码,需要安装插件,具体安装步骤如下:1.打开菜单- ...
- ssh框架-Struts2(二)
上篇文章我们了解了怎么配置struts.xml文件,以及前端控制器配置怎么配置,,Action进阶,Result结果配置,Struts2中的Servlet的API的访问,以及怎么获得请求参数.今天我们 ...
- emqtt 试用(二)验证 emq 和 mosquito 的共享订阅
本地订阅(Local Subscription) 本地订阅(Local Subscription)只在本节点创建订阅与路由表,不会在集群节点间广播全局路由,非常适合物联网数据采集应用. 使用方式: 订 ...
- redis入门(14)redis集群下的数据分区存储
redis入门(10)redis集群下的数据分区存储
- Docker学习笔记 - Docker的镜像
一个容器实际上是运行在宿主机上的一个进程. 只不过在启动这个进程之前进行了一些特殊处理,让这个容器进入了一个全新的虚拟环境,与宿主机的环境分开, 所以这个进程及其子进程认为自己运行在一个独立的世界里面 ...
- testNG常用方法
1.常用注释: 注解 描述 @BeforeSuite 在该套件的所有测试都运行在注释的方法之前,仅运行一次. @After ...
- 深入理解JavaScript的this指向问题
Javascript的this用法 this是Javascript语言的一个关键字.它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用.比如: function test(){ this.x ...
- JavaScript 克隆
JavaScript 克隆 本次学习内容: 克隆:只克隆标签和属性,不克隆文本. 克隆的功能,如果不添加使用Ture,就只会克隆标签和属性,不会克隆文本. 克隆的参数全部是节点对象,不能是字符串 &l ...
- spark2.1操作json(save/read)
建筑物配置信息: case class BuildingConfig(buildingid: String, building_height: Long, gridcount: Long, gis_d ...
- ST-LINK V2 DIY笔记(一)
最近一段时间调试STM32板子的时候,都是用JLINK+杜邦线,或者拿官方板子当STLINK用,可以用,但是体积比较大,有时候觉得比较麻烦.正好前一阵手头项目少,就想DIY一个STLINK. 图是网上 ...