js实现数据双向绑定(访问器监听)

本文实例为大家分享了js实现数据双向绑定的具体代码,供大家参考,具体内容如下

双向绑定:

双向绑定基于MVVM模型:model-view-viewModel

model: 模型层,负责业务逻辑以及与数据库的交互
view:视图层,负责将数据模型与UI结合,展示到页面中
viewModel:视图模型层,作为model和view的通信桥梁

双向绑定的含义:当model数据发生变化的时候,会通知到view层,当用户修改了view层的数据的时候,会反映到模型层。

而双向数据绑定的好处在于:只关注于数据操作,DOM操作减少

Vue.js实现的原理就是采用的访问器监听,所以这里也采用访问器监听的方式实现简单的数据双向绑定。

访问器监听的实现,主要采用了javascript中原生方法:Object.defineProperty,该方法可以为某对象添加访问器属性,当访问或者给该对象属性赋值的时候,会触发访问器属性,因此利用此思路,可以在访问器属性中添加处理程序。

这里先实现一个简单的input标签的数据双向绑定过程,先大致了解一下什么是数据的双向绑定。

<input type="text">

<script>
// 获取到input输入框对象
let input = document.querySelector('input');
// 创建一个没有原型链的对象,用于监听该对象的某属性的变化
let model = Object.create(null);
// 当鼠标移开输入框的时候,view层数据通知model层数据的变化
input.addEventListener('blur',function() {
    model['user'] = this.value;
})

// 当model层数据发生变化的时候,通知view层数据的变化。
Object.defineProperty(model, 'user', {
    set(v) {
        user = v;
        input.value = v;
    },
    get() {
        return user;
    }
})
</script>

以上的代码中首先对Input标签对象进行获取,然后对input元素对象添加监听事件(blur),当事件被触发的时候,也就是view层发生变化的时候,就需要去通知model层去更新数据,这里的model层利用的是一个没有原型的空对象(使用空对象的原因:避免获取某属性的时候,由于原型链的存在,造成数据的误读)。

使用Object.defineProperty的方法,为该对象的指定属性添加访问器属性,当该对象的属性被修改,就会触发setter访问器,我们这里就可以为view层的数据赋值,更新view层的数据,这里的view层指的是Input标签的属性value。

看一下效果:

在文本框中输入一个数据,在控制台打印model.user可以看到数据已经影响到了model层

接着在控制台手动修改model层的数据:model.user = ‘9090';
此时可以看到数据文本框也被相应的进行了修改,影响到了view层

好啦,实现了最简单的只针对于文本框的数据双向绑定,我们可以从以上的案例中可以发现以下的实现逻辑:

①. 要实现view层到model的数据通信,就需要知道view层的数据变化了,以及view层的值,但是一般要获取到标签本身的值,除非有内置属性,比如:input标签的value属性,可以获得文本框的输入值

②. 利用Object.defineProperty实现model层向view层的通信,当数据被修改,就会立马触发访问器属性setter,从而可以通知使用了该属性的所有view层去更新他们的现在的数据(观察者)

③. 被绑定的数据需要是作为一个对象的属性,因为Object.defineProperty是对某一个对象的属性开启的访问器特性。

争对以上的总结,我们可以设计出类似于vue.js的数据双向绑定模式:
利用自定义指令实现view到model层的数据通信
利用Object.defineProperty实现model层到view层的数据通信。

这里的实现涉及到三个主要的函数:

  • _observer: 对数据进行处理,重写每一个属性的getter/setter
  • _compile:对自定义指令(这里只涉及了e-bind/e-click/e-model)进行解析,并在解析过程中为节点绑定原生处理事件,以及实现view层到model层的绑定
  • Watcher: 作为model与view的中间桥梁,当model发生变化进一步更新view层

