Vue2.x源碼解析系列五:數據響應之Watcher

NO IMAGE

寫在前面的話:關於作者言川

筆名言川, 前端工程師,精通 Vue/Webpack/Git等,熟悉Node/React等,涉獵廣泛,對算法/後端/人工智能/linux等都有一定研究。開源愛好者,github上目前總計5000+ Star

  • 我的github主頁:https://github.com/lihongxun945
  • 我的博客地址:https://github.com/lihongxun945/myblog
  • 我的掘金主頁:https://juejin.im/user/1398234518660551/posts
  • 我的知乎專欄:https://zhuanlan.zhihu.com/c_1007281871281090560

此博客原地址:https://github.com/lihongxun945/myblog/issues/27

computed 說起

為了弄懂 Watcher 我們需要選擇一個切入點,這次我們選擇從 computed 為切入點來講解。這個是大家非常常用的功能,而且他能比較好的解釋我們是如何檢測到狀態變化並獲取最新值的。我們先假設我們有如下組件:

export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
upperMsg () {
return this.msg.toUpperCase()
}
}
}

我們有 data.msgcomputed.upperMsg 兩個自定義的數據。顯然,upperMsg 依賴於 msg,當msg 更新的時候,upperMsg 也會更新。根據上一章的講解,我們知道通過 Observer 我們可以監控 msg 的讀寫,那麼如何和 upperMsg 關聯起來呢?

Vue2.x源碼解析系列五:數據響應之Watcher

Watcher 就是把這兩者連接起來的關鍵,我們來看看 initWatcher 的代碼如何工作的。完整代碼如下:

core/observer/watcher.js

function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}

為了方便起見,我們把在開發環境下的一些友好警告刪除,並刪除一些不影響我們邏輯的代碼,再看看代碼:

我們一行一行的來看代碼,為了方便起見,我們把在開發環境下的一些友好警告跳過,也跳過一些不影響我們邏輯和理解代碼意思的幾行。

首先是開頭兩行代碼:

var watchers = vm._computedWatchers = Object.create(null);
var isSSR = isServerRendering();

這兩行代碼定義了兩個變量,watchers 是空的對象,顯然是用來存儲接下來創建的 watchersisSSR 表示是否是服務器端渲染,因為如果是在服務器端渲染,就沒有必要進行監聽了,我們暫且不考慮服務器端的內容。

接下來是一個 for 循環,會遍歷 computed對象,循環體的第一段代碼如下:

    var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}

這裡的 getter 就是我們的 upperMsg 函數,不過他處理了我們通過 getter 來定義的情況。有了 getter 之後,就會對我們定義的每一個key創建一個Watcher。這裡是我們要講解的重點。我們暫且跳入 watcher 的構造函數中看看,在文件 core/observer/watcher 中。

深入 Watcher 類

完整的構造函數代碼如下:

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}

代碼雖然有些長,但是大部分代碼都是一些屬性的初始化,其中比較重要的幾個是:

  • lazy 如果設置為 true 則在第一次 get 的時候才計算值,初始化的時候並不計算。默認值為 true
  • deps,newDeps, depIds, newDepIds 記錄依賴,這是我們要講的重點
  • expOrFn 我們的表達式本身

除了這些屬性的設置之外,只有最後一行代碼:

    this.value = this.lazy
? undefined
: this.get()

