vuejs如何實(shí)現(xiàn)數(shù)據(jù)雙向綁定
實(shí)現(xiàn)數(shù)據(jù)綁定的做法有大致如下幾種:
發(fā)布者-訂閱者模式(backbone.js)
臟值檢查(angular.js)
數(shù)據(jù)劫持(vue.js)
發(fā)布者-訂閱者模式:
一般通過sub, pub的方式實(shí)現(xiàn)數(shù)據(jù)和視圖的綁定監(jiān)聽,更新數(shù)據(jù)方式通常做法是vm.set('property', value),這里有篇文章講的比較詳細(xì),有興趣可點(diǎn)這里
這種方式現(xiàn)在畢竟太low了,我們更希望通過vm.property = value 這種方式更新數(shù)據(jù),同時(shí)自動(dòng)更新視圖,于是有了下面兩種方式
臟值檢查:
angular.js 是通過臟值檢測(cè)的方式比對(duì)數(shù)據(jù)是否有變更,來決定是否更新視圖,最簡(jiǎn)單的方式就是通過setInterval()
定時(shí)輪詢檢測(cè)數(shù)據(jù)變動(dòng),當(dāng)然Google不會(huì)這么low,angular只有在指定的事件觸發(fā)時(shí)進(jìn)入臟值檢測(cè),大致如下:
DOM事件,譬如用戶輸入文本,點(diǎn)擊按鈕等。( ng-click )
XHR響應(yīng)事件 ( $http )
瀏覽器Location變更事件 ( $location )
Timer事件(
interval )
執(zhí)行
apply()
數(shù)據(jù)劫持:
vue.js 則是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個(gè)屬性的setter,getter,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)。
主要的知識(shí)點(diǎn):
1.Vue雙向綁定原理(一)文檔片段DocumentFragment
2.Vue雙向綁定原理(二)訪問器屬性defineProperty()和發(fā)布/訂閱模式
1、文檔片段DocumentFragment
當(dāng)我們更新少量dom節(jié)點(diǎn)的時(shí)候,可以創(chuàng)建他們?nèi)缓笾苯觓ppendChild()插入DOM樹。但是如果我們要?jiǎng)?chuàng)建大量節(jié)點(diǎn)的時(shí)候,每次都創(chuàng)建再插入,會(huì)調(diào)用很多次appendChild()方法,會(huì)非常浪費(fèi)性能。為了解決這個(gè)問題,就有了documentFragmeng文檔片段,可以先把這些創(chuàng)建的元素放入文檔片段,然后在把文檔片段插入DOM樹,這樣就只會(huì)調(diào)用一次appendChild()方法了。
createDocumentFragment()
用于創(chuàng)建一個(gè)文檔片段作為容器,其中可以包含多個(gè)dom節(jié)點(diǎn)。這里有兩點(diǎn)需要特別注意的地方:
當(dāng)把文檔片段插入DOM樹的時(shí)候,只會(huì)把它的子節(jié)點(diǎn)插進(jìn)去,它作為容器本身是不會(huì)進(jìn)入DOM樹的。
當(dāng)把DOM樹種的節(jié)點(diǎn)插入文檔片段的時(shí)候,這些節(jié)點(diǎn),會(huì)真的從DOM樹種消失。我們也把這個(gè)過程叫做劫持。
在Vue中的作用
上邊說清楚了documentFragment是干嘛的,現(xiàn)在說說他在vue中的作用。
每個(gè)vue實(shí)例都有一個(gè)根元素id的屬性el,Vue對(duì)象通過它來找到要渲染的部分。之后使用createDocumentFragment()方法創(chuàng)建一個(gè)documentFragment,遍歷根元素的所有子元素,依次劫持并插入文檔片段,將根元素掏空。然后執(zhí)行Vue的編譯:遍歷documentFragment中的節(jié)點(diǎn),對(duì)其中的v-for,v-text等屬性進(jìn)行相應(yīng)的處理。最后,把編譯完成后的documentFragment還給根元素。
這也就是為什么,我們寫在模板中的HTML,有v-for,v-model等屬性,而實(shí)際頁(yè)面F12之后卻沒有,因?yàn)槟鞘荲ue編譯之后返回的結(jié)果。
2、訪問器屬性
js的對(duì)象有兩種屬性:數(shù)據(jù)屬性和訪問器屬性。
1.數(shù)據(jù)屬性
數(shù)據(jù)屬性包含一個(gè)數(shù)據(jù)值的位置。這個(gè)位置可以讀取和寫入值。數(shù)據(jù)屬性也就是我們最常見的對(duì)象屬性。數(shù)據(jù)屬性有4個(gè)描述他行為的特性:
Configurable: 能否用delete刪除屬性從而重新定義屬性。默認(rèn)為false
Enumerable: 能否通過for-in遍歷,即是否可枚舉。默認(rèn)為false
Writable: 是否能修改屬性的值。默認(rèn)為false
Value: 包含這個(gè)屬性的數(shù)據(jù)值,讀寫屬性的時(shí)候其實(shí)就在這里讀寫。默認(rèn)為undefined
要修改屬性的上述4個(gè)默認(rèn)特性,就必須使用ECMAScript的Object.defineProperty()方法,該方法包含3個(gè)參數(shù):屬性所在的對(duì)象,屬性名,描述符對(duì)象。描述符對(duì)象的屬性必須在上述4個(gè)屬性中。例如:
var person = {};
Object.defineProperty(person,"name",{
writable: false,
value: "Nicholas"
});
alert(person.name); // "Nicholas"
person.name = "Tom";
alert(person.name); // "Nicholas"
上例創(chuàng)建了一個(gè)不可寫的name屬性并賦值。所以無(wú)法修改。
注意,一旦把Configurable屬性設(shè)置為false,就無(wú)法再將其變回true了,此時(shí)再想修改特性,就都會(huì)報(bào)錯(cuò)了。
2.訪問器屬性
訪問器屬性不包含數(shù)據(jù)值,他們包含一對(duì)getter和setter函數(shù)(非必須)。在讀寫訪問器屬性的值的時(shí)候,會(huì)調(diào)用相應(yīng)的getter和setter函數(shù),而我們的vue就是在getter和setter函數(shù)中增加了我們需要的操作。
訪問器屬性有以下4個(gè)特性:
Configurable: 能否用delete刪除屬性從而重新定義屬性。默認(rèn)false
Enumerable: 能否通過for-in遍歷,即是否可枚舉。默認(rèn)false
get: 讀取屬性時(shí)調(diào)用的函數(shù),默認(rèn)undefined
set: 寫入屬性時(shí)調(diào)用的函數(shù),默認(rèn)undefined
在Vue中的作用
Vue會(huì)遍歷實(shí)例的data屬性,把每一個(gè)data都設(shè)置為訪問器,然后在該屬性的getter函數(shù)中將其設(shè)為watcher,在setter中向其他watcher發(fā)布改變的消息。這樣,配合發(fā)布/訂閱模式,改變其中的一個(gè)值,會(huì)發(fā)布消息,所有的watcher會(huì)更新自己,這些watcher也就是綁定在dom中的顯示信息,比如 v-text=”year” 和 {{ year }} 這些節(jié)點(diǎn)。從而達(dá)到改變?yōu)gdom,在瀏覽器中實(shí)時(shí)變化的效果
(Object.defineProperty 是僅 ES5 支持,且無(wú)法 shim 的特性,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器的原因。)
數(shù)據(jù)劫持:
vue.js 則是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個(gè)屬性的setter,getter,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)。
大致思路: 首先Vue會(huì)使用documentfragment劫持根元素里包含的所有節(jié)點(diǎn),這些節(jié)點(diǎn)不僅包括標(biāo)簽元素,還包括文本,甚至換行的回車。
然后Vue會(huì)把data中所有的數(shù)據(jù),用defindProperty()變成Vue的訪問器屬性,這樣每次修改這些數(shù)據(jù)的時(shí)候,就會(huì)觸發(fā)相應(yīng)屬性的get,set方法。
接下來編譯處理劫持到的dom節(jié)點(diǎn),遍歷所有節(jié)點(diǎn),根據(jù)nodeType來判斷節(jié)點(diǎn)類型,根據(jù)節(jié)點(diǎn)本身的屬性(是否有v-model等屬性)或者文本節(jié)點(diǎn)的內(nèi)容(是否符合{{文本插值}}的格式)來判斷節(jié)點(diǎn)是否需要編譯。對(duì)v-model,綁定事件當(dāng)輸入的時(shí)候,改變Vue中的數(shù)據(jù)。對(duì)文本節(jié)點(diǎn),將他作為一個(gè)觀察者watcher放入觀察者列表,當(dāng)Vue數(shù)據(jù)改變的時(shí)候,會(huì)有一個(gè)主題對(duì)象,對(duì)列表中的觀察者們發(fā)布改變的消息,觀察者們?cè)俑伦约?,改變?jié)點(diǎn)中的顯示,從而達(dá)到雙向綁定的目的。
思路整理:
已經(jīng)了解到vue是通過數(shù)據(jù)劫持的方式來做數(shù)據(jù)綁定的,其中最核心的方法便是通過Object.defineProperty()來實(shí)現(xiàn)對(duì)屬性的劫持,達(dá)到監(jiān)聽數(shù)據(jù)變動(dòng)的目的,無(wú)疑這個(gè)方法是本文中最重要、最基礎(chǔ)的內(nèi)容之一,如果不熟悉defineProperty,猛戳這里整理了一下,要實(shí)現(xiàn)mvvm的雙向綁定,就必須要實(shí)現(xiàn)以下幾點(diǎn):
1、實(shí)現(xiàn)一個(gè)數(shù)據(jù)監(jiān)聽器Observer,能夠?qū)?shù)據(jù)對(duì)象的所有屬性進(jìn)行監(jiān)聽,如有變動(dòng)可拿到最新值并通知訂閱者
2、實(shí)現(xiàn)一個(gè)指令解析器Compile,對(duì)每個(gè)元素節(jié)點(diǎn)的指令進(jìn)行掃描和解析,根據(jù)指令模板替換數(shù)據(jù),以及綁定相應(yīng)的更新函數(shù)
解析器Compile實(shí)現(xiàn)步驟:
(1).解析模板指令,并替換模板數(shù)據(jù),初始化視圖
(2).將模板指令對(duì)應(yīng)的節(jié)點(diǎn)綁定對(duì)應(yīng)的更新函數(shù),初始化相應(yīng)的訂閱器
為了解析模板,首先需要獲取到dom元素,然后對(duì)含有dom元素上含有指令的節(jié)點(diǎn)進(jìn)行處理,因此這個(gè)環(huán)節(jié)需要對(duì)dom操作比較頻繁,所有可以先建一個(gè)fragment片段,將需要解析的dom節(jié)點(diǎn)存入fragment片段里再進(jìn)行處理
3、實(shí)現(xiàn)一個(gè)Watcher,作為連接Observer和Compile的橋梁,能夠訂閱并收到每個(gè)屬性變動(dòng)的通知,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖
4、mvvm入口函數(shù),整合以上三者