实现代码:

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>双向数据绑定</title>
    <style>
        #app {
            text-align: center;
        }
    </style>
    <script src="/js/eBind.js"></script>
    <script>
        window.onload = function () {
           let ebind =  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>
    <input e-model="number" type="text">
    <form>
        <input type="text" e-model="person.age">
        <button type="button" e-click="addAge">增加</button>
    </form>
    <h3 e-bind="person.age"></h3>
</div>
</body>
</html>

eBind.js

function EBind(options) {
    this._init(options);
}

// 根据所给的自定义参数,进行数据双向绑定的初始化工作
EBind.prototype._init = function (options) {
    // options是初始化时的数据,包括el,data,method
    this.$options = options;

    // el是需要管理的Element对象,el:#app this.$el:id为app的Element对象
    this.$el = document.querySelector(options.el);

    // 数据
    this.$data = options.data;

    // 方法
    this.$methods = options.methods;

    // _binding保存着model与view的映射关系,也就是Wachter的实例,当model更新的时候,更新对应的view
    this._binding = {};

    // 重写 this.$data的get和set方法
    this._obverse(this.$data);

    // 解析指令
    this._compile(this.$el);
}

// 该函数的作用:对所有的this.$data里面的属性进行监听,访问器监听,实现model到view层的数据通信。当model层改变的时候通知view层
EBind.prototype._obverse = function (currentObj, completeKey) {
    // 保存上下文
    var _this = this;

    // currentObj就是需要重写get/set的对象,Object.keys获取该对象的属性,得到的是一个数组
    // 对该数组进行遍历
    Object.keys(currentObj).forEach(function (key) {

        // 当且仅当对象自身的属性才监听
        if (currentObj.hasOwnProperty(key)) {

            // 如果是某一对象的属性,则需要以person.age的形式保存
            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];

            // 修改对象的每一个属性的get和set,在get和set中添加处理事件
            Object.defineProperty(currentObj, key, {
                enumerable: true,
                configurable: true, // 避免默认为false
                get() {
                    return value;
                },
                set(v) {
                    // value保存当前属性的值
                    if (value != v) {
                        // 如果数据被修改,则需要通知每一个使用该数据的地方进行更新数据,也即:model通知view层,Watcher类作为中间层去完成该操作(通知操作)
                        value = v;
                        binding._directives.forEach(function (item) {
                            item.update();
                        })
                    }
                }
            })
        }
    })
}

