Vue響應式原理原始碼淺析

RaUnicorn發表於2017-10-27

第一次寫文章,寫得不對的地方希望各位大神指出。

先大概說下Vue響應式實現的原理。

Vue的響應式原理是通過“觀察者/訂閱者”模式實現的。

首先,Vue會給data及data下的陣列、物件迴圈呼叫Object.defineProperty方法來設定getter和setter,以此來攔截data的賦值和取值。也就是說,當我們賦值(如:this.property='string')時,會呼叫Object.defineProperty方法設定的set方法,當我們取值時(如:<div v-if="title">{{title}}</div>),會呼叫設定的get方法。

get方法會判斷當前Dep.target(Dep物件用於維護依賴,Dep.target用於儲存當前Watcher物件)是否為空,如果不為空,則將Dep.target加入到dep物件的subs陣列中用以記錄依賴,也就是說這個subs陣列中記錄了所有會取該data的Watcher物件,這樣的話當該data發生改變時,我們就能通過這個陣列來通知所有的依賴去進行更新,從而完成響應式。

這個通知過程就是通過set方法來完成的。set方法會呼叫dep.notify方法來通知所有依賴的Watcher物件,讓他們呼叫自己的update方法來進行更新。

接下來說一下這個Watcher是如何建立的。

當Vue元件在渲染時,會先通過compileToFunctions函式將元件的template來生成一個render函式(這個render函式是用於生成VNode虛擬DOM樹的),然後會建立一個Watcher物件,並將生成的render函式傳給這個Watcher物件用於更新。當Watcher物件建立時,會呼叫我們傳進去的render函式,呼叫render函式時會去獲取template中使用到的data的值,這樣的話就會觸碰到getter,將這個Watcher物件新增到依賴中。這樣我們整一個鏈路就完成了。

當data發生改變,就會通知這個Watcher物件去更新,這個Watcher物件就會呼叫render函式去重新渲染。由於Vue2.0使用的是Virtual DOM,所以當data改變時,重新渲染的就只有改變的部分,不用擔心整個元件重新渲染造成效能問題,所以整一個render就只需要一個Watcher物件去維護而不是像Vue1.0時那樣每一個Directive對應一個Watcher物件。

1. 為什麼有時候我的資料響應式會失效?

由於這個響應式的建立是在Vue元件渲染時就進行的,所以在程式碼中給data新增屬性就無法實現響應式,因為這些屬性並沒有加上setter和getter,當它被修改時無法通知Watcher物件去進行更新。

如果我們需要在元件渲染完之後去新增一個響應式的屬性,需要用Vue.$set(obj,'name',value)來為data物件中已有的物件新增屬性。也就是說data中的根屬性必須要一開始就定義好,否則無法實現響應式。

舉個例子:

我們可以通過Vue.$set方法給dialog新增一個響應式的callback屬性,但是無法新增一個響應式的data根屬性productId(假設productId這個屬性一開始沒有定義)。

2. 為什麼計算屬性也能實現響應式?

在Vue2.0中,data改變時Watcher物件呼叫render函式重新渲染,所以使用到計算屬性的地方也會被重新計算,從而實現了響應式。

3. 為什麼有時響應會有延時?

比如當我們修改資料後馬上去獲取DOM時會發現獲取到DOM似乎還沒有改變,這是因為當資料發生變化時,Vue會將資料的變化放到一個佇列中,等到下一個‘tick’再去執行DOM的更新,從而避免反覆地去更新DOM。如果我們有一些需要依賴更新後的DOM的操作,我們可以將這些操作作為回撥放到vm.$nextTick(callback)裡,這樣在下一個‘tick’就會執行我們回撥函式。

接下來看一下程式碼的具體實現。

Init

先從建立一個Vue例項開始看。

可以看到,建立Vue例項時,會呼叫this._init方法,接下來看一下this._init方法中的關鍵程式碼。

這裡面呼叫了一個initState方法,看一下initState方法幹了什麼。

可以看到,在initState方法中,會呼叫initProps,initMethods,initData,initComputed,initWatch等方法。它們會根據元件的props,methods,data,computed,watch等進行初始化。
我們主要關注initData方法。

首先,元件options中的data會被賦給vm._data,然後會執行observe(data,true),接下來看看observe方法是怎麼定義的。

Observer

如果value已經有ob物件的話,會返回value.ob,否則經過一系列判斷後(如value是否為陣列或物件,value是否可擴充,value是否為Vue例項等)使用value來建立一個Observer物件並返回。接下來看看Observer類是如何定義的。

