本文同步發表在 https://github.com/whxaxes/blog/issues/7javascript
在一篇文章中簡單講了 vue 是如何把模板解析成 render function 的,這一篇文章就來說講 vue 是如何把數據包裝成 reactive,從而實現 MDV(Model-Driven-View) 的效果。vue
先說明一下什麼叫 reactive,簡單來講,就是將數據包裝成一種可觀測的類型,當數據產生變動的時候,咱們可以感知到。java
而 Vue 的相關實現代碼所有都在 core/observer
目錄下,而要自行閱讀的話,建議從 core/instance/index.js
中開始。node
在開始講 reactive 的具體實現以前,先說說幾個對象:Watcher、Dep、Observer。react
Watcher 是 vue 實現的一個用於觀測數據的對象,具體實如今 core/observer/watcher.js
中。git
這個類主要是用來觀察方法/表達式
中引用到的數據(數據須要是 reative 的,即 data 或者 props)變動,當變動後作出相應處理。先看一下實例化 Watcher 這個類須要傳的入參有哪些:github
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object )
能夠看到,有四個入參可供選擇,其中 options 是非必傳的,解釋一下這幾個入參是幹嗎的:oop
vm
:當前這個 watcher 所屬的 VueComponent。expOrFn
:須要監聽的 方法/表達式。舉個例子:VueComponent 的 render function,或者是 computed property 的 getter 方法,再或者是abc.bbc.aac
這種類型的字符串(因爲 vue 的 parsePath 方法是用 split('.') 來作的屬性分割,因此不支持abc['bbc']
)。expOrFn 若是是方法,則直接賦值給 watcher 的 getter 屬性,若是是表達式,則會轉換成方法再給 getter。cb
:當 getter 中引用到的 data 發生改變的時候,就會觸發該回調。options
:額外參數,能夠傳入的參數爲包括deep
、user
,lazy
,sync
,這些值默認都是爲 false。
deep
若是爲 true,會對 getter 返回的對象再作一次深度遍歷,進行進一步的依賴收集,好比 $watch 一個對象,若是 deep 爲 true,那麼當這個對象裏的元素更改,也會觸發 callback。user
是用於標記這個監聽是否由用戶經過 $watch 調用的。lazy
用於標記 watcher 是否爲懶執行,該屬性是給 computed property 用的,當 data 中的值更改的時候,不會當即計算 getter 獲取新的數值,而是給該 watcher 標記爲 dirty,當該 computed property 被引用的時候纔會執行從而返回新的 computed property,從而減小計算量。sync
則是表示當 data 中的值更改的時候,watcher 是否同步更新數據,若是是 true,就會當即更新數值,不然在 nextTick 中更新。其實,只要瞭解了入參是用來幹嗎的以後,也就基本上知道 Watcher 這個對象幹了啥或者是須要幹啥了。this
Dep 則是 vue 實現的一個處理依賴關係的對象,具體實如今 core/observer/dep.js
中,代碼量至關少,很容易理解。lua
Dep 主要起到一個紐帶的做用,就是鏈接 reactive data 與 watcher,每個 reactive data 的建立,都會隨着建立一個 dep 實例。參見 observer/index.js 中的 defineReactive
方法,精簡的 defineReactive 方法以下。
function defineReactive(obj, key, value) { const dep = new Dep(); Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend(); } return value } set(newValue) { value = newValue; dep.notify(); } }) }
建立完 dep 實例後,就會在該 data 的 getter 中注入收集依賴的邏輯,同時在 setter 中注入數據變動廣播的邏輯。
所以當 data 被引用的時候,就會執行 getter 中的依賴收集,而何時 data 會被引用呢?就是在 watcher 執行 watcher.getter 方法的時候,在執行 getter 以前 watcher 會被塞入 Dep.target,而後經過調用 dep.depend() 方法,這個數據的 dep 就和 watcher 建立了鏈接,執行 getter 完成以後再把 Dep.target 恢復成此前的 watcher。
建立鏈接以後,當 data 被更改,觸發了 setter 邏輯。而後就能夠經過 dep.notify() 通知到全部與 dep 建立了關聯的 watcher。從而讓各個 watcher 作出響應。
好比我 watch 了一個 data ,而且在一個 computed property 中引用了同一個 data。再同時,我在 template 中也有顯式引用了這個 data,那麼此時,這個 data 的 dep 裏就關聯了三個 watcher,一個是 render function 的 watcher,一個是 computed property 的 watcher,一個是用戶本身調用 $watch 方法建立的 watcher。當 data 發生更改後,這個 data 的 dep 就會通知到這三個 watcher 作出相應處理。
Observer 能夠將一個 plainObject 或者 array 變成 reactive 的。代碼不多,就是遍歷 plainObject 或者 array,對每個鍵值調用 defineReactive
方法。
以上三個類介紹完了,基本上對 vue reactive 的實現應該有個模糊的認識,接下來,就結合實例講一下整個流程。
在 vue 實例化的時候,會先調用 initData,再調用 initComputed,最後再調用 mountComponent 建立 render function 的 watcher。從而完成一個 VueComponent 的數據 reactive 化。
initData 方法在 core/instance/state.js 中,而這個方法裏大部分都是作一些判斷,好比防止 data 裏有跟 methods 裏重複的命名之類的。核心其實就一行代碼:
observe(data, true)
而這個 observe 方法乾的事就是建立一個 Observer 對象,而 Observer 對象就像我上面說的,對 data 進行遍歷,而且調用 defineReactive 方法。
就會使用 data 節點建立一個 Observer 對象,而後對 data 下的全部數據,依次進行 reactive 的處理,也就是調用 defineReactive
方法。當執行完 defineReactive 方法以後,data 裏的每個屬性,都被注入了 getter 以及 setter 邏輯,而且建立了 dep 對象。至此 initData 執行完畢。
而後是 initComputed 方法。這個方法就是處理 vue 中 computed 節點下的屬性,遍歷 computed 節點,獲取 key 和 value,建立 watcher 對象,若是 value 是方法,實例化 watcher 的入參 expOrFn 則爲 value,不然是 value.get。
function initComputed (vm: Component, computed: Object) { ... const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] let getter = typeof userDef === 'function' ? userDef : userDef.get ... watchers[key] = new Watcher(vm, getter, noop, { lazy: true }) if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { ... } } }
咱們知道 expOrFn 是能夠爲方法,也能夠是字符串的。所以,經過上面的代碼咱們發現了一種官方文檔裏沒有說明的用法,好比個人 data 結構以下
{ obj: { list: [{value: '123'}] } }
若是咱們要在 template 中須要使用 list 中第一個節點的 value 屬性 值,就寫個 computed property:
computed: { value: { get: 'obj.list.0.value' } }
而後在 template 中使用的時候,直接用{{ value }}
,這樣的話,就算 list 爲空,也能保證不會報錯,相似於 lodash.get 的用法,例子 https://jsfiddle.net/wanghx/n5r1vj1o/1/ 。
扯遠了,回到正題上。
建立完 watcher,就經過 Object.defineProperty 把 computed property 的 key 掛載到 vm 上。而且在 getter 中添加如下邏輯
if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value
前面我有說過,computed property 的 watcher 是 lazy 的,當 computed property 中引用的 data 發生改變後,是不會立馬從新計算值的,而只是標記一下 dirty 爲 true,而後當這個 computed property 被引用的時候,上面的 getter 邏輯就會判斷 watcher 是否爲 dirty,若是是,就從新計算值。
然後面那一段watcher.depend
。則是爲了收集 computed property 中用到的 data 的依賴,從而可以實現當 computed property 中引用的 data 發生更改時,也能觸發到 render function 的從新執行。
depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } }
把 data 以及 computed property 都初始化好以後,則建立一個 render function 的 watcher。邏輯以下:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el ... callHook(vm, 'beforeMount') let updateComponent ... updateComponent = () => { vm._update(vm._render(), hydrating) } ... vm._watcher = new Watcher(vm, updateComponent, noop) if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
能夠看到,建立 watcher 時候的入參 expOrFn 爲 updateComponent 方法,而 updateComponent 方法中則是執行了 render function。而這個 watcher 不是 lazy 的,所以建立該 watcher 的時候,就會立馬執行 render function 了,當執行 render function 的時候。若是 template 中有使用 data,則會觸發 data 的 getter 邏輯,而後執行 dep.depend() 進行依賴收集,若是 template 中有顯式使用 computed property,也會觸發 computed property 的 getter 邏輯,從而再收集 computed property 的方法中引用的 data 的依賴。最終完成所有依賴的收集。
最後舉個例子:
<template> <div>{{ test }}</div> </template> <script> export default { data() { return { name: 'cool' } }, computed: { test() { return this.name + 'test'; } } } </script>
至此,vue 的 reactive 是怎麼實現的,就講完了。