一直以來我對vue中的watch
和computed
都只知其一;不知其二的,知道一點(例如:watch
和computed
的本質都是new Watcher
,computed
有緩存,只有調用的時候纔會執行,也只有當依賴的數據變化了,纔會再次觸發...),而後就沒有而後了。javascript
也看了不少大佬寫的文章,一大段一大段的源碼列出來,着實讓我這個菜雞看的頭大,天然也就不想看了。最近,我又開始學習vue源碼,才真正理解了它們的實現原理。vue
data() { return { msg: 'hello guys', info: {age:'18'}, name: 'FinGet' } }
watcher
是什麼?偵聽器?它就是個類class
!java
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ } }
vm
vue實例exprOrFn
多是字符串或者回調函數(有點懵就日後看,如今它不重要)options
各類配置項(配置啥,日後看)isRenderWatcher
是不是渲染Wathcer
Vue 初始化中 會執行一個 initState
方法,其中有你們最熟悉的initData
,就是Object.defineProperty
數據劫持。緩存
export function initState(vm) { const opts = vm.$options; // vue 的數據來源 屬性 方法 數據 計算屬性 watch if(opts.props) { initProps(vm); } if(opts.methods) { initMethod(vm); } if(opts.data) { initData(vm); } if(opts.computed){ initComputed(vm); } if(opts.watch) { initWatch(vm, opts.watch); } }
在數據劫持中,Watcher
的好基友Dep
出現了,Dep
就是爲了把Watcher
存起來。函數
function defineReactive(data, key, val) { let dep = new Dep(); Object.defineProperty(data, key, { get(){ if(Dep.target) { dep.depend(); // 收集依賴 } return val; }, set(newVal) { if(newVal === val) return; val = newVal; dep.notify(); // 通知執行 } }) }
當initData
的時候,Dep.target
啥也不是,因此收集了個寂寞。target
是綁在Dep這個類上的(靜態屬性),不是實例上的。
可是當$mount
以後,就不同了。至於$mount
中執行的什麼compile
、generate
、render
、patch
、diff
都不是本文關注的,不重要,繞過!學習
你只須要知道一件事:會執行下面的代碼this
new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一個渲染watcher
updateComponent
就是更新哈,不計較具體執行,它如今就是個會更新頁面的回調函數,它會被存在Watcher
的getter
中。它對應的就是最開始那個exprOrFn
參數。lua
嘿嘿嘿,這個時候就不同了:spa
get
。new Watcher
就會調用一個方法把這個實例放到Dep.target
上。pushTarget(watcher) { Dep.target = watcher; }
這兩件事正好湊到一塊兒,那麼 dep.depend()
就幹活了。prototype
因此到這裏能夠明白一件事,全部的data
中定義的數據,只要被調用,它都會收集一個渲染watcher
,也就是數據改變,執行set
中的dep.notify
就會執行渲染watcher
下圖就是定義了msg
、info
、name
三個數據,它們都有個渲染Watcher
:
眼尖的小夥伴應該看到了msg
中還有兩個watcher
,一個是用戶定義的watch
,另外一個也是用戶定義的watch
。啊,固然不是啦,vue
是作了去重的,不會有重複的watcher
,正如你所料,另外一個是computed watcher
;
咱們通常是這樣使用watch的:
watch: { msg(newVal, oldVal){ console.log('my watch',newVal, oldVal) } // or msg: { handler(newVal, oldVal) { console.log('my watch',newVal, oldVal) }, immediate: true } }
這裏會執行一個initWatch
,一頓操做以後,就是提取出exprOrFn
(這個時候它就是個字符串了)、handler
、options
,這就和Watcher
莫名的契合了,而後就瓜熟蒂落的調用了vm.$watch
方法。
Vue.prototype.$watch = function(exprOrFn, cb, options = {}) { options.user = true; // 標記爲用戶watcher // 核心就是建立個watcher const watcher = new Watcher(this, exprOrFn, cb, options); if(options.immediate){ cb.call(vm,watcher.value) } }
來吧,避免不了看看這段代碼(原本粘貼了好長一段,但說了大白話,我就把和這段關係不大的給刪減了):
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ this.vm = vm; this.callback = callback; this.options = options; if(options) { this.user = !!options.user; } this.id = id ++; if (typeof exprOrFn == 'function') { this.getter = exprOrFn; // 將內部傳過來的回調函數 放到getter屬性上 } else { this.getter = parsePath(exprOrFn); if (!this.getter) { this.getter = (() => {}); } } this.value = this.get(); } get(){ pushTarget(this); // 把當前watcher 存入dep中 let result = this.getter.call(this.vm, this.vm); // 渲染watcher的執行 這裏會走到observe的get方法,而後存下這個watcher popTarget(); // 再置空 當執行到這一步的時候 因此的依賴收集都完成了,都是同一個watcher return result; } }
// 這個就是拿來把msg的值取到,取到的就是oldVal function parsePath(path) { if (!path) { return } var segments = path.split('.'); return function(obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj } }
你們能夠看到,new Watcher
會執行一下get
方法,當是渲染Watcher就會渲染頁面,執行一次updateComponent
,當它是用戶Watcher就是執行parsePath
中的返回的方法,而後獲得一個值this.value
也就是oldVal
。
嘿嘿嘿,既然取值了,那又走到了msg
的get
裏面,這個時候dep.depend()
又幹活了,用戶Watcher就存進去了。
當msg
改變的時候,這過程當中還有一些騷操做,不重要哈,最後會執行一個run
方法,調用回調函數,把newValue
和oldValue
傳進去:
run(){ let oldValue = this.value; // 再執行一次就拿到了如今的值,會去重哈,watcher不會重複添加 let newValue = this.get(); this.value = newValue; if(this.user && oldValue != newValue) { // 是用戶watcher, 就調用callback 也就是 handler this.callback(newValue, oldValue) } }
computed: { c_msg() { return this.msg + 'computed' } // or c_msg: { get() { return this.msg + 'computed' }, set() {} } },
computed
有什麼特色:
調用的時候執行,我怎麼知道它在調用?嘿嘿嘿,Object.defineProperty
不就是幹這事的嘛,巧了不是。
依賴的數據改變時會從新計算,那就須要收集依賴了。仍是那個邏輯,調用了this.msg
-> get
-> dep.depend()
。
function initComputed(vm) { let computed = vm.$options.computed; const watchers = vm._computedWatchers = {}; for(let key in computed) { const userDef = computed[key]; // 獲取get方法 const getter = typeof userDef === 'function' ? userDef : userDef.get; // 建立計算屬性watcher lazy就是第一次不調用 watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true }); defineComputed(vm, key, userDef) } }
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: () => {}, set: () => {} } function defineComputed(target, key, userDef) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key) } else { sharedPropertyDefinition.get = createComputedGetter(userDef.get); sharedPropertyDefinition.set = userDef.set; } // 使用defineProperty定義 這樣才能作到使用才計算 Object.defineProperty(target, key, sharedPropertyDefinition) }
下面這一段最重要,上面的看一眼就好,上面作的就是把get
方法找出來,用Object.defineProperty
綁定一下。
class Watcher{ constructor(vm,exprOrFn,callback,options,isRenderWatcher){ ... this.dirty = this.lazy; // lazy 第一次不執行 this.value = this.lazy ? undefined : this.get(); ... } update(){ if (this.lazy) { // 計算屬性 須要更新 this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); // 這就是個襯托 如今無論它 } } evaluate() { this.value = this.get(); this.dirty = false; } }
緩存就在這裏,執行get
方法會拿到一個返回值this.value
就是緩存的值,在用戶Watcher中,它就是oldValue
,寫到這裏的時候,對尤大神的佩服,又加深一層。🐂🍺plus!
function createComputedGetter(key) { return function computedGetter() { // this 指向vue 實例 const watcher = this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { // 若是dirty爲true watcher.evaluate();// 計算出新值,並將dirty 更新爲false } // 若是依賴的值不發生變化,則返回上次計算的結果 return watcher.value } } }
watcher
的update
是何時調用的?也就是數據更新調用dep.notify()
,dirty
就須要變成true
,可是計算屬性仍是不能立刻計算,仍是須要在調用的時候才計算,因此在update
的時候只是改了dirty
的狀態!而後下次調用的時候就會從新計算。
class Dep { constructor() { this.id = id ++; this.subs = []; } addSub(watcher) { this.subs.push(watcher); } depend() { Dep.target.addDep(this); } notify() { this.subs.forEach(watcher => watcher.update()) } }
watch
和 computed
本質都是Watcher
,都被存放在Dep
中,當數據改變時,就執行dep.notify
把當前對應Dep
實例中存的Watcher
都run
一下,這樣執行了渲染Watcher
頁面就刷新了;Dep
,若是他在模版中被調用,那它必定有一個渲染Watcher
;initData
時,是沒有 Watcher
能夠收集的;Watcher
和 Computed
中,exprOrFn
都是函數,用戶Watcher
中都是字符串。
文章中的代碼是簡略版的,還有不少細枝末節的東西沒說,不重要也只是針對本文不重要,你們能夠去閱讀源碼更深刻的理解。