作為剛?cè)胄蠽ue的小白磕谅,看了不少Vue響應(yīng)式原理的文章私爷,還是不太明白,要么說的太復(fù)雜膊夹,要么說的不夠詳細衬浑。因此,我專門研究了一下具體的代碼放刨,并參考了滴滴團隊的《Vue.js權(quán)威指南》一書中的深入響應(yīng)式原理一章工秩,總結(jié)了能通俗易懂的相關(guān)知識。
由于手機上編輯不便进统,暫時沒有加入代碼事例助币,等以后補充。
正文:
先上傳一張官方的圖解麻昼。
Vuejs實現(xiàn)響應(yīng)式主要靠三步:
1奠支、對數(shù)據(jù)進行處理。使得數(shù)據(jù)能夠?qū)?shù)據(jù)變化有反應(yīng)抚芦。
2倍谜、對v-text這些指令做處理迈螟。賦予他們改變DOM中對應(yīng)數(shù)據(jù)的方法,類似update(value)就把頁面上的插值改成value
3尔崔、提供watcher答毫。通過watcher把數(shù)據(jù)的命令傳達給指令
響應(yīng)式原理主要包括三個部分:Data、Watcher季春、Directive(即v-text這些指令)
先說Data部分:
Vuejs有個生命周期洗搂,第一步就是vm._initData方法。這個方法主要干了兩件事:
1载弄、_proxy代理數(shù)據(jù)耘拇。
就是把data中的數(shù)據(jù)代理到vm實例上,那么以后我們訪問vm.times就等于訪問vm._data.times宇攻,代理的過程就是利用Object.defineProperty()方法惫叛,具體實現(xiàn)可以看源碼。
2逞刷、observe數(shù)據(jù)嘉涌。
這步的工作主要是上面提到的賦予數(shù)據(jù)有感知變化的能力,俗話說觀測數(shù)據(jù)夸浅。也就是通過Object.defineProperty方法定義數(shù)據(jù)新的setter()和getter()方法仑最,可見,Object.defineProperty是核心方法帆喇。
這里的observe(val)針對的是具體的val警医,也就是說如果val是數(shù)組,就遍歷番枚,直至單個元素法严。
具體observe干了什么呢?
① 對每個數(shù)據(jù)new了一個Dep實例葫笼,即dep深啤。
這個dep的作用主要是用來訂閱Watcher實例的,因為你一個數(shù)據(jù)可能多出要用路星,要與多個Watcher實例鏈接起來溯街。說是訂閱,其實就是把這些Watcher實例存到這個dep的一個subs數(shù)組屬性中洋丐。
② 重新定義了getter方法呈昔。
這個方法主要是把上面創(chuàng)建的那個dep傳給當前正在計算的watcher實例(這叫依賴收集)。什么叫當前正在計算的watcher實例友绝?可以這樣理解堤尾,有一個全局變量,在依次初始化每個watcher的時候迁客,會把自己存到這個全局變量中去以供獲取郭宝。
③ 重新定義了setter方法
只有一步辞槐,就是dep.notify(),這個方法主要是調(diào)用上面那個當時數(shù)據(jù)的dep來遍歷它subs數(shù)組里面的watcher實例粘室,并調(diào)用他們的update方法來更新榄檬。俗話說就是,通知跟這個數(shù)據(jù)有關(guān)的watcher實例要更新了衔统。
至此鹿榜,data部分結(jié)束。
directive部分
指令太多锦爵,就以一個v-text為例:hello {{world}}舱殿。
瀏覽器得認識插值,于是進行編譯险掀,調(diào)用_compile方法怀薛。這個方法主要有3步。
① 將template編譯成document fragment迷郑,然后深度遍歷編譯Dom樹,即編譯每個節(jié)點创倔。
② 每個節(jié)點如何編譯的呢嗡害?
以文本節(jié)點"hello {{world}}"為例,將其解析成一個tokens數(shù)組畦攘,有兩個對象霸妹,一個是僅有value="hello "的對象,另一個是包含html知押、oneTime叹螟、tag、value等字段的對象(就是插值那部分)台盯。
然后新建一個document fragment罢绽,遍歷這個tokens,分別創(chuàng)建dom插到這個document fragment里静盅,如果是有tag字段的對象(即插值那個對象)良价,還會擴展其token對象,即在有之前的字段的情況下蒿叠,再增加一個descriptor字段明垢,值是一個對象,包括def市咽、expression痊银、name等字段,最后返回一個linkFn的函數(shù)施绎。
注意溯革,這里返回的是一個函數(shù)贞绳,解析完每個節(jié)點后返回的是一個函數(shù)。函數(shù)的作用是如果滿足前面token對象里的一些字段鬓照,就調(diào)用vm._bindDir()方法創(chuàng)建指令對象熔酷,指令對象的屬性也是根據(jù)token對象里的字段設(shè)置,并且豺裆,這個指令對象實例還要放到vm實例的directives數(shù)組中去拒秘,以便于后面比如遍歷使用。
注意臭猜,此處函數(shù)并沒有調(diào)用躺酒,只是作為返回值提供。
③ 所有節(jié)點編譯的最后會遍歷所有節(jié)點的linkFn方法蔑歌,在此處才執(zhí)行羹应,這個時候所有的指令對象就創(chuàng)建了,于是下一步就將這些指令對象排序并綁定一些比如update方法次屠,這里的update方法是指令自身的园匹,會更改dom。
在對每個指令對象綁定的過程中都會new一個watcher實例劫灶,并把指令對象的update方法傳給watcher實例作為一個回調(diào)裸违,此處指令對象就與watcher建立起了鏈接。watcher實例是在此處new的本昏。
Watcher部分
watcher主要用來鏈接data和directive供汛。
① 將插值表達式轉(zhuǎn)成帶有exp和get()方法的對象。并把get()方法賦給這個watcher實例的getter涌穆。
這個get()方法的作用就是傳一個vm進去怔昨,會返回vm.exp的值
② 調(diào)用Watcher實例自身的get()方法。
第一步設(shè)置自己為當前正在計算的Watcher宿稀,即上面Data部分提到的那個全局變量趁舀。
第二步利用call方法取一次該數(shù)據(jù)。
如何獲取的原叮?即this.getter.call(vm,vm)赫编,getter方法在第一步定義了,會對vm中數(shù)據(jù)執(zhí)行一次取值操作奋隶。
比如上面插值中的world擂送,這個時候會觸發(fā)world數(shù)據(jù)的getter方法,即一方面收集到這個world數(shù)據(jù)的dep(依賴收集)唯欣,另一方面會讓world數(shù)據(jù)訂閱到我這個watcher實例(訂閱)嘹吨,加入到world數(shù)據(jù)dep的屬性subs數(shù)組里。
③ 數(shù)據(jù)改變時境氢,world數(shù)據(jù)的setter方法會觸發(fā)蟀拷,即dep.notify()方法觸發(fā)碰纬,于是遍歷了dep的subs數(shù)組,并調(diào)用了里面每個watcher實例的update方法问芬。
watcher實例的update方法只有在一部分情況下會直接調(diào)用悦析,大部分情況下會延遲執(zhí)行。
如何延遲執(zhí)行的呢此衅?
即把當前watcher實例推入一個內(nèi)部隊列中强戴,這個隊列會通過nextTick方法在下一個事件循環(huán)周期處理,遍歷執(zhí)行挡鞍,這樣可以只觸發(fā)一次DOM操作骑歹。
watcher實例是如何update的呢?
我們得比較新舊值看是否需要更新墨微。
舊值就是之前調(diào)用this.get()獲得的值道媚。
新值就再次執(zhí)行上面的步驟②!步驟②會重新再取一次值翘县,并重新收集依賴及訂閱最域,如果值一樣就沒操作,如果不一樣锈麸,就調(diào)用watcher實例創(chuàng)建時羡宙,directive傳入的那個回調(diào),那個回調(diào)就是指令對象自己的update方法掐隐,這樣就可以更新具體的值了。
至此結(jié)束钞馁!