// 该函数的作用是:对自定义指令进行编译,为其添加原生监听事件,实现view到model层的数据通信,也即当view层数据变化之后通知model层数据更新
// 实现原理:通过托管的element对象:this.$el,获取到所有的子节点,遍历所有的子节点,查看其是否有自定义属性,如果有指定含义的自定义属性
// 比如说:e-bind/e-model/e-click则根据节点上添加的自定义属性的不同为其添加监听事件
// e-click添加原生的onclick事件,这里主要注意点就是:需要将this.$method中指定方法的上下文this改为this.$data
// e-model为绑定的数据更新,这里只支持input,textarea标签,原因:采用标签自带的value属性实现的view到model层的数据通信
// e-bind
EBind.prototype._compile = function (root) {
    // 保存执行上下文
    var _this = this;

    // 获取到托管节点元素的所有子节点,只包括元素节点
    var nodes = root.children;

    for (let i = 0; i < nodes.length; i++) {
        // 获取到子节点/按顺序
        var node = nodes[i];

        // 如果当前节点有子节点,则继续逐层处理子节点
        if (node.children.length) {
            this._compile(node);
        }

        // 如果当前节点绑定了e-click属性,则需要为当前节点绑定onclick事件
        if (node.hasAttribute('e-click')) {
            // hasAttribute可以获取到自定义属性
            node.addEventListener('click',(function () {
                // 获取到当前节点的属性值,也就是方法
                var attrVal = node.getAttribute('e-click');
                // 由于绑定的方法里面的数据要使用data里面的数据,所以需要将执行的函数的上下文,也就是this改为this.$data
                // 而使用bind,不使用call/apply的原因是onclick方法需要触发之后才会执行,而不是立马执行
                return _this.$methods[attrVal].bind(_this.$data);
            })())
        }

        // 只对input和textarea标签元素可以施行双向绑定,原因:利用这两个标签的内置的value属性实现双向绑定
        if (node.hasAttribute('e-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
            // 给element对象添加监听input事件 ,第二个参数是一个立即执行函数,获取到节点的索引值,执行函数内部代码,返回事件处理
            node.addEventListener('input', (function (index) {
                // 获取到当前节点的属性值,也就是方法
                var attrVal = node.getAttribute('e-model');

                // 给当前element对象添加model到view层的映射
                _this._binding[attrVal]._directives.push(new Watcher({
                    name: 'input',
                    el: node,
                    eb: _this,
                    exp: attrVal,
                    attr: 'value'
                }))

                // 如果input标签value值改变,此时需要更新model层的数据,也就是view层到model层的改变
                return function () {
                    // 获取到绑定的属性,以.为分隔符,如果只是一个值,就直接获取当前值,如果是个对象(obj.key)的形式,则绑定的其实obj对象
                    // 中的key的值,此时就需要获取到key,并对key进行赋值为已改变的input标签的value值
                    var keys = attrVal.split('.');

                    // 获取上一步得到的属性的集合中最后一个属性(最后一个属性才是真正被绑定的值)
                    var lastKey = keys[keys.length - 1];

                    // 获得真正被绑定的值的父对象
                    // 因为如果是对象,比如:obj.key.val,则需要找到key的引用,因为这里要改变的是val
                    // 通过引用key 从而改变val的值,但是如果直接获取到的val的引用,val是数值型存储,赋值给另一个变量的时候,其实是新开辟的一个空间
                    // 并不能直接改变model层也就是this.$data里面的数据,而引用数据存储的话,赋值给另一个变量,另一个变量的修改,会影响原来的引用的数据
                    // 所以这里需要找到真正被绑定值的父对象,也就是obj.key里面的obj值
                    var model = keys.reduce(function (value, key) {
                        // 如果不是对象,则直接返回属性value
                        if (typeof value[key] !== 'object') {
                            return value;
                        }

                        return value[key];
                        // 这里使用model层作为起始值,原因:keys里面记录的是this.$data里面的属性,所以需要从父对象this.$data出发去找目标属性
                    }, _this.$data);

                    // model也就是之前说得父对象,obj.key中的obj,而lastkey也就是真正被绑定的属性,找到了之后就需要对其更新为节点的值啦。
                    // 这里的model层被修改会触发_observe里面的访问器属性setter,所以如果其他地方也使用了这个属性的话,也会相应的发生改变哦
                    model[lastKey] = nodes[index].value;
                }
            })(i))
        }

        // 对节点上绑定e-bind,为其添加model到view的映射即可,原因:e-bind实现的是model到view的数据通信,而在this._observer中
        // 已经通过definePrototype实现了,所以这里只需要添加通信,便于在_oberver中实现。
        if(node.hasAttribute('e-bind')) {
            var attrVal = node.getAttribute('e-bind');
            _this._binding[attrVal]._directives.push(new Watcher({
                name: 'text',
                el: node,
                eb: _this,
                exp: attrVal,
                attr: 'innerHTML'
            }))
        }
    }
}

/**
 * options 属性:
 * name: 节点名称:文本节点:text, 输入框:input
 * el: 指令对应的DOM元素
 * eb: 指令对应的EBind实例
 * exp: 指令对应的值:e-bind="test";test就是指令对应的值
 * attr: 绑定的属性值, 比如:e-bind绑定的属性,其实会反应到innerHTML中,v-model绑定的标签会反应到value中
 */
function Watcher(options) {
    this.$options = options;
    this.update();
}

Watcher.prototype.update = function () {
    // 保存上下文
    var _this = this;
    // 获取到被绑定的对象
    var keys = this.$options.exp.split('.');

    // 获取到DOM对象上要改变的属性,对其进行更改
    this.$options.el[this.$options.attr] = keys.reduce(function (value, key) {
        return value[key];
    }, _this.$options.eb.$data)
}

实现效果:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • js实现瀑布流触底动态加载数据

    本文实例为大家分享了js实现瀑布流触底动态加载数据的具体代码,供大家参考,具体内容如下 // onScrollEvent 滚动条事件 <div class="box" ref="box" @mousewheel="onScrollEvent"> //每一个方块内的内容start <div class="boxItemStyle" v-for="(userTag, i) in dataSource&q

  • JS中可能会常用到的一些数据处理方法

    目录 DOM处理 数组 方法 总结 DOM处理 DOM 为文档提供了结构化表示,并定义了如何通过脚本来访问文档结构.目的其实就是为了能让js操作html元素而制定的一个规范.DOM就是由节点组成的. 检查一个元素是否被聚焦 const hasFocus = ele => (ele === document.activeElement); 检查用户是否滚动到页面底部 const isAtBottom = () => document.documentElement.clientHeight +

  • js基础语法与maven项目配置教程案例

    目录 一,js的语句 二,js的数组 三,js的函数 四,Maven 五.总结 一,js的语句 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>测试 js的语句</title> <!-- 在HTML里嵌入js代码 --> <script> // 2. 循环结构 //练习3:在控制台输出结果,输出1亿每天花一半能花多少天,

  • Javascript中的解构赋值语法详解

    前言 首先在 ES6中引入的"解构赋值语法"允许把数组和对象中的值插入到不同的变量中.虽然看上去可能很难,但实际上很容易学习和使用. 解构赋值语法是一种 JS表达式.ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构.通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量. 在ES6解构赋值出现之前,我们需要为变量赋值的时候,只能直接指定值. 比如: let a = 1; let b = 2; let c = 3; let d = 4; let e

  • 超详细的JavaScript基本语法规则

    目录 01 JavaScript (简称:js) js分三个部分: JavaScript是什么? js的代码可以分三个地方写: 02 操作符 操作符:一些符号-----用来计算 关系运算符: 关系运算表达式: 逻辑运算符: 逻辑运算表达式: 03 JS变量 变量名的注意问题-变量名的命名: 04 JS变量作用 05 JS变量的交换 使用第三方的变量进行交换 第二种方式交换:一般适用于数字的交换 06 注释 注释的方式: 07 JS的数据类型 值类型(基本类型): 引用数据类型: 08 JS的数字

  • JavaScript的基础语法和数据类型详解

    目录 引入JavaScript 1.内部标签 2.外部引入 基础语法 数据类型 number 字符串 布尔值 逻辑运算 比较运算符 数组 对象 流程控制 Map和Set iterator 总结 引入JavaScript 1.内部标签 <script> alert("hello world"); </script> 2.外部引入 <script src="js/abc.js"></script> 基础语法 定义变量 &l

  • js实现数据双向绑定(访问器监听)

    本文实例为大家分享了js实现数据双向绑定的具体代码,供大家参考,具体内容如下 双向绑定: 双向绑定基于MVVM模型:model-view-viewModel model: 模型层,负责业务逻辑以及与数据库的交互 view:视图层,负责将数据模型与UI结合,展示到页面中 viewModel:视图模型层,作为model和view的通信桥梁 双向绑定的含义:当model数据发生变化的时候,会通知到view层,当用户修改了view层的数据的时候,会反映到模型层. 而双向数据绑定的好处在于:只关注于数据操

  • js实现数据双向绑定(访问器监听)

    本文实例为大家分享了js实现数据双向绑定的具体代码,供大家参考,具体内容如下 双向绑定: 双向绑定基于MVVM模型:model-view-viewModel model: 模型层,负责业务逻辑以及与数据库的交互 view:视图层,负责将数据模型与UI结合,展示到页面中 viewModel:视图模型层,作为model和view的通信桥梁 双向绑定的含义:当model数据发生变化的时候,会通知到view层,当用户修改了view层的数据的时候,会反映到模型层. 而双向数据绑定的好处在于:只关注于数据操

  • JS原生数据双向绑定实现代码

    代码如下: <span style="font-family:Times New Roman;font-size:14px;" deep="7"><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Demo</title> <script> fu

  • vue.js使用v-model指令实现的数据双向绑定功能示例

    本文实例讲述了vue.js使用v-model指令实现的数据双向绑定功能.分享给大家供大家参考,具体如下: vue.js的一大功能便是实现数据的双向绑定,本文就表单处理时运用v-model指令实现双向绑定做一个介绍: v-model这个指令只能用在<input>, <select>,<textarea>这些表单元素上,所谓双向绑定,指的就是我们在js中的vue实例中的data与其渲染的dom元素上的内容保持一致,两者无论谁被改变,另一方也会相应的更新为相同的数据.这是通过

  • JS数据双向绑定原理与用法实例分析

    本文实例讲述了JS数据双向绑定原理与用法.分享给大家供大家参考,具体如下: 通常在前端开发过程中,经常遇到需要绑定两个甚至多个元素之间的值,比如将input的值绑定到一个h1上,改变input的值,h1的文字也自动更新. <h1 id="title">Hello</h1> <input type="text" id="a" /> 首先是在界面上更改input的值,需要监听input的"input&qu

  • js实现视图和数据双向绑定的方法分析

    本文实例讲述了js实现视图和数据双向绑定的方法.分享给大家供大家参考,具体如下: 前言 视图和数据绑定,使视图和逻辑层分离,使视图层变为数据驱动是前端的一大进步.由此诞生了mvvm类的前端框架,大大提升了开发的效率. 那么在使用旧有的项目中,如何使用更加先进的设计模式来替换掉大量的面向过程编程. 各大框架对于数据绑定的实现都有各自的方式,这里不做深入只是简单介绍一下. Vue使用了es5  Object.defineProperty的特性来实现对数据读取和设置的监听,是一种元编程的方式.个人感觉

  • Nuxt.js 数据双向绑定的实现

    假定我们有一个需求,一开始通过mounted()将一个字符串渲染在页面上,但是我们经过操作后修改了数据并且需要将得到的结果重新异步渲染到页面中去,而不是跳转刷新页面来重新渲染 首先模板中data()中定义数据,并且要将定义的数据显示出来 <template> <div> <span @click="click">{{ text }}</span> </div> </template> <script>

  • proxy实现vue3数据双向绑定原理

    目录 一.proxy对比Object.defineProperty的优点 二..proxy监听对象的简单实现 1.代理对象简单实现 2.补充知识 Reflect 3.proxy方法 三.手写vue3.0双向绑定-es6 Proxy 1.什么是Proxy 2.vue.js中使用双向绑定 四.Proxy对比Object.defineProperty 一.proxy对比Object.defineProperty的优点 proxy的优点: Proxy 可以直接监听对象而非属性: Proxy 可以直接监听

  • Vue数据双向绑定原理及简单实现方法

    Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. 一.示例 var vm = new Vue({ data: { obj: { a: 1 } }, created: function () { console.log(this.obj); } }); 二.实现原理 vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的. 1)数据劫持.vue是通过Object.defineProperty()来实现数据劫持

  • Vue数据双向绑定原理实例解析

    Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新 MVVM模式 MVVM模式就是Model–View–ViewModel模式.它实现了View的变动,自动反映在 ViewModel,反之亦然.对于双向绑定的理解,就是用户更

随机推荐