前言
如果自己去實現資料驅動的模式,如何解決一下幾個問題:
- 通過什麼手段去知道我的資料變了?
- 通過什麼東西去同步更新檢視?
資料劫持——obvserver
我們需要知道資料的獲取和改變,資料劫持是最基礎的手段。在Obeserver中,我們可以看到程式碼如下:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
}
})
複製程式碼
通過Object.defineProperty這個方法,我們可以在資料發生改變或者獲取的時候,插入一些自定義操作。同理,vue也是在這個方法中做依賴收集和派發更新的。
繫結和更新檢視——watcher
從初始化開始,我們渲染檢視的時候,便會生成一個watcher,他是監視檢視中引數變化以及更新檢視的。程式碼如下:
// 在mount的生命鉤子中
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製程式碼
當然,我們可以保留疑問:
- watcher是怎麼去更新檢視的
- 資料又是怎麼和watcher聯動起來的
具體的繫結和更新的流程,我們到後續的依賴收集中講解。
我們先來講講響應式系統中涉及到的設計模式。
釋出訂閱模式
在釋出訂閱模式中,釋出者和訂閱者之間多了一個釋出通道;一方面從釋出者接收事件,另一方面向訂閱者釋出事件;訂閱者需要從事件通道訂閱事件
以此避免釋出者和訂閱者之間產生依賴關係
vue的響應式流程
vue的響應式系統借鑑了資料劫持和釋出訂閱模式。
Vue用Dep作為一箇中間者,解藕了Observer和Watcher之間的關係,使得兩者的職能更加明確。
那具體是如何來完成依賴收集和訂閱更新的呢?
依賴收集過程
- 依賴收集的流程
舉個例子
<div id="app">
{{ message }}
{{ message1 }}
<input type="text" v-model="message">
<div @click="changeMessage">改變message</div>
</div>
複製程式碼
var app = new Vue({
el: '#app',
data: {
message: '1',
message1: '2',
},
methods: {
changeMessage() {
this.message = '2'
}
},
watch: {
message: function(val) {
this.message1 = val
}
}
})
複製程式碼
依賴收集流程圖:
如何看懂這個依賴收集流程?關鍵在watcher程式碼中:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
// 省略
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
複製程式碼
呼叫的這個this.getter有兩種,一種是key值的getter方法,還有一種是expOrFn,比如mounted中傳入的updateComponent。
- 如何防止重複收集
我們不妨想想什麼才算是重複收集了?
筆者想到一種情況:就是dep陣列中,出現了多個一樣的watcher。
比如renderWatch就容易被重複收集,因為我們在html模版中,會重複使用data中的某個變數。那他是如何去重的呢?
1、只有watch在執行get時,觸發的取數操作,才會被收集
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
})
複製程式碼
當只有Dep.target這個存在的時候才進行依賴收集。Dep.target這個值只有在watcher執行get方法的時候才會存在。
2、在dep.depend的時候會判斷watch的id
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
複製程式碼
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
複製程式碼
我們會發現,在depend過程中,會有一個newDepIds去記錄已經存入的dep的id,當一個watcher已經被該dep存過時,便不再會進行依賴收集操作。
派發更新過程
收集流程講完了,不妨在聽聽更新流程。
- 訂閱更新的流程 老例子
<div id="app">
{{ message }}
{{ message1 }}
<input type="text" v-model="message">
<div @click="changeMessage">改變message</div>
</div>
複製程式碼
var app = new Vue({
el: '#app',
data: {
message: '1',
message1: '2',
},
methods: {
changeMessage() {
this.message = '3'
}
},
watch: {
message: function(val) {
this.message1 = val
}
}
})
複製程式碼
依賴收集的最終結果:
當觸發click事件的時候,便會觸發訂閱更新流程。
訂閱更新流程圖:
當renderWatch執行更新的時候,回去呼叫beforeUpdate生命鉤子,然後執行patch方法,進行檢視的變更。
- 如何防止重複更新
如何去防止重複更新呢?renderWatch會被很多dep進行收集,如果檢視多次渲染,會造成效能問題。
其實問題的關在在於——queueWatcher
在queueWatcher中有兩個操作:去重和非同步更新。
function queueWatcher (watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
queue.push(watcher)
// ...
if (!waiting) {
waiting = true
// ...
nextTick(flushSchedulerQueue)
}
}
}
複製程式碼
其實queueWatcher很簡單,將所有watch收集到一個陣列當中,然後去重。
這樣至少可以避免renderWatch頻繁更新。
比如上述例子中的,message和message1都有一個renderWatch,但是隻會執行一次。
非同步更新也可以保證當一個事件結束之後,才會觸發檢視層的更新,也能防止renderWatch重複更新
結尾
文章講述了響應式流程的原因,程式碼細節並未深入,如果喜歡瞭解原始碼的,可以翻看筆者其他的文章:
Watcher原始碼解析(未完成)