JS中的双向数据绑定及Object.defineProperty方法

缘起

前几天在看一些流行的迷你mvvm框架(比如avalon.js、 vue.js 这种较轻的框架,而非Angularjs、Emberjs这种较重的框架)的实现。现代流行的mvvm框架一般都会将数据双向绑定(two-ways data binding)做掉,作为框架自身的一个卖点( Ember.js 貌似是不支持数据双向绑定的。),而且每种框架双向数据绑定的实现方式都不太一致,比如Anguarjs内部使用的是 脏检查,而avalon.js内部实现方式的本质是设置 属性访问器

这里不打算具体的讨论各个框架对双向数据绑定的具体实现,仅说一下前端实现双向数据绑定的几种常用方法,并着重讲一下avalon.js实现双向数据绑定的技术选型。

双向数据绑定的常规实现方式

首先我们来说一下何为前端的 双向数据绑定 。简单的来说,就是框架的控制器层(这里的控制器层是一个泛指,可以理解为控制view行为和联系model层的中间件)和UI展示层(view层)建立一个双向的数据通道。当这两层中的任何一方发生变化时,另一层将会立即(或者看起来是 立即)自动作出相应的变化。

一般来说要实现这种双向数据绑定关系(控制器层与展示层的关联过程),在前端目前会有三种方式,

  1. 脏检查
  2. 观察机制
  3. 封装属性访问器

脏检查

我们说Angularjs(这里特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)双向数据绑定的技术实现是脏检查,大致的原理就是,Angularjs内部会维护一个序列,将所有需要监控的属性放在这个序列中,当发生某些特定事件时(注意,这里并不是定时的而是由某些特殊事件触发的),Angularjs会调用 $digest 方法,这个方法内部做的逻辑就是遍历所有的watcher,对被监控的属性做对比,对比其在方法调用前后属性值有没有发生变化,如果发生变化,则调用对应的handler。网上有许多剖析Angularjs双向数据绑定实现原理的文章,比如 这篇 ,再比如 这篇 ,等等。

这种方式的缺点很明显,遍历轮训watcher是非常消耗性能的,特别是当单页的监控数量达到一个数量级的时候。

观察机制

博主之前有一篇转载翻译的文章, Object.observe()带来的数据绑定变革 ,说的就是使用ECMAScript7中的 Object.observe 方法对对象(或者其属性)进行监控观察,一旦其发生变化时,将会执行相应的handler。

这是目前监控属性数据变更最完美的一种方法,语言(浏览器)原生支持,没有什么比这个更好了。唯一的遗憾就是目前支持广度还不行,有待全面推广。

封装属性访问器

在php中有 魔术方法 这样一种概念,比如php中的 __get()__set() 方法。在javascript中也有类似的概念,不过不叫魔术方法,而是叫做访问器。我们来看个示例代码,

var data = {
 name: "erik",
 getName: function() {
 return this.name;
 },
 setName: function(name) {
 this.name = name;
 }
};
</pre>

从上面的代码中我们可以管中窥豹,比如 data 中的 getName()setName() 方法,我们可以简单的将其看成 data.name 的访问器(或者叫做 存取器 )。

其实,针对上述的代码,更加严格一点的话,不允许直接访问 data.name 属性,所有对 data.name 的读写都必须通过 data.getName()data.setName() 方法。所以,想象一下,一旦某个属性不允许对其进行直接读写,而必须是通过访问器进行读写时,那么我当然通过重写属性的访问器方法来做一些额外的情,比如属性值变更监控。使用属性访问器来做数据双向绑定的原理就是在此。

这种方法当然也有弊端,最突出的就是每添加一个属性监控,都必须为这个属性添加对应访问器方法,否则这个属性的变更就无法捕获。

Object.defineProperty 方法

