什么是双向数据绑定
双向数据绑定简单来说就是 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 https://developer.mozilla.org/zh-CN/docs/web/JavaScript/Reference/Global_Objects/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 构造函数接收应用根元素, 数据, 方法来初始化双向数据绑定:
- function EBind(options) {
- this._init(options);
- }
- 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 中的更新方法.
- 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 元素中.
- 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 实现对视图的更新.
- function Watcher(options) {
- // options 属性:
- // name 指令名称, 例如文本节点, 该值设为 "text"
- // el 指令对应的 DOM 元素
- // eb 指令所属 EBind 实例
- // exp 指令对应的值, 本例如 "number"
- // attr 绑定的属性值, 本例为 "innerHTML"
- 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
来源: https://www.cnblogs.com/laixiangran/p/8922301.html