爲何咱們改變了數據,
Vue
可以自動幫咱們刷新DOM
。就是由於有Watcher
。固然,Watcher
只是派發數據更新,真正的修改DOM
,還須要借用VNode
,咱們這裏先不討論VNode
。javascript
computed
計算屬性,內部實現也是基於Watcher
html
watcher
選項的使用方法,我目前經過看文檔和源碼理解到的,有五種,以下:vue
new Vue ({
data: {
a: { x: 1 }
b: { y: 1 }
},
watch: {
a() {
// do something
},
'a.x'() {
// do something
},
a: {
hander: 'methodName',
deep: Boolean
immediate: Boolean
},
a: 'methodName',
a: ['methodName', 'methodName']
}
});
複製代碼
代碼來源:Vue項目下 src/core/instance/lifecycle.js
java
updateComponent = () => {
// vm._render 會根據咱們的html模板和vm上的數據生成一個 新的 VNode
// vm._update 會將新的 VNode 與 舊的 Vnode 進行對比,執行 __patch__ 方法打補丁,並更新真實 dom
// 初始化時,確定沒有舊的 Vnode 咯,這個時候就會全量更新 dom
vm._update(vm._render(), hydrating)
}
// 當 new Watcher 時,會執行 updateComponent ,
// 執行 updateComponent 函數會訪問 data 中的數據,至關於觸發 data 中數據的 get 屬性
// 觸發 data 中數據的 get 屬性,就至關於觸發了 依賴收集
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製代碼
衆所周知,Vue
是在觸發數據的 get
時,收集依賴,改變數據時觸發set
, 達到派發更新的目的。node
依賴收集 和 派發更新的 代碼 在上一篇文章,有簡單解釋過。咱們再來重溫下代碼react
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
// 每一個數據都有一個屬於本身的 dep
const dep = new Dep()
// 省略部分代碼...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 省略部分代碼...
if (Dep.target) {
// 收集依賴
dep.depend()
// 省略部分代碼...
}
// 省略部分代碼...
},
set: function reactiveSetter (newVal) {
// 省略部分代碼...
// 派發更新
dep.notify()
}
})
}
複製代碼
這裏我省略了部分用於判斷和兼容的代碼,由於感受一會兒要看全部代碼的話,會有些懵比。咱們如今知道了 dep.depend
用於收集依賴,dep.notify
用於派發更新,咱們按着這兩條主線,去一步步摸索。express
dep
是在代碼開始的地方定義的:const dep = new Dep()
。數組
因此咱們要先找到 Dep
這個構造函數,而後咱們還要了解 Dep.target
是個啥東西性能優化
Dep 構造函數定義在 Vue 項目下:/src/core/observer/dep.js
dom
咱們能夠發現 Dep
的實現就是一個觀察者模式,很像一個迷你的事件系統。
Dep
中的 addSub, removeSub
,和 咱們定義一個 Events
時裏面的 on, off
是很是類似的。
// 用於看成 Dep 的標識
let uid = 0
/** * A dep is an observable that can have multiple * directives subscribing to it. */
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
// 定義一個 subs 數組,這個數組是用來存放 watcher 實例的
constructor () {
this.id = uid++
this.subs = []
}
// 將 watcher 實例添加到 subs 中
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 從 subs 中移除對應的 watcher 實例。
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 依賴收集,這就是咱們以前看到的 dep.dpend 方法
depend () {
// Dep.target 是 watcher 實例
if (Dep.target) {
// 看到這裏應該能明白 watcher 實例上 有一個 addDep 方法,參數是當前 dep 實例
Dep.target.addDep(this)
}
}
// 派發更新,這就是咱們以前看到的 dep.notify 方法
notify () {
// 複製一份,多是由於下面要作排序,但是又不能影響 this.subs 數組內元素的順序
// 因此就複製一份出來。
const subs = this.subs.slice()
// 這裏作了個排序操做,具體緣由是什麼,我還不清楚
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 遍歷 subs 數組,依次觸發 watcher 實例的 update
for (let i = 0, l = subs.length; i < l; i++) {
// 看到這裏應該能明白 watcher 實例上 有一個 update 方法
subs[i].update()
}
}
}
// 在 Dep 上掛一個靜態屬性,
// 這個 Dep.target 的值會在調用 pushTarget 和 popTarget 時被賦值,值爲當前 watcher 實例對象。
Dep.target = null
// 維護一個棧結構,用於存儲和刪除 Dep.target
const targetStack = []
// pushTarget 會在 new Watcher 時被調用
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
// popTarget 會在 new Watcher 時被調用
export function popTarget () {
Dep.target = targetStack.pop()
}
複製代碼
Dep
是一個類,用於依賴收集和派發更新,也就是存放watcher實例
和觸發watcher實例
上的update
。
Watcher
也是一個類,用於初始化 數據的watcher實例
。它的原型上有一個update
方法,用於派發更新。
一句話歸納:Dep
是watcher實例
的管理者。相似觀察者模式的實現。
Watcher 的代碼比較多,我這裏省略部分代碼,並在主要代碼上加上註釋,方便你們理解。
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function, // 要 watch 的屬性名稱
cb: Function, // 回調函數
options?: ?Object,
isRenderWatcher?: boolean // 是不是渲染函數觀察者,Vue 初始化時,這個參數被設爲 true
) {
// 省略部分代碼... 這裏代碼的做用是初始化一些變量
// expOrFn 能夠是 字符串 或者 函數
// 何時會是字符串,例如咱們正常使用的時候,watch: { x: fn }, Vue內部會將 `x` 這個key 轉化爲字符串
// 何時會是函數,其實 Vue 初始化時,就是傳入的渲染函數 new Watcher(vm, updateComponent, ...);
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 在文章開頭,我描述了 watch 的幾種用法,
// 當 expOrFn 不爲函數時,多是這種描述方式:watch: {'a.x'(){ //do } },具體到了某個對象的屬性
// 這個時候,就須要經過 parsePath 方法,parsePath 方法返回一個函數
// 函數內部會去獲取 'a.x' 這個屬性的值了
this.getter = parsePath(expOrFn)
// 省略部分代碼...
}
// 這裏調用了 this.get,也就意味着 new Watcher 時會調用 this.get
// this.lazy 是修飾符,除非用戶本身傳入,否則都是 false。能夠先無論它
this.value = this.lazy
? undefined
: this.get()
}
get () {
// 將 當前 watcher 實例,賦值給 Dep.target 靜態屬性
// 也就是說 執行了這行代碼,Dep.target 的值就是 當前 watcher 實例
// 並將 Dep.target 入棧 ,存入 targetStack 數組中
pushTarget(this)
// 省略部分代碼...
try {
// 這裏執行了 this.getter,獲取到 屬性的初始值
// 若是是初始化時 傳入的 updateComponent 函數,這個時候會返回 udnefined
value = this.getter.call(vm, vm)
} catch (e) {
// 省略部分代碼...
} finally {
// 省略部分代碼...
// 出棧
popTarget()
// 省略部分代碼...
}
// 返回屬性的值
return value
}
// 這裏再回顧一下
// dep.depend 方法,會執行 Dep.target.addDep(dep) 其實也就是 watcher.addDep(dep)
// watcher.addDep(dep) 會執行 dep.addSub(watcher)
// 將當前 watcher 實例 添加到 dep 的 subs 數組 中,也就是收集依賴
// dep.depend 和 這個 addDep 方法,有好幾個 this, 可能有點繞。
addDep (dep: Dep) {
const id = dep.id
// 下面兩個 if 條件都是去重的做用,咱們能夠暫時不考慮它們
// 只須要知道,這個方法 執行 了 dep.addSub(this)
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 將當前 watcher 實例添加到 dep 的 subs 數組中
dep.addSub(this)
}
}
}
// 派發更新
update () {
// 若是用戶定義了 lazy ,this.lazy 是描述符,咱們這裏能夠先無論它
if (this.lazy) {
this.dirty = true
// this.sync 表示是否改變了值以後當即觸發回調。若是用戶定義爲true,則當即執行 this.run
} else if (this.sync) {
this.run()
// queueWatcher 內部也是執行的 watcher實例的 run 方法,只不過內部調用了 nextTick 作性能優化。
// 它會將當前 watcher 實例放入一個隊列,在下一次事件循環時,遍歷隊列並執行每一個 watcher實例的run() 方法
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
// 獲取新的屬性值
const value = this.get()
if (
// 若是新值不等於舊值
value !== this.value ||
// 若是新值是一個 引用 類型,那麼必定要觸發回調
// 舉個例子,若是舊值原本就是一個對象,
// 在新值內,咱們只改變對象內的某個屬性值,那新值和舊值自己仍是相等的
// 也就是說,若是 this.get 返回的是一個引用類型,那麼必定要觸發回調
isObject(value) ||
// 是否深度 watch
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
// this.user 是一個標誌符,若是開發者添加的 watch 選項,這個值默認爲 true
// 若是是用戶本身添加的 watch ,就加一個 try catch。方便用戶調試。不然直接執行回調。
if (this.user) {
try {
// 觸發回調,並將 新值和舊值 做爲參數
// 這也就是爲何,咱們寫 watch 時,能夠這樣寫: function (newVal, oldVal) { // do }
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// 省略部分代碼...
// 如下是 Watcher 類的其餘方法
cleanUpDeps() { }
evaluate() { }
depend() { }
teardown() { }
}
複製代碼
Watcher 的代碼較多,我就不將所有方法都解釋一遍了。有興趣的朋友能夠本身去看下源碼,瞭解下。
這裏再順帶說下 parsePath
函數,其實這個函數的做用就是解析 watch
的 key
值是字符串,且爲 obj.x.x
這種狀況。
代碼來源:Vue項目下vue/src/core/util/lang.js
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
// 若是 path 參數,不包含 字母 或 數字 或 下劃線,或者不包含 `.`、`$` ,直接返回
// 也就是說 obj-a, obj/a, obj*a 等值,會直接返回
if (bailRE.test(path)) {
return
}
// 假如傳入的值是 'a.b.c',那麼此時 segments 就是 ['a', 'b', 'c']
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
// 由於這個函數調用時,是 call(vm, vm) 的形式,因此第一個 obj 是 vm
// 注意這裏的 vm 是形參
// 執行順序以下
// obj = vm['a'] -> 拿到 a 對象 , 當前 obj 的值 爲 vm.a
// obj = a['b'] -> 拿到 b 對象, 當前 obj 的值 爲 a.b
// obj = b[c] -> 拿到 c 對象, 當前 obj 的值 是 a.b.c
// 循環結束
obj = obj[segments[i]]
}
return obj
}
}
複製代碼
代碼來源:Vue項目下 src/core/instance/state.js
// line - 286
// initWatch 會在 new Vue 初始化 的時候被調用
function initWatch (vm: Component, watch: Object) {
// 這裏的 watch 參數, 就是咱們 定義的 watch 選項
// 咱們定義的 watch選項 是一個 Object,因此要用 for...in 循環遍歷它。
for (const key in watch) {
// key 就是咱們要 watch 的值的名稱
const handler = watch[key]
// 若是 是這種調用方式 key: [xxx, xxx]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
複製代碼
// line - 299
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) {
// 若是 handler 是一個對象, 如:key: { handler: 'methodName', deep: true } 這種方式調用
// 將 handler.handler 賦值給 handler,也就是說 handler 的值會被覆蓋 爲 'methodName'
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 若是handler 是一個字符串,則 從 vm 對象上去獲取函數,賦值給 handler
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
複製代碼
// line - 341
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
const vm: Component = this
// 若是回調是對象的話,調用 createWatcher 將參數規範化, createWatcher 內部再調用 vm.$watch 進行處理。
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 設置 user 默認值 爲 true,剛纔咱們分析的 Watcher 類,它的 run 方法裏面就有關於 user 的判斷
options.user = true
// 初始化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 若是 immediate 爲true, 當即觸發一次回調
if (options.immediate ) {
cb.call(vm, watcher.value)
}
// 返回一個函數,能夠用來取消 watch
return function unwatchFn () {
watcher.teardown()
}
}
複製代碼
還在畫圖中...
謝謝閱讀。若是文章有錯誤的地方,煩請指出。