理解前端数据双向绑定原理——Object.defineProperty()

Object.defineProperty与vue之间的关系

vue是通过数据劫持的方式来做数据绑定的,最核心的方法是通过 Object.defineProperty()方法来实现对属性的劫持,达到能监听到数据的变动。要实现数据的双向绑定。

理解前端数据双向绑定原理:Object.defineProperty()

Object.definedProperty方法可以在一个对象上直接定义一个新的属性、或修改一个对象已经存在的属性,最终返回这个对象。

Object.defineProperty 需要三个参数(object , propName , descriptor)

1 object 对象 => 被定义或修改属性的对象;

2 propName 属性名 => 要加的属性的名字 【类型:String】

3 descriptor 属性描述 => 加的这个属性有什么样的特性【类型:Object】

返回值

函数将返回传递给他的obj对象本身。

描述符(descriptor)说明

该方法允许开发者精确的对对象属性的定义和修改。通过正常赋值进行属性添加而构建的属性会被枚举器方法(如for…in循环或Object.keys方法)获取,从而导致属性值被外部方法改变或删除。而Object.defineProperty()可以避免以上描述的情况,默认的,通过Object.defineProperty()添加的属性是默认不可改变的。
属性描述参数(descriptor)主要由两部分构成:数据描述符(data descriptor)和访问器描述符(accessor descriptor)。数据描述符就是一个包含属性的值,并说明这个值可读或不可读的对象;访问器描述符就是包含该属性的一对getter-setter方法的对象。一个完整的属性描述(descriptor)必须是这两者之一,并且不可以两者都有。

数据描述符和访问器描述符各自都是对象,他们必须包含以下键值对:

configurable
仅当设置的属性的描述符需要被修改或需要通过delete来删除该属性时,configurable属性设置为true。默认为false。

enumerable
仅当设置的属性需要被枚举器(如for…in)访问时设置为true。默认为false。

数据描述符可以包含以下可选键值对:

value
设置属性的值,可以是任何JavaScript值类型(number,object,function等类型)。默认为undefined。

writable
仅当属性的值可以被赋值操作修改时设置为true。默认为false。

访问器描述符可以包含以下可选键值对:

get
属性的getter方法,若属性没有getter方法则为undefined。该方法的返回为属性的值。默认为undefined。

set
属性的setter方法,若属性没有setter方法则为undefined。该方法接收唯一的参数,作为属性的新值。默认为undefined。

使用示例

创建一个属性

如果当前对象不存在我们要设置的属性,Object.defineProperty()会根据方法设置为对象创建一个新的属性。如果描述符参数缺失,则会被设置为默认值。所有布尔型描述符属性会被默认设置为false。而value,get,set会被默认设置为undefined。一个未设置get/set/value/writable的属性被称为一个“原生属性(generic)”,并且他的描述符(descriptor)会被“归类”为一个数据描述符(data descriptor)。

修改一个属性

当某个属性已经存在了,Object.defineProperty()会根据对象的属性配置(configuration)和新设置的值来尝试修改该属性。如果该属性的configurable被设置为false,则该属性无法被修改(这种情况下有个特殊情况:如果之前的writable设置为true,则我们仍可以将writable设置为false,一旦这么做之后,任何描述符属性将变得不可设置)。如果属性的configurable设置为false,则我们无法将属性的描述符在数据描述符和访问器描述符之间转换。
如果新设置的属性和该属性不同,并且该属性的configurable被设置为false,则一个类型错误(TypeError)会被抛出(除了上一段文字中说的特殊情况)。若新旧属性完全相同,则什么都不会发生。

可写特性-writable

当一个属性的writable被设置为false,这个属性就成为“不可写的(non-writable)”。该属性不可被重新赋值。

可枚举特性-enumerable

属性的enumerable值定义对象的属性是否会出现在枚举器(for…in循环和Object.keys())中。

可配置特性-configurable

属性的configurable值控制一个对象的属性可否被delete删除,同时也控制该属性描述符的配置可否改变(除了前文所述在configurable为false时,若writable为true,则仍可以进行一次修改将writable改变为false)。

如果o.a属性的configurable为true,就不会有任何错误抛出,并且o.a在最后的delete操作中会被删除。

添加属性时的默认值

考虑描述符特性的默认值如何被应用是非常重要的。正如下面示例所示,简单的使用"."符号来设置一个属性和使用Object.defineProperty()是有很大区别的。

定制的Setters和Getters

下面的示例展示了如何实现一个“自存档(self-archiving)”的对象。当temperature属性被设置时,archive数组就会添加一个日志记录。

 

译者注

Object.defineProperty()方法被许多现代前端框架(如Vue.js,React.js)用于数据双向绑定的实现,当我们在框架Model层设置data时,框架将会通过Object.defineProperty()方法来绑定所有数据,并在数据变化的同时修改虚拟节点,最终修改页面的Dom结构。在这个过程中有几点需要注意:

延迟发生变化

现代框架为了避免密集的Dom修改操作,对绑定的数据修改后将会设置一个极小(通常为1ms)的setTimeout延迟再应用变化。也就是说,虚拟节点和页面Dom树的变化和数据的变化中间会存在一个空闲期。注意到这一点的开发者就会意识到,如果我们想实现一个功能:如果我们当前对data进行了修改,期望model层立即发生变化并处于可操作的状态,这是不可行的。当然,许多框架也为我们提供了许多应对方法,例如Vue的nextTick()方法等。

数组的变化

先让我们了解下Object.defineProperty()对数组变化的跟踪情况:

可以看到,当a.b被设置为数组后,只要不是重新赋值一个新的数组对象,任何对数组内部的修改都不会触发setter方法的执行。这一点非常重要,因为基于Object.defineProperty()方法的现代前端框架实现的数据双向绑定也同样无法识别这样的数组变化。因此第一点,如果想要触发数据双向绑定,我们不要使用arr[1]=newValue;这样的语句来实现;第二点,框架也提供了许多方法来实现数组的双向绑定。
对于框架如何实现数组变化的监测,大多数情况下,框架会重写Array.prototype.push方法,并生成一个新的数组赋值给数据,这样数据双向绑定就会触发。作为框架使用者,我们需要知道的就是,这样实现的数组修改会消耗更多的性能。

configurable和writable

原文中描述过一种特殊情况:当configurable为false时,我们唯一仍能改变的属性就是将设置为true的writable设置为false。对此译者进行了以下测试(以下代码在Chrome和IE下运行论证,输出结果相同):

 

posted @ 2019-03-19 13:47  【小新】  阅读(885)  评论(0编辑  收藏  举报