vuejs如何实现数据双向绑定 与 Object.defineProperty()

vuejs如何实现数据双向绑定

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)
脏值检查(angular.js)
数据劫持(vue.js)

发布者-订阅者模式:

一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是vm.set('property', value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查:

angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval()

定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )

  • XHR响应事件 ( $http )

  • 浏览器Location变更事件 ( $location )

  • Timer事件( interval )

  • 执行 apply()

数据劫持:

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

主要的知识点:
1.Vue双向绑定原理(一)文档片段DocumentFragment
2.Vue双向绑定原理(二)访问器属性defineProperty()和发布/订阅模式

1、文档片段DocumentFragment

当我们更新少量dom节点的时候,可以创建他们然后直接appendChild()插入DOM树。但是如果我们要创建大量节点的时候,每次都创建再插入,会调用很多次appendChild()方法,会非常浪费性能。为了解决这个问题,就有了documentFragmeng文档片段,可以先把这些创建的元素放入文档片段,然后在把文档片段插入DOM树,这样就只会调用一次appendChild()方法了。

createDocumentFragment() 
用于创建一个文档片段作为容器,其中可以包含多个dom节点。这里有两点需要特别注意的地方:

当把文档片段插入DOM树的时候,只会把它的子节点插进去,它作为容器本身是不会进入DOM树的。

当把DOM树种的节点插入文档片段的时候,这些节点,会真的从DOM树种消失。我们也把这个过程叫做劫持

在Vue中的作用

上边说清楚了documentFragment是干嘛的,现在说说他在vue中的作用。

每个vue实例都有一个根元素id的属性el,Vue对象通过它来找到要渲染的部分。之后使用createDocumentFragment()方法创建一个documentFragment,遍历根元素的所有子元素,依次劫持并插入文档片段,将根元素掏空。然后执行Vue的编译:遍历documentFragment中的节点,对其中的v-for,v-text等属性进行相应的处理。最后,把编译完成后的documentFragment还给根元素。

这也就是为什么,我们写在模板中的HTML,有v-for,v-model等属性,而实际页面F12之后却没有,因为那是Vue编译之后返回的结果。

2、访问器属性

js的对象有两种属性:数据属性和访问器属性

1.数据属性

数据属性包含一个数据值的位置。这个位置可以读取和写入值。数据属性也就是我们最常见的对象属性。数据属性有4个描述他行为的特性:

Configurable: 能否用delete删除属性从而重新定义属性。默认为false
Enumerable: 能否通过for-in遍历,即是否可枚举。默认为false
Writable: 是否能修改属性的值。默认为false
Value: 包含这个属性的数据值,读写属性的时候其实就在这里读写。默认为undefined

要修改属性的上述4个默认特性,就必须使用ECMAScript的Object.defineProperty()方法,该方法包含3个参数:属性所在的对象,属性名,描述符对象。描述符对象的属性必须在上述4个属性中。例如:

var person = {};
Object.defineProperty(person,"name",{
    writable: false,
    value: "Nicholas"
});

alert(person.name);   // "Nicholas"
person.name = "Tom";
alert(person.name);  // "Nicholas"

上例创建了一个不可写的name属性并赋值。所以无法修改。

注意,一旦把Configurable属性设置为false,就无法再将其变回true了,此时再想修改特性,就都会报错了。

2.访问器属性

访问器属性不包含数据值,他们包含一对getter和setter函数(非必须)。在读写访问器属性的值的时候,会调用相应的getter和setter函数,而我们的vue就是在getter和setter函数中增加了我们需要的操作。

访问器属性有以下4个特性:

Configurable: 能否用delete删除属性从而重新定义属性。默认false
Enumerable: 能否通过for-in遍历,即是否可枚举。默认false
get: 读取属性时调用的函数,默认undefined
set: 写入属性时调用的函数,默认undefined
在Vue中的作用

Vue会遍历实例的data属性,把每一个data都设置为访问器,然后在该属性的getter函数中将其设为watcher,在setter中向其他watcher发布改变的消息。这样,配合发布/订阅模式,改变其中的一个值,会发布消息,所有的watcher会更新自己,这些watcher也就是绑定在dom中的显示信息,比如 v-text=”year” 和 {{ year }} 这些节点。从而达到改变浏dom,在浏览器中实时变化的效果

(Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。)

数据劫持:
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

大致思路: 首先Vue会使用documentfragment劫持根元素里包含的所有节点,这些节点不仅包括标签元素,还包括文本,甚至换行的回车。
然后Vue会把data中所有的数据,用defindProperty()变成Vue的访问器属性,这样每次修改这些数据的时候,就会触发相应属性的get,set方法。
接下来编译处理劫持到的dom节点,遍历所有节点,根据nodeType来判断节点类型,根据节点本身的属性(是否有v-model等属性)或者文本节点的内容(是否符合{{文本插值}}的格式)来判断节点是否需要编译。对v-model,绑定事件当输入的时候,改变Vue中的数据。对文本节点,将他作为一个观察者watcher放入观察者列表,当Vue数据改变的时候,会有一个主题对象,对列表中的观察者们发布改变的消息,观察者们再更新自己,改变节点中的显示,从而达到双向绑定的目的。

思路整理:

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
解析器Compile实现步骤:

(1).解析模板指令,并替换模板数据,初始化视图

(2).将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

为了解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,因此这个环节需要对dom操作比较频繁,所有可以先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

你可能感兴趣的:(vuejs如何实现数据双向绑定 与 Object.defineProperty())