咱們在前面推導過程當中實現了一個簡單版的watcher。這裏面還有一些問題vue
class Watcher { constructors(component, getter, cb){ this.cb = cb // 對應的回調函數,callback this.getter = getter; this.component = component; //這就是執行上下文 } //收集依賴 get(){ Dep.target = this; this.getter.call(this.component) if (this.deep) { traverse(value) } Dep.target = null; } update(){ this.cb() } }
所謂的同步更新是指當觀察的主體改變時馬上觸發更新。而實際開發中這種需求並很少,同一事件循環中可能須要改變好幾回state狀態,但視圖view只須要根據最後一次計算結果同步渲染就行(react中的setState就是典型)。若是一直作同步更新無疑是個很大的性能損耗。
這就要求watcher在接收到更新通知時不能全都馬上執行callback。咱們對代碼作出相應調整react
constructors(component, getter, cb, options){ this.cb = cb // 對應的回調函數,callback this.getter = getter; this.id = UUID() // 生成一個惟一id this.sync = options.sync; //默認通常爲false this.vm = component; //這就是執行上下文 this.value = this.getter() // 這邊既收集了依賴,又保存了舊的值 } update(){ if(this.sync){ //若是是同步那就馬上執行回調 this.run(); }else{ // 不然把此次更新緩存起來 //可是就像上面說的,異步更新每每是同一事件循環中屢次修改同一個值, // 那麼一個wather就會被緩存屢次。由於須要一個id來判斷一下, queueWatcher(this) } } run: function(){ //獲取新的值 var newValue = this.getter(); this.cb.call(this.vm, newValue, this.value) }
這裏的一個要注意的地方是,考慮到極限狀況,若是正在更新隊列中wather時,又塞入進來該怎麼處理。所以,加入一個flushing
來表示隊列的更新狀態。
若是加入的時候隊列正在更新狀態,這時候分兩種狀況:緩存
let flushing = false; let has = {}; // 簡單用個對象保存一下wather是否已存在 function queueWatcher (watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 若是以前沒有,那麼就塞進去吧,若是有了就不用管了 if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // ... 等同一事件循環結束後再依次處理隊列裏的watcher。具體代碼放到後面nexttick部分再說 } } }
這麼設計不無道理。咱們之因此爲了將wather放入隊列中,就是爲了較少沒必要要的操做。考慮以下代碼框架
data: { a: 1 }, computed: { b: function(){ this.a + 1 } } methods: { act: function(){ this.a = 2; // do someting this.a = 1 } }
在act操做中,咱們先改變a,再把它變回來。咱們理想情況下是a沒變,b也不從新計算。這就要求,b的wather執行update的時候要拿到a最新的值來計算。這裏就是1。若是隊列中a的watehr已經更新過,那麼就應該把後面的a的wather放到當前更新的wather後面,當即更新。這樣能夠保證後面的wather用到a是能夠拿到最新的值。
同理,若是a的wather尚未更新,那麼把新的a的wather放的以前的a的wather的下一位,也是爲了保證後面的wather用到a是能夠拿到最新的值。dom
之因此把計算屬性拿出愛單獨講,是由於異步
所謂的按需計算顧名思義就是用到了纔會計算,即調用了某個計算屬性的get方法。在前面的方法中,咱們在class Watcher的constructor中直接調用了getter方法收集依賴,這顯然是不符合按需加載的原則的。函數
實際開發中,咱們發現一個計算屬性每每由另外一個計算屬性得來。如,性能
computed: { a: function(){ return this.name; }, b: function(){ return this.a + "123"; } }
對於a而言,它是b的依賴,所以有必要在a的wather執行update操做時也更新b,也就意味着,a的watcher裏須要收集着b的依賴。而收集的時機是執行b的回調時,this.a調用了a的get方法的時候
在computed部分,已經對計算屬性的get方法進行了改寫學習
function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { //調用一個計算屬性的get方法時,會在watcher中收集依賴。 watcher.depend() return watcher.evaluate() } }
咱們再修改一下wather代碼:this
class Watcher { constructors(component, getter, cb, options){ this.cb = cb this.getter = getter; this.id = UUID() this.sync = options.sync; this.vm = component; if(options){ this.computed = options.computed //因爲是對計算屬性特殊處理,那確定要給個標識符以便判斷 } this.dirty = this.computed // for computed watchers this.value = this.lazy ? undefined : this.get(); } update(){ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run: function(){ //拿到新值 const value = this.get() if (value !== this.value || //基本類型的值直接比較 // 對象沒辦法直接比較,所以都進行計算 isObject(value)) { // set new value const oldValue = this.value this.value = value this.dirty = false cb.call(this.vm, value, oldValue) } } // 新增depend方法,收集計算屬性的依賴 depend () { if (this.dep && Dep.target) { this.dep.depend() } } } //不要忘了還要返回當前computed的最新的值 //因爲可能不是當即更新的,所以根據dirty再判斷一下,若是數據髒了,調用get再獲取一下 evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
在綁定依賴以前(computed的get被觸發一次),computed用到的data數據改變是不會觸發computed的從新計算的。
對於render和computed想要收集依賴,咱們只須要執行一遍回調函數就行,可是對於$watch方法,咱們並不關心他的回調是什麼,而更關心咱們須要監聽哪一個值。
這裏的需求多種多樣,
好比單個值監聽,監聽對象的某個屬性(.),好比多個值混合監聽(&&, ||)等。這就須要對監聽的路徑進行解析。
constructors(component, expOrFn, cb, options){ this.cb = cb this.id = UUID() this.sync = options.sync; this.vm = component; if(options){ this.computed = options.computed } if(typeof expOrFn === "function"){ // render or computed this.getter = expOrFn }else{ this.getter = this.parsePath(); } if(this.computed){ this.value = undefined this.dep = new Dep() }else{ this.value = this.get(); //非計算屬性是經過調用getter方法收集依賴。 } } parsePath: function(){ // 簡單的路徑解析,若是都是字符串則不須要解析 if (/[^\w.$]/.test(path)) { return } // 這邊只是簡單解析了子屬性的狀況 const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
咱們在watcher乞丐版的基礎上,根據實際需求推導出了更健全的watcher版本。下面是完整代碼
class Watcher { constructors(component, getter, cb, options){ this.cb = cb this.getter = getter; this.id = UUID() this.sync = options.sync; this.vm = component; if(options){ this.computed = options.computed //因爲是對計算屬性特殊處理,那確定要給個標識符以便判斷 } if(typeof expOrFn === "function"){ // render or computed this.getter = expOrFn }else{ this.getter = this.parsePath(); } this.dirty = this.computed // for computed watchers if(this.computed){ // 對於計算屬性computed而言,咱們須要關心preValue嗎? ********************* this.value = undefined // 若是是計算屬性,就要收集依賴 //同時根據按需加載的原則,這邊不會手機依賴,主動執行回調函數。 this.dep = new Dep() }else{ this.value = this.get(); //非計算屬性是經過調用getter方法收集依賴。 } } update(){ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run: function(){ //拿到新值 const value = this.get() if (value !== this.value || //基本類型的值直接比較 // 對象沒辦法直接比較,所以都進行計算 isObject(value)) { // set new value const oldValue = this.value this.value = value this.dirty = false cb.call(this.vm, value, oldValue) } } // 新增depend方法,收集計算屬性的依賴 depend () { if (this.dep && Dep.target) { this.dep.depend() } } } //不要忘了還要返回當前computed的最新的值 //因爲可能不是當即更新的,所以根據dirty再判斷一下,若是數據髒了,調用get再獲取一下 evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
能夠看到,基本vue的實現同樣了。VUE中有些代碼,好比teardown
方法,清除自身的訂閱信息我並無加進來,由於沒有想到合適的應用場景。
這種逆推的過程我以爲比直接讀源碼更有意思。直接讀源碼並不難,但很容易形成似是而非的狀況。邏輯很容易理解,可是真正爲何這麼寫,一些細節緣由很容易漏掉。可是無論什麼框架都是爲了解決實際問題的,從需求出發,才能更好的學習一個框架,並在本身的工做中加以借鑑。
借VUE的生命週期圖進行展現
局部圖:
從局部圖裏能夠看出,vue收集依賴的入口只有兩個,一個是在加載以前處理$wacth方法,一個是render生成虛擬dom。而對於computed,只有在使用到時纔會收集依賴。若是咱們在watch和render中都沒有使用,而是在methods中使用,那麼加載的過程當中是不會計算這個computed的,只有在調用methods中方法時纔會計算。