Vue.js 的核心包括一套「響應式系統」。「響應式」,是指當數據改變後,Vue 會通知到使用該數據的代碼。例如,視圖渲染中使用了數據,數據改變後,視圖也會自動更新。html
舉個簡單的例子,對於模板:vue
{{ name }}react
建立一個 Vue 組件:數組
var vm = new Vue({ el: '#root', data: { name: 'luobo' } })
代碼執行後,頁面上對應位置會顯示:luobo。瀏覽器
若是想改變顯示的名字,只須要執行:緩存
vm.name = 'tang'
這樣頁面上就會顯示修改後的名字了,並不須要去手動修改 DOM 更新數據。app
接下來,咱們就一塊兒深刻了解 Vue 的數據響應式原理,搞清楚響應式的實現機制。異步
基本概念ide
Vue 的響應式,核心機制是 觀察者模式。函數
數據是被觀察的一方,發生改變時,通知全部的觀察者,這樣觀察者能夠作出響應,好比,從新渲染而後更新視圖。
咱們把依賴數據的觀察者稱爲 watcher,那麼這種關係能夠表示爲:
data -> watcher
數據能夠有多個觀察者,怎麼記錄這種依賴關係呢?
Vue 經過在 data 和 watcher 間建立一個 dep 對象,來記錄這種依賴關係:
data - dep -> watcher
dep 的結構很簡單,除了惟一標識屬性 id,另外一個屬性就是用於記錄全部觀察者的 subs:
1.id - number
2.subs - [Watcher]
再來看 watcher。
Vue 中 watcher 的觀察對象,確切來講是一個求值表達式,或者函數。這個表達式或者函數,在一個 Vue 實例的上下文中求值或執行。這個過程當中,使用到數據,也就是 watcher 所依賴的數據。用於記錄依賴關係的屬性是 deps,對應的是由 dep 對象組成的數組,對應全部依賴的數據。而表達式或函數,最終會做爲求值函數記錄到 getter 屬性,每次求值獲得的結果記錄在 value 屬性:
1.vm - VueComponent
2.deps - [Dep]
3.getter - function
4.value - *
另外,還有一個重要的屬性 cb,記錄回調函數,當 getter 返回的值與當前 value 不一樣時被調用:
1.cb - function
咱們經過示例來整理下 data、dep、watcher 的關係:
var vm = new Vue({ data: { name: 'luobo', age: 18 } }) var userInfo = function () { return this.name + ' - ' + this.age } var onUserInfoChange = function (userInfo) { console.log(userInfo) } vm.$watch(userInfo, onUserInfoChange)
上面代碼首先建立了一個新的 Vue 實例對象 vm,包含兩個數據字段:name、age。對於這兩個字段,Vue 會分別建立對應的 dep 對象,用於記錄依賴該數據的 watcher。
而後定義了一個求值函數 userInfo,注意,這個函數會在對應的 Vue 示例上下文中執行,也就是說,執行時的 this 對應的就是 vm。
回調函數 onUserInfoChange 只是打印出新的 watcher 獲得的新的值,由 userInfo 執行後生成。
經過 vm.$watch(userInfo, onUserInfoChange),將 vm、getter、cb 集成在一塊兒建立了新的 watcher。建立成功後,watcher 在內部已經記錄了依賴關係,watcher.deps 中記錄了 vm 的 name、age 對應的 dep 對象(由於 userInfo 中使用了這兩個數據)。
接下來,咱們修改數據:
vm.name = 'tang'
執行後,控制檯會輸出:
tang - 18
一樣,若是修改 age 的值,也會最終觸發 onUserInfoChange 打印出新的結果。
用個簡單的圖來整理下上面的關係:
vm.name -- dep1 vm.age -- dep2 watcher.deps --> [dep1, dep2]
修改 vm.name 後,dep1 通知相關的 watcher,而後 watcher 執行 getter,獲得新的 value,再將新的 value 傳給 cb:
vm.name -> dep1 -> watcher -> getter -> value -> cb
可能你也注意到了,上面例子中的 userInfo,貌似就是計算屬性的做用嘛:
var vm = new Vue({ data: { name: 'luobo', age: 18 }, computed: { userInfo() { return this.name + ' - ' + this.age } } })
其實,計算屬性在內部也是基於 watcher 實現的,每一個計算屬性對應一個 watcher,其 getter 也就是計算屬性的聲明函數。
不過,計算屬性對應的 watcher 與直接經過 vm.$watch() 建立的 watcher 略有不一樣,畢竟若是沒有地方使用到這個計算屬性,數據改變時都從新進行計算會有點浪費,這個在本文後面會講到。
上面描述了 data、dep、watcher 的關係,可是問題來了,這種依賴關係是如何創建的呢?數據改變後,又是如何通知 watcher 的呢?
接下來咱們深刻 Vue 源碼,搞清楚這兩個問題。
創建依賴關係
Vue 源碼版本 v2.5.13,文中摘錄的部分代碼爲便於分析進行了簡化或改寫。
響應式的核心邏輯,都在 Vue 項目的 「vue/src/core/observer」 目錄下面。
咱們仍是先順着前面示例代碼來捋一遍,首先是 Vue 實例化過程:
var vm = new Vue(/* ... */)
跟將傳入的 data 進行響應式初始化相關的代碼,在 「vue/src/core/instance/state.js」 文件中:
observer/state.js#L149
// new Vue() -> ... -> initState() -> initData() observe(data)
函數 observe() 的目的是讓傳入的整個對象成爲響應式的,它會遍歷對象的全部屬性,而後執行:
observer/index.js#L64
// observe() -> new Observer() -> observer.walk() defineReactive(obj, key, value)
defineReactive() 就是用於定義響應式數據的核心函數。它主要作的事情包括:
1.新建一個 dep 對象,與當前數據對應
2.經過 Object.defineProperty() 從新定義對象屬性,配置屬性的 set、get,從而數據被獲取、設置時能夠執行 Vue 的代碼
OK,先到這裏,關於 Vue 實例化告一段落。
須要要注意的是,傳入 Vue 的 data 的全部屬性,會被代理到新建立的 Vue 實例對象上,這樣經過 vm.name 進行操做的其實就是 data.name,這也是藉助 Object.defineProperty() 實現的。
再來看 watcher 的建立過程:
vm.$watch(userInfo, onUserInfoChange)
上述代碼執行後,會調用:
instance/state.js#L346
// Vue.prototype.$watch() new Watcher(vm, expOrFn, cb, options)
也就是:
new Watcher(vm, userInfo, onUserInfoChange, {/* 略 */})
在 watcher 對象建立過程當中,除了記錄 vm、getter、cb 以及初始化各類屬性外,最重要的就是調用了傳入的 getter 函數:
observer/watcher.js#L103
// new Watcher() -> watcher.get() value = this.getter.call(vm, vm)
在 getter 函數的執行過程當中,獲取讀取須要的數據,因而觸發了前面經過 defineReactive() 配置的 get 方法:
if (Dep.target) { dep.depend() }
回到 watcher.get() 方法,在執行 getter 函數的先後,分別有以下代碼:
pushTarget(this) // ... value = this.getter.call(vm, vm) // ... popTarget()
pushTarget() 將當前 watcher 設置爲 Dep.target,這樣在執行到 vm.name 進一步執行對應的 get 方法時,Dep.target 的值就是這裏的 watcher,而後經過 dep.depend() 就創建了依賴關係。
dep.depend() 執行的邏輯就比較好推測了,將 watcher(經過 Dep.target 引用到)記錄到 dep.subs 中,將 dep 記錄到 watcher.deps 中 —— 依賴關係創建了!
而後來看創建的依賴關係是如何使用的。
數據變動同步
繼續前面的例子,執行以下代碼時:
vm.name = 'tang'
會觸發經過 defineReactive() 配置的 set 方法,若是數據改變,那麼:
// defineReactive() -> set() dep.notify()
經過 dep 對象來通知全部的依賴方法,因而 dep 遍歷內部的 subs 執行:
// dep.notify() watcher.update()
這樣 watcher 就被通知到了,知道了數據改變,從而繼續後續的處理。這裏先不展開。
到這裏,基本就搞清楚響應式的基本機制了,整理一下:
1.經過 Object.defineProperty() 替換配置對象屬性的 set、get 方法,實現「攔截」
2.watcher 在執行 getter 函數時觸發數據的 get 方法,從而創建依賴關係
3.寫入數據時觸發 set 方法,從而藉助 dep 發佈通知,進而 watcher 進行更新
這樣再看 Vue 官方的圖就比較好理解了:
圖片來源:https://vuejs.org/v2/guide/reactivity.html
上圖中左側是以組件渲染(render)做爲 getter 函數來演示響應式過程的,這其實就是 RenderWatcher 這種特殊類型 watcher 的做用機制,後面還會再講。
計算屬性
本文前面提到過計算屬性,在 Vue 中也是做爲 watcher 進行處理的。計算屬性(ComputedWatcher)特殊的地方在於,它其實沒有 cb(空函數),只有 getter,而且它的值只在被使用時才計算並緩存。
首先,ComputedWatcher 在建立時,不會當即執行 getter(lazy 選項值爲 false),這樣一開始 ComputedWatcher 並無和使用到的數據創建依賴關係。
計算屬性在被「get」時,首先執行預先定義的 ComputedGetter 函數,這裏有一段特殊邏輯:
instance/state.js#L238
function computedGetter () { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value }
首先判斷 watcher 是否是 dirty 狀態,什麼意思呢?
計算屬性對應的 watcher 初始建立的時候,並無執行 getter,這個時候就會設置 dirty 爲 true,這樣當前獲取計算屬性的值的時候,會執行 getter 獲得 value,而後標記 dirty 爲 false。這樣後續再獲取計算屬性的值,不須要再計算(執行 getter),直接就能返回緩存的 value。
另外,計算屬性的 watcher 在執行 watcher.evaluate() 是,進一步調用 watcher.get(),從而進行依賴收集。而依賴的數據在改變後,會通知計算屬性的 watcher,可是 watcher 只是標記自身爲 dirty,而不計算。這樣的好處是能夠減少開銷,只在有地方須要計算屬性的值時才執行計算。
若是依賴的數據發生變動,計算屬性只是標記 dirty 爲 true,會不會有問題呢?
解決這個問題的是上面代碼的這一部分:
if (Dep.target) { watcher.depend() }
也就是說,若是當前有在收集依賴的 watcher,那麼當前計算屬性的 watcher 會間接地經過 watcher.depend() 將依賴關係「繼承」給這個 watcher(watcher.depend() 內部是對每一個 watcher.deps 記錄的 dep 執行 dep.depend() 從而讓依賴數據與當前的 watcher 創建依賴關係)。
因此,依賴數據改變,依賴計算屬性的 watcher 會直接獲得通知,再來獲取計算屬性的值的時候,計算屬性才進行計算求值。
因此,依賴計算屬性的 watcher 能夠視爲依賴 watcher 的 watcher。這樣的 watcher 在 Vue 中最多見不過,那就是 RenderWatcher。
RenderWatcher 及異步更新
相信讀過前文,你應該對 Vue 響應式原理有基本的認識。那麼 Vue 是如何將其運用到視圖更新中的呢?答案就是這裏要講的 RenderWatcher。
RenderWatcher 首先是 watcher,只不過和計算屬性對應的 ComputedWatcher 相似,它也有些特殊的行爲。
RenderWatcher 的建立,在函數 mountComponent 中:
// Vue.prototype.$mount() -> mountComponent() let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
核心代碼就在這裏了。這個 watcher 就是 Vue 實例對象惟一的 RenderWatcher,在 watcher 構造函數中,會記錄到 vm._watcher 上(普通 watcher 只會記錄到 vm._watchers 數組中)。
這個 watcher 也會在建立的最後執行 watcher.get(),也就是執行 getter 收集依賴的過程。而在這裏,getter 就是 updateComponent,也就是說,執行了渲染+更新 DOM!而且,這個過程當中使用到的數據也被收集了依賴關係。
那麼,理所固然地,在 render() 中使用到數據,發生改變,天然會通知到 RenderWatcher,從而最終更新視圖!
不過,這裏會有個疑問:若是進行屢次數據修改,那麼豈不是要頻繁執行 DOM 更新?
這裏就涉及到 RenderWatcher 的特殊功能了:異步更新。
結合前面內容,咱們知道數據更新後,依賴該數據的 watcher 會執行 watcher.update(),這個在前文中沒有展開,如今咱們來看下這個方法:
observer/watcher.js#L161
if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) }
第一種狀況,lazy 爲 true,也就是計算屬性,上一節已經提到過,只是標記 dirty 爲 true,並不當即計算,再也不贅述。sync 爲 true 的狀況,這裏也無論,不過看起來也很簡單,就是當即執行計算嘛。
最後的狀況,就是這裏 RenderWatcher 的場景,並不當即執行,也不是像計算屬性那樣標記爲 dirty 就完了,而是放到了一個隊列中。
這個隊列是幹什麼的呢?
相關代碼在 observer/scheduler.js 中,簡單來講,就是實現了異步更新。
理解其實現,首先要對瀏覽器的事件循環(Event Loop)機制有必定了解。若是你對事件循環機制不是很瞭解,能夠看下面這篇文章:
JavaScript 運行機制詳解:
事件循環機制其實有點複雜,但只有理解事件循環,才能對這裏 Vue 異步更新的方案有深刻的認識。
基於事件循環機制,RenderWatcher 將其 getter,也就是 updateComponent 函數異步執行,而且,屢次觸發
RenderWatcher 的 update(),最終也只會執行一次 updateComponent,這樣也就解決了性能問題。
不過,隨之而來的新問題是,修改完數據,不能直接反應到 DOM 上,而是要等異步更新執行事後才能夠,這也是爲何 Vue 提供了 nextTick() 接口,而且要求開發者將對 DOM 的操做放到 nextTick() 回調中執行的緣由。
Vuex、Vue-Router
再來看 Vue 套裝中的 Vuex、Vue-Router,它們也是基於 Vue 的響應式機制實現功能。
先來看 Vuex,代碼版本 v3.0.1。
Vuex
在應用了 Vuex 的應用中,全部組件均可以經過 this.$store 來引用到全局的 store,而且在使用了 store 的數據後,還能在數據改變後獲得同步,這其實就是響應式的應用了。
首先看 this.$store 的實現,這個實際上是經過全局 mixin 實現,代碼在:
src/mixin.js#L26
this.$store = options.store || options.parent.$store
這樣在每一個組件的 beforeCreate 時,會執行 $store 屬性的初始化。
而 store 數據的響應式處理,則是經過實例化一個 Vue 對象實現:
src/store.js#L251
// new Store() -> resetStoreVM() store._vm = new Vue({ data: { $$state: state }, computed // 對應 store.getters })
結合前文的介紹,這裏就很好理解了。由於 state 以及處理爲響應式數據,而 getters 也建立爲計算屬性,因此對這些數據的使用,就創建依賴關係,從而能夠響應數據改變了。
Vue-Router
Vue-Router 中,比較重要的數據是 $route,即當前的頁面路由數據,在路由改變的時候,須要替換展現不一樣組件(router-view 組件實現)。
vm.$route 實踐上是來自 Vue.prototype,但其對應的值,最終對應到的是 router.history.current。
結合前面的分析,這裏的 history.current 確定得是響應式數據,因此,來找下對其進行初始化的地方,實際上是在全局 mixin 的 beforeCreate 這裏:
v2.8.1/src/install.js#L27
// beforeCreate Vue.util.defineReactive(this, '_route', this._router.history.current)
這樣 this._route 就是響應式的了,那麼若是頁面路由改變,又是如何修改這裏的 _route 的呢?
答案在 VueRouter 的 init() 這裏:
history.listen(route => { this.apps.forEach((app) => { app._route = route }) })
一個 router 對象可能和多個 vue 實例對象(這裏叫做 app)關聯,每次路由改變會通知全部的實例對象。
再來看使用 vm.$route 的地方,也就是 VueRouter 的兩個組件:
兩個組件都是在 render() 中,與 $route 創建了依賴關係,根據 route 的值進行渲染。這裏具體過程就不展開了,感興趣能夠看下相關源碼(v2.8.1/src/components),原理方面在 RenderWatcher 一節已經介紹過。
實踐:watch-it
瞭解了以上這麼多,也想本身試試,把 Vue 響應式相關的核心邏輯剝離出來,作一個單純的數據響應式的庫。因爲只關注數據,因此在剝離過程當中,將與 Vue 組件/實例對象相關的部分都移除了,包括 watcher.vm 也再也不須要,這樣 watcher.getter 計算時再也不指定上下文對象。
感興趣,想直接看代碼的,能夠前往 luobotang/watch-it。
watch-it 只包括數據響應式相關的功能,暴露了4個接口:
1.defineReactive(obj, key, val):爲對象配置一個響應式數據屬性
2.observe(obj):將一個數據對象配置爲響應式,內部對全部的屬性執行 defineReactive
3.defineComputed(target, key, userDef):爲對象配置一個計算屬性,內部建立了 watcher
4.watch(fn, cb, options):監聽求值函數中數據改變,變化時調用 cb,內部建立了 watcher
來看一個使用示例:
const { observe, watch } = require('@luobotang/watch-it') const data = { name: 'luobo', age: 18 } observe(data) const userInfo = function() { return data.name + ' - ' + data.age } watch(userInfo, (value) => console.log(value))
這樣,當數據修改時,經過會打印出新的 userInfo 的值。