Vue響應式原理源碼淺析

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

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

Vue的響應式原理是經過「觀察者/訂閱者」模式實現的。react

首先,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對象添加到依賴中。這樣咱們整一個鏈路就完成了。this

當data發生改變,就會通知這個Watcher對象去更新,這個Watcher對象就會調用render函數去從新渲染。因爲Vue2.0使用的是Virtual DOM,因此當data改變時,從新渲染的就只有改變的部分,不用擔憂整個組件從新渲染形成性能問題,因此整一個render就只須要一個Watcher對象去維護而不是像Vue1.0時那樣每個Directive對應一個Watcher對象。3d

1. 爲何有時候個人數據響應式會失效?code

因爲這個響應式的創建是在Vue組件渲染時就進行的,因此在代碼中給data添加屬性就沒法實現響應式,由於這些屬性並無加上setter和getter,當它被修改時沒法通知Watcher對象去進行更新。cdn

若是咱們須要在組件渲染完以後去添加一個響應式的屬性,須要用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變化。

相關文章
相關標籤/搜索