原生JS数据绑定的实现

时间: 2018-02-24阅读: 2507标签: 数据

双向数据绑定是非常重要的特性 —— 将js模型与html视图对应,能减少模板编译时间同时提高用户体验。我们将学习在不使用框架的情况下,使用原生js实现双向绑定 —— 一种为Object.observe(译注:现已废弃,作者写博客时为14年11月),另一种为覆盖get / set。PS: 第二种更好,详情请参阅底部的TL;DR(译注:too long;don't read. 直译为“太长,不想看”,意译为“简单粗暴来吧”)

1: Object.observe 和 DOM.onChange

Object.observe()是一种新特性,其在ES7中实现,但在最新的Chrome中已可用 —— 允许对js对象进行响应式更新。简单说就是 —— 只要对象(的属性)发生变化就调用回调函数

一般用法为:

log = console.log
user = {}
Object.observe(user, function(changes){    
    changes.forEach(function(change) {
        user.fullName = user.firstName + " " + user.lastName;         
    });
});

user.firstName = 'Bill';
user.lastName = 'Clinton';
user.fullName // 'Bill Clinton'

这很方便,且能实现响应式编程 —— 保证所有内容都是最新的。

如下:

//<input id="foo">
user = {};
div = $("#foo");
Object.observe(user, function(changes){    
    changes.forEach(function(change) {
        var fullName = (user.firstName || "") + " " + (user.lastName || "");         
        div.text(fullName);
    });
});

user.firstName = 'Bill';
user.lastName = 'Clinton';

div.text() //Bill Clinton

JSFiddle

如上,我们自己实现了模型到数据的绑定!封装一下(译注:此处原文为Let’s DRY ourselves with a helper function. DRY即 don't repeat yourself)

//<input id="foo">
function bindObjPropToDomElem(obj, property, domElem) { 
  Object.observe(obj, function(changes){    
    changes.forEach(function(change) {
      $(domElem).text(obj[property]);        
    });
  });  
}

user = {};
bindObjPropToDomElem(user,'name',$("#foo"));
user.name = 'William'
$("#foo").text() //'William'

JSFiddle

换一种方式 —— 将DOM元素与js值绑定起来。简单的方法是使用jQuery.change

//<input id="foo">
$("#foo").val("");
function bindDomElemToObjProp(domElem, obj, propertyName) {  
  $(domElem).change(function() {
    obj[propertyName] = $(domElem).val();
    alert("user.name is now "+user.name);
  });
}

user = {}
bindDomElemToObjProp($("#foo"), user, 'name');
//enter 'obama' into input
user.name //Obama.

JSFiddle

简直不要太方便,在实际开发时,可以将两者结合,通过函数来创建一个双向数据绑定:

function bindObjPropToDomElem(obj, property, domElem) { 
  Object.observe(obj, function(changes){    
    changes.forEach(function(change) {
      $(domElem).text(obj[property]);        
    });
  });  
}

function bindDomElemToObjProp(obj, propertyName, domElem) {  
  $(domElem).change(function() {
    obj[propertyName] = $(domElem).val();
    console.log("obj is", obj);
  });
}

function bindModelView(obj, property, domElem) {  
  bindObjPropToDomElem(obj, property, domElem)
  bindDomElemToObjProp(obj, propertyName, domElem)
}

注意:在双向绑定时,需正确进行DOM操作,因为不同的DOM元素(input,div,textarea,select)有不同的取值方式(text,val)。同时注意:双向数据绑定并不是必须的 —— “输出型”元素一般不需要视图到模型的绑定,而“输入型”元素一般不需要模型到视图的绑定。

下面为第二种方式:

2: 深入'get'和'set'属性

上面的解决方法并不完美。比如直接的修改并不会自动触发jQuery的“change”事件 —— 例如,直接通过代码对DOM进行修改,比如以下代码不起作用:

$("#foo").val('Putin')
user.name //still Obama. Oops.

现在,我们来用一种更激进的方式实现 —— 重写getter和setter。因为我们不仅要监测变化,我们将重写js最底层的功能,即get/setting变量的能力,所以不那么“安全”。后面我们将会看到,这种元编程的方式有多强大。

那么,如果我们可以重写get和set对象值的方法会怎么样呢?这也是数据绑定的实质。用 Object.defineProperty() 即可实现.

其实,以前就有已废弃且非标准实现方式,但通过Object.defineProperty的实现方式更好(最重要的是标准),如下所示:

user = {}
nameValue = 'Joe';
Object.defineProperty(user, 'name', {
  get: function() { return nameValue }, 
  set: function(newValue) { nameValue = newValue; },
  configurable: true //to enable redefining the property later
});

user.name //Joe 
user.name = 'Bob'
user.name //Bob
nameValue //Bob

现在user.name是nameValue的别名。但可做的不仅仅是创建新的变量名 - 我们可以通过它来保证模型和视图的一致。如下:

//<input id="foo">
Object.defineProperty(user, 'name', {
  get: function() { return document.getElementById("foo").value }, 
  set: function(newValue) { document.getElementById("foo").value = newValue; },
  configurable: true //to enable redefining the property later
});

user.name现在绑定到#foo元素。这种底层的方式非常简洁 —— 通过定义(或扩展)变量属性的get / set实现。由于实现非常简洁,因此可以根据情况轻松扩展/修改代码 —— 仅绑定或扩展get / set中的一个,比如绑定其他数据类型。

可封装如下:

function bindModelInput(obj, property, domElem) {
  Object.defineProperty(obj, property, {
    get: function() { return domElem.value; }, 
    set: function(newValue) { domElem.value = newValue; },
    configurable: true
  });
}

使用:

user = {};
inputElem = document.getElementById("foo");
bindModelInput(user,'name',inputElem);

user.name = "Joe";
alert("input value is now "+inputElem.value) //input is now 'Joe';

inputElem.value = 'Bob';
alert("user.name is now "+user.name) //model is now 'Bob';

JSFiddle

注意:上面的domElem.value只对input元素有效。(可在bindModelInput中扩展,对不同的DOM类型使用对应的方法来设置它的值)。

思考:

  • DefineProperty浏览器兼容性良好 。
  • 注意:上面的实现中,在某些场景下,视图可认为是符合SPOT (single point of truth )原则的,但该原则常常被忽视(因为双向数据绑定也就意味着等价)。然而,深究下去可能就会发现问题了,在实际开发中也会遇到。 —— 比如,当删除DOM元素时,关联的模型会自动注销么?答案是不会。bindModelInput函数在domElem元素上创建了一个闭包,使DOM元素常驻在内存中 —— 并保持模型与模型的绑定关系 —— 即使DOM元素被移除。即使视图被移除了,但模型依旧存在。反之一样 —— 若模型被移除了,视图依然能够正常显示。在某些刷新视图和模型无效的情况下,理解这些内部原理就能找到原因了。

(译注:SPOT简单翻译为“单点原则”,即引起变化最好的是由单一入口引起的,而不是由多个入口引起的,比如一个函数,其返回结果最好仅由参数决定,这样输入和输出才能一致,而不会由于其他变化导致用一个输入会出现不同的输出)

这种自己实现的数据绑定方法与Knockout或Angular等框架数据绑定相比,有一些优点,例如:

  • 理解:一旦掌握数据绑定的源码,不仅理解更深入,而且也能对其进行扩展和修改。
  • 性能:不要将所有东西都绑定在一起,只绑定所需的,避免监测过多对象
  • 避免锁定:若所用的框架不支持数据绑定,则自行实现的数据绑定更强大

缺点是由于不是真正的绑定(没有脏检查),有些情况会失败 —— 视图更新时不会触发模型中的数据,所以当试着同步视图中的两个DOM元素时将会失败。也就是说,将两个元素绑定到同一个模型上时,只有更新模型,则两个元素才会被正确更新。可以通过自定义一个更新函数来实现:

//<input id='input1'>
//<input id='input2'>
input1 = document.getElementById('input1')
input2 = document.getElementById('input2')
user = {}
Object.defineProperty(user, 'name', {
  get: function() { return input1.value; }, 
  set: function(newValue) { input1.value = newValue; input2.value = newValue; },
  configurable: true
});
input1.onchange = function() { user.name = user.name } //sync both inputs.

TL;DR:

当需要使用原生js创建模型和视图的双向数据绑定时,如下:

function bindModelInput(obj, property, domElem) {
  Object.defineProperty(obj, property, {
    get: function() { return domElem.value; }, 
    set: function(newValue) { domElem.value = newValue; },
    configurable: true
  });
}

//<input id="foo">
user = {}
bindModelInput(user,'name',document.getElementById('foo')); //hey presto, we now have two-way data binding.

感谢阅读,本文也发布在 JavaScript Weekly, 可在reddit回复我

翻译来源:  原生JS数据绑定
原文出处 native_javascript_data_binding 


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/435

Highcharts数据量超过1000时无法显示问题

今天在vue的项目中引入Highcharts,想做一个大数据量的实时刷新曲线图,发现当数据量超过1000就无法显示。经过排查发现 Highcharts为了保证更好的性能设置了一个性能阈值检查

mock.js模拟数据

开发时,后端还没完成数据输出,前端只好写静态模拟数据。数据太长了,将数据写在js文件里,完成后挨个改url。某些逻辑复杂的代码,加入或去除模拟数据时得小心翼翼。想要尽可能还原真实的数据,要么编写更多代码,要么手动修改模拟数据

Redis 的底层数据结构(SDS和链表)

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。可能几乎所有的线上项目都会使用到 Redis,无论你是做缓存、或是用作消息中间件,用起来很简单方便

JS实现前端将数据导出excel

方法一将table标签,包括tr、td等对json数据进行拼接,将table输出到表格上实现,这种方法的弊端在于输出的是伪excel,虽说生成xls为后缀的文件,但文件形式上还是html,代码如下

8种常见数据结构及其Javascript实现

做前端的同学不少都是自学成才或者半路出家,计算机基础的知识比较薄弱,尤其是数据结构和算法这块,所以今天整理了一下常见的数据结构和对应的Javascript的实现,希望能帮助大家完善这方面的知识体系。

js常见的八种数据结构

数组(Array)数组是最简单、也是使用最广泛的数据结构。栈(Stack)栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。

web scraper 抓取网页数据的几个常见问题

如果你想抓取数据,又懒得写代码了,可以试试 web scraper 抓取数据。如果你在使用 web scraper 抓取数据,很有可能碰到如下问题中的一个或者多个,而这些问题可能直接将你计划打乱,甚至让你放弃 web scraper 。

vue数据初始化--initState

Vue 实例在建立的时候会运行一系列的初始化操作,而在这些初始化操作里面,和数据绑定关联最大的是 initState。首先,来看一下他的代码:

JavaScript 如何与 CSS 和 Sass 共享数据

JavaScript 与 CSS 已经相生相伴 20 年了,但是要在它们之间共享数据却非常困难。当然,这方面肯定有过一些尝试,不过我想到的是简单而又直观的做法:不需要做代码结构上的改动,而是利用 CSS 自定义属性,甚至是 Sass 变量

javascript中的typeof返回的数据类型_以及强制/隐式类型转换

由于js为弱类型语言拥有动态类型,这意味着相同的变量可用作不同的类型。 typeof 运算符返回一个用来表示表达式的数据类型的字符串,目前typeof返回的字符串有以下这些: undefined、boolean、string、number、object、function、“symbol

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广