什么是双向数据绑定

双向数据绑定简单来说就是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-binde-modele-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实现简单的双向数据绑定的更多相关文章

  1. 五十行javascript代码实现简单的双向数据绑定

    五十行javascript代码实现简单的双向数据绑定 Vue框架想必从事前端开发的同学都使用过,它的双向数据绑定机制能给我们带来很大的方便.今天闲着没事,尝试着实现一下双向数据绑定,接下来给大家分享一 ...

  2. 利用ES6中的Proxy和Reflect 实现简单的双向数据绑定

    利用ES6中的Proxy (代理) 和 Reflect 实现一个简单的双向数据绑定demo. 好像vue3也把 obj.defineProperty()  换成了Proxy+Reflect. 话不多说 ...

  3. JavaScript实现简单的双向绑定

    很多的前端框架都支持数据双向绑定了,最近正好在看双向绑定的实现,就用Javascript写了几个简单的例子. 几个例子中尝试使用了下面的方式实现双向绑定: 发布/订阅模式 属性劫持 脏数据检测 发布/ ...

  4. React简单实现双向数据绑定

    import React, { Component } from 'react' import ReactDOM from 'react-dom' class App extends Componen ...

  5. 自己手动实现简单的双向数据绑定 mvvm

    数据绑定 数据绑定一般就是指的 将数据 展示到 视图上.目前前端的框架都是使用的mvvm模式实现双绑的.大体上有以下几种方式: 发布订阅 ng的脏检查 数据劫持 vue的话采用的是数据劫持和发布订阅相 ...

  6. 原生js简单实现双向数据绑定原理

    根据对象的访问器属性去监听对象属性的变化,访问器属性不能直接在对象中设置,而必须通过 defineProperty() 方法单独定义. 访问器属性的"值"比较特殊,读取或设置访问器 ...

  7. 简单实现双向数据绑定mvvm。

  8. angularJs初体验,实现双向数据绑定!使用体会:比较爽

    使用初体验:ng 双向数据绑定: 最简单的双向数据绑定:(使用默认模块控制) <body ng-app> <input type="text" ng-model= ...

  9. 深入vue源码,了解vue的双向数据绑定原理

    大家都知道vue是一种MVVM开发模式,数据驱动视图的前端框架,并且内部已经实现了双向数据绑定,那么双向数据绑定是怎么实现的呢? 先手动撸一个最最最简单的双向数据绑定 <div> < ...

随机推荐

  1. AngularJS1.X学习笔记13-动画和触摸

    本文主要涉及了ngAnimation和ngTouch模块,自由男人讲的比较少,估计要用的时候还要更加系统的学习一下. 一.安装 没错,就是酱紫. 二.玩玩动画 <!DOCTYPE html> ...

  2. VMware-vCenter-Server-Appliance VCSA升级步骤

    1.下载ZIP升级文件并解压 2.打开HFS,把解压后的文件夹拖到"Virtual File System"下,在弹出的对话框中点击"Virtual folder&quo ...

  3. Redux应用单一的store原则案例详解

    在开发reac单页面应用的时候,页面的展示逻辑跟数据状态的关系管理变得越来越复杂,redux很好的解决这个问题.废话不多说,直接先上官网api链接. http://cn.redux.js.org/in ...

  4. guava-19.0和google-collections-1.0 的 ImmutableSet 类冲突

    guava-19.0 google-collections-1.0 都有 ImmutableSet 类,包路径也一致,前者有 copyOf(Collection)? 一.应用报错: 二.解决办法 co ...

  5. IDE-Android Studio -FAQ-使用习惯(不断更新 欢迎留言)

    摘要: 从ecplise工具切换到android studio后遇到了很多问题,起初亦非常痛苦,城墙内外阅博无数才得以解决.所以把当时遇到的问题记录下来,方便后来人学习. 另如果有遇到未纪录的问题欢迎 ...

  6. python Http协议

    Http协议 一 HTTP概述 HTTP(hypertext transport protocol),即超文本传输协议.这个协议详细规定了浏览器和万维网服务器之间互相通信的规则. HTTP就是一个通信 ...

  7. POJ-1995 Raising Modulo Numbers---快速幂模板

    题目链接: https://vjudge.net/problem/POJ-1995 题目大意: 求一堆ab的和模上m 思路: 直接上模板 #include<iostream> #inclu ...

  8. 1.4 正则化 regularization

    如果你怀疑神经网络过度拟合的数据,即存在高方差的问题,那么最先想到的方法可能是正则化,另一个解决高方差的方法就是准备更多数据,但是你可能无法时时准备足够多的训练数据,或者获取更多数据的代价很高.但正则 ...

  9. A Neural Algorithm of Artistic Style 图像风格转换 - keras简化版实现

    前言 深度学习是最近比较热的词语.说到深度学习的应用,第一个想到的就是Prisma App的图像风格转换.既然感兴趣就直接开始干,读了论文,一知半解:看了别人的源码,才算大概了解的具体的实现,也惊叹别 ...

  10. C++11 作用域内枚举

    enum class MyEnum{ P1 = , P2, P3, P4, P5 }; MyEnum myEnum = MyEnum::P2; 使用作用域的方式获取并限定P2的值.之所以要使用作用域, ...