這裡首先會給Observer物件new一個Dep物件,Dep物件是用於處理資料依賴的,它有一個id和subs(用於收集依賴)。然後def方法會通過defineProperty把該Observer物件作為ob屬性新增給value。然後判斷value是否為陣列,如果是,則呼叫observeArray方法對陣列中的元素呼叫observe方法;如果不是陣列的話會呼叫walk方法。walk方法會對value中的屬性迴圈呼叫definereactivity方法。下面看看definereactivity中的關鍵程式碼。

首先我們會對value的屬性進行observe(let childOb = !shallow && observe(val))。在definereactivity中會呼叫defineProperty方法給value設定getter和setter,這樣我們就可以攔截到value的get和賦值。也就是說當我們使用value時會呼叫getter來取值,給value賦值時會呼叫setter而不是想原來一樣直接賦值。

當getter被呼叫時,如果Dep.target不為空,則將呼叫dep.depend方法,在depend方法中會呼叫Dep.target.addDep方法(addDep是Watcher類的一個方法)將dep物件push到Dep.target的newDeps陣列中,同時會呼叫Dep類的addSub方法將Watcher物件push到Dep物件中用來記錄依賴的subs陣列中。然後會呼叫childOb.dep.depend()將Watcher物件收集到value的childOb的dep物件中。

有一個問題是,childOb的dep物件是Observer類中的dep,而當我們呼叫setter時,呼叫dep.notify()來通知依賴該資料的Watcher時,使用的是在definereactivity方法中定義的dep,所以這一步暫時意義不明,可能有別的用途。

當Watcher物件呼叫getter時,通過以上程式碼就可以將依賴該value的Watcher收集起來。

再來看看setter。setter首先是會設定新的值,然後重新observe這個新的值,最後呼叫dep.notify()通知依賴該資料的Watcher物件呼叫update方法。

Render

接下來看一下Vue元件的渲染過程。

在_init方法中,會呼叫vm.$mount方法將template或el編譯成render函式。這個生成的render函式會在vm._render方法中被呼叫,生成VNode物件。然後經過DOM Diff演算法查詢差異,生成真正的DOM樹,從而實現渲染。

下面看一下具體的實現過程,看看$mount方法到底做了什麼。

在$mount方法中,會呼叫compileToFunctions方法生成render和staticRenderFns。render就是render函式,staticRenderFns是一個陣列,包含著不會發生變化的VNode節點所生成的函式。

接下來,$mount方法會呼叫這個mount方法,而這個mount方法會呼叫mountComponent方法。

可以看到在mountComponent方法中,會建立一個新的Watcher物件,並傳入updateComponent函式,這個函式會返回vm._update(vm._render(), hydrating)

前面我們知道vm._render方法會呼叫生成的render函式來返回一個VNode物件,vm._update方法會呼叫vm.__patch__方法將這個VNode物件與之前的VNode物件比較,把差異的部分渲染到真正的DOM樹上。

Watcher

最後來看一看Watcher類的定義。

首先它會將getter設為expOrFn,從上面看到,在渲染時,這個expOrFn就是updateComponent函式。然後會呼叫get方法。

在get方法中,首先會呼叫pushTarget函式將Watcher物件設為Dep.target,然後會呼叫getter函式獲取value,也就是會呼叫vm._update(vm._render(), hydrating),從而呼叫compileToFunctions函式生成的render函式。在呼叫render函式的時候,會去獲取模板中所使用到的資料,從而觸發資料Observer的getter。

由於設定了Dep.target,所以觸發getter時,資料的Dep物件會將Watcher物件收集為依賴,這樣就完成了渲染的依賴收集。每當我們去修改響應式資料時,setter就會通過dep.notify方法來呼叫Watcher的update方法。在呼叫完getter函式後,會通過popTarget函式將Dep.target置空。

可以看到update方法中會呼叫run方法或queueWatcher方法,queueWatcher會將Watcher物件加入到佇列中,在nextTick呼叫它的run方法。所以這兩種方式最終都會呼叫Watcher物件的run方法。在run方法中會再次呼叫Watcher物件的get方法,重新取值並收集依賴。上面可以看到Watcher物件的get方法會呼叫getter函式,這個getter函式會去呼叫vm._update(vm._render(), hydrating),從而重新渲染。

這樣當我們修改資料時,就完成了響應式的DOM變化。

相關文章