注意這個設計 this.value , Vue 的設計上,Watcher 不止會監聽 Observer ,而且他會直接把值計算出來放在 this.value 上。雖然這裡因為 lazy 沒有直接計算,但是取值的時候肯定要計算的,所以我們直接看看 getter 的代碼:

Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
``
這裡我們看到了熟悉的 `pushTarget` 函數,不過這次不是清除了,而是真的把 `this` 作為一個參數傳進去,那麼結果就是 `Dep.target === this`。忘記這一塊的童鞋,我直接把 `pushTarget` 代碼再貼一遍:
```js
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

當我們取 upperMsg 的值的時候,全局的 Dep.target 就變成了 upperMsg 對應的 watcher 實例了。接下來就可以直接取值了:

value = this.getter.call(vm, vm)

這樣,我們執行了 upperMsg 函數,取到了 msg 的大寫字符串。而在 getter 函數中,我們有這樣的代碼 this.msg 會讀取 msg 的值,因此,他會跳入 defineReactive 中的 getter 函數。

再回顧下我們在 defineReactive 中的代碼:

get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// 省略
}
}

此時的 value 肯定是 msg 的值,重點是 if 函數,因為 Dep.target 就是我們為 upperMsg 創建的 watcher 實例,所以此時會執行 dep.depend() 函數,這個函數如下:

Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};

代碼就一行,因為 Dep.target 就是 watcher,所以這行代碼等價於 watcher.addDep(dep).讓我們看看 addDep 函數:

Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};

當執行 addDep 的時候會把 dep 存起來,不過這裡會有之前初始化的兩個數組 depsnewDeps,以及 depIdsnewDepIds 兩個 set。其實大家一看就能明白,這裡明顯是用來去重的,特別是其中的 depIdsnewDepIds 是一個 Set

但是這個去重的邏輯有些複雜,因為包含了兩個 if,分別對 depIdsnewDepIds 進行去重。那麼為什麼要進行兩次去重呢? 舉個栗子說明,我們首先假設我們有這樣一個計算屬性:

computed: {
doubleMsg () {
return this.msg + this.msg
}
}

這裡進行了兩次 this.msg 取值,那麼顯然會觸發兩次 getter 函數,而 getter 中的 dep.depend() 調用並沒有判斷任何重複條件,所以為了計算一個 doubleMsg 會兩次進入 Watcher.prototype.addDep 函數。而第二次進入的時候,由於 newDepIds 已經記錄了 dep 實例的id,因此會直接忽略。那麼為什麼第二次進入的時候 dep 和第一次是同一個呢?因為 dep 是在getter/setter外面的閉包中的,對當前 msg 來說是唯一的。

我們弄懂了 newDepIds 是怎麼去重的,那麼裡面的那個 if 中使用了 depIds 去重,又是怎麼回事呢?我們首先看看哪裡用到了 newDepIds,其實是在 Watcher.protototype.cleanupDeps 函數中,而這個函數是在 Watcher.prototype.get 中調用的,我們看看 get 的代碼中的 finally 是怎麼寫的:

finally {
// 省略
this.cleanupDeps();
}

也就是在 get 取到值後,就調用 this.cleanupDeps ,這個函數會把 newDepIds 的值賦給 depIds,然後把 newDepIds 清空。

當Vue對 doubleMsg 進行求值的時候,會調用兩次 this.msg,求值結束後,會進行 this.cleanupDeps 操作。這樣求值結束之後,我們的依賴就存在於 depIds 而不是 newDepIds 中。知道了這一點之後就比較好理解了。newDepIds 只是在對 doubleMsg 進行求值的過程中,避免對 msg 的多次依賴。當求值結束之後,newDepIds 就空了嗎,而依賴被記錄在 depIds 中。如果我們在第一次對 doubleMsg 求值之後,再次進行求值會怎麼樣呢? 比如我們這樣:

mounted () {
this.msg = 'aaaa'
}

$mount 結束後對 this.msg 進行賦值,那麼就會觸發 watcher.update方法,而這裡面會進行再次進行 this.msg 求值。此時,newDepIds 為空,而 depIds 有值,因此不會被重複記錄依賴。

所以總結下來就是:

  • newDepIds 可以在 upperMsg 的一次求值過程中,避免對 msg 的重複依賴
  • depIds 可以在由於 msg 更新而導致再次對 doubleMsg 求值的時候,避免對 msg 的重複依賴

搞懂了去重代碼之後,最主要的一行代碼就是 dep.addSub(this)。也就是會把 watcher 添加到 dep.subs 中。

到目前為止,我們能做到 一旦 調用 this.upperMsg 讀取值,就會觸發依賴收集。那麼當 msg 被更新的時候,watcher.value 又是怎麼知道而更新的呢?還是先看 defineReactive 中的 setter 定義:

set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}

其中最重要的是最後一行代碼 dep.notify 而這行代碼就會去通知所有的 watchernotify 代碼如下:

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

他會調用 watcher.update 來更新 value ,這樣當我們給 msg 設置了一個新的值,watcher.value 就會自動被更新。因為性能問題,watcher.update 函數默認是異步更新的,我們看看代碼:

update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

裡面有很多註釋,前幾行是處理當有其他的值依賴我們的 upperMsg 的情況的,我們下面會講到,這裡暫且跳過。直接看最後幾行代碼:

 if (this.sync) {
this.run()
} else {
queueWatcher(this)
}

如果是 sync 模式,那麼直接調用 run 來更新 value。默認情況是異步的,所以會進入 queueWatcher(this) 方法,會把 run 的運行推遲到 nextTick 才運行。這也是我們為什麼更新了 msg 之後立刻讀取 upperMsg 其實內容並沒有被更新的原因。因為把所有的更新都集中到 nextTick 進行,所以 Vue 會有比較好的性能。queueWatcher 其實比較簡單,他會用一個隊列記錄所有的操作,然後在 nextTick 的時候統一調用一次。這裡就不做過多介紹了,我們會有單獨的一章來介紹。

到這裡我們已經弄懂了 upperMsg 是如何依賴 msg 的,我畫了一個圖來梳理他們之間的關係:

Vue2.x源碼解析系列五:數據響應之Watcher

解釋一下這個圖,其中藍色的線是引用關係(除了 Observer 和 dep 中間那條線,因為那條線其實是閉包而不是引用),紅色的線是依賴的觸發流程。

  1. 我們通過 this.msg = xxx 來修改 msg 的值,他被 observer 監聽,因此 observer 可以知道這個更新的發生
  2. Observer 中有一個 dep 記錄了依賴,他會調用 dep.notify 來通知那些訂閱者
  3. dep.subs 就保存了訂閱者,會調用他們的 update方法
  4. 調用了 watcher.update 方法,經過幾次調用後最終會在 nextTick 的時候更新 this.value的值

回到 initComputed

再回到我們最開始的 initComputed 函數,前面那麼多內容我們弄懂了 new Watcher 的工作原理,這個函數還有最後一段代碼:

if (!(key in vm)) {
defineComputed(vm, key, userDef);
}

defineComputed 函數的作用是在 this 上做一個 upperMsg 的代理,因此我們可以通過 this.upperMsg 來訪問。 defineComputed 代碼如下:

export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

他會通過 Object.defineProperty 設置 this.upperMsg,依然是通過 getter/setter 來定義的,this.upperMsg 的讀寫會被代理到我們在 options 中定於的 upperMsg 上。

到此我們通過對 datacomputed 的解讀,徹底弄懂了響應式的工作原理。至於 props 因為涉及到VDOM,這裡暫時先不展開了,但是他的響應式部分實現和 data 是一樣的。

相關文章

在react裡渲染vue組件

要實現60FPS動畫,你需要了解這些

體驗WebAssembly

Vue2.x源碼解析系列七:深入Compiler理解render函數的生成過程