国产mvvm框架avalon.js实现数据双向绑定的原理就是属性访问器。不过它当然不会像上述示例代码一样原始。它使用了ECMAScript5.1(ECMA-262)中定义的标准属性 Object.defineProperty 方法。针对国内行情,部分还不支持 Object.defineProperty 低级浏览器采用VBScript作了完美兼容,不像其他的mvvm框架已经逐渐放弃对低端浏览器的支持。

我们先来MDN上对 Object.defineProperty 方法的定义,

The Object.defineProperty() method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

意义很明确, Object.defineProperty 方法提供了一种直接的方式来定义对象属性或者修改已有对象属性。其方法原型如下,


Object.defineProperty(obj, prop, descriptor)
</pre>

</figure>

其中,

  • obj ,待修改的对象
  • prop ,带修改的属性名称
  • descriptor ,待修改属性的相关描述

descriptor 要求传入一个对象,其默认值如下,

 * @{param} descriptor
 */
     }//欢迎加入全栈开发交流圈一起学习交流:582735936
    ]//面向1-3年前端人员
  }   //帮助突破技术瓶颈,提升思维能力
{
 configurable: false,
 enumerable: false,
 writable: false,
 value: null,
 set: undefined,
 get: undefined
}
</pre>

</figure>
  1. configurable ,属性是否可配置。可配置的含义包括:是否可以删除属性( delete ),是否可以修改属性的 writableenumerableconfigurable 属性。
  2. enumerable ,属性是否可枚举。可枚举的含义包括:是否可以通过 for...in 遍历到,是否可以通过 Object.keys() 方法获取属性名称。
  3. writable ,属性是否可重写。可重写的含义包括:是否可以对属性进行重新赋值。
  4. value ,属性的默认值。
  5. set ,属性的重写器(暂且这么叫)。一旦属性被重新赋值,此方法被自动调用。
  6. get ,属性的读取器(暂且这么叫)。一旦属性被访问读取,此方法被自动调用。

下面来一段示例代码,

var o = {};
Object.defineProperty(o, 'name', {
 value: 'erik'
});

console.log(Object.getOwnPropertyDescriptor(o, 'name')); // Object {value: "erik", writable: false, enumerable: false, configurable: false}

Object.defineProperty(o, 'age', {
 value: 26,
 configurable: true,
 writable: true
});
     }//欢迎加入全栈开发交流圈一起学习交流:582735936
    ]//面向1-3年前端人员
  }   //帮助突破技术瓶颈,提升思维能力
console.log(o.age); // 26

o.age = 18;
console.log(o.age); // 18. 因为age属性是可重写的

console.log(Object.keys(o)); // []. name和age属性都不是可枚举的

Object.defineProperty(o, 'sex', {
 value: 'male',
 writable: false
});

o.sex = 'female'; // 这里的赋值其实是不起作用的
console.log(o.sex); // 'male';

delete o.sex; // false, 属性删除的动作也是无效的
</pre>

</figure>

经过上述的示例,正常情况下 Object.definePropert() 的使用都是比较简单的。

不过还是有一点需要额外注意一下, Object.defineProperty() 方法设置属性时,属性不能同时声明访问器属性( setget )和 writable 或者 value 属性。 意思就是,某个属性设置了writable 或者 value 属性,那么这个属性就不能声明 getset 了,反之亦然。

因为 Object.defineProperty() 在声明一个属性时,不允许同一个属性出现两种以上存取访问控制。

示例代码,

var o = {},
 myName = 'erik';
Object.defineProperty(o, 'name', {
 value: myName,
 set: function(name) {
 myName = name;
 },
 get: function() {
 return myName;
 }
});
</pre>

</figure>

上面的代码看起来貌似是没有什么问题,但是真正执行时会报错,报错如下,

TypeError: Invalid property.  A property cannot both have accessors and be writable or have a value, #<Object>
</pre>

</figure>

因为这里的 name 属性同时声明了 value 特性和 setget 特性,这两者提供了两种对 name 属性的读写控制。这里如果不声明 value 特性,而是声明 writable 特性,结果也是一样的,同样会报错。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容