摘要:這里面還有一些問題對應的回調函數(shù)這就是執(zhí)行上下文收集依賴同步異步更新所謂的同步更新是指當觀察的主體改變時立刻觸發(fā)更新。
我們在前面推導過程中實現(xiàn)了一個簡單版的watcher。這里面還有一些問題
class Watcher { constructors(component, getter, cb){ this.cb = cb // 對應的回調函數(shù),callback this.getter = getter; this.component = component; //這就是執(zhí)行上下文 } //收集依賴 get(){ Dep.target = this; this.getter.call(this.component) if (this.deep) { traverse(value) } Dep.target = null; } update(){ this.cb() } }同步異步更新
所謂的同步更新是指當觀察的主體改變時立刻觸發(fā)更新。而實際開發(fā)中這種需求并不多,同一事件循環(huán)中可能需要改變好幾次state狀態(tài),但視圖view只需要根據(jù)最后一次計算結果同步渲染就行(react中的setState就是典型)。如果一直做同步更新無疑是個很大的性能損耗。
這就要求watcher在接收到更新通知時不能全都立刻執(zhí)行callback。我們對代碼做出相應調整
constructors(component, getter, cb, options){ this.cb = cb // 對應的回調函數(shù),callback this.getter = getter; this.id = UUID() // 生成一個唯一id this.sync = options.sync; //默認一般為false this.vm = component; //這就是執(zhí)行上下文 this.value = this.getter() // 這邊既收集了依賴,又保存了舊的值 } update(){ if(this.sync){ //如果是同步那就立刻執(zhí)行回調 this.run(); }else{ // 否則把這次更新緩存起來 //但是就像上面說的,異步更新往往是同一事件循環(huán)中多次修改同一個值, // 那么一個wather就會被緩存多次。因為需要一個id來判斷一下, queueWatcher(this) } } run: function(){ //獲取新的值 var newValue = this.getter(); this.cb.call(this.vm, newValue, this.value) }
這里的一個要注意的地方是,考慮到極限情況,如果正在更新隊列中wather時,又塞入進來該怎么處理。因此,加入一個flushing來表示隊列的更新狀態(tài)。
如果加入的時候隊列正在更新狀態(tài),這時候分兩種情況:
這個watcher已經(jīng)更新過, 就把這個watcher再放到當前執(zhí)行的下一位,當前watcher處理完,立即處理這個最新的。
這個watcher還沒有處理,就找到這個wather在隊列中現(xiàn)有的位置,并再把新的放在后面。
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) } // ... 等同一事件循環(huán)結束后再依次處理隊列里的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執(zhí)行update的時候要拿到a最新的值來計算。這里就是1。如果隊列中a的watehr已經(jīng)更新過,那么就應該把后面的a的wather放到當前更新的wather后面,立即更新。這樣可以保證后面的wather用到a是可以拿到最新的值。
同理,如果a的wather還沒有更新,那么把新的a的wather放的之前的a的wather的下一位,也是為了保證后面的wather用到a是可以拿到最新的值。
之所以把計算屬性拿出愛多帶帶講,是因為
計算屬性存在按需加載的情況
與render和$watcher相比,計算屬性a可能依賴另一個計算屬性b。
按需加載所謂的按需計算顧名思義就是用到了才會計算,即調用了某個計算屬性的get方法。在前面的方法中,我們在class Watcher的constructor中直接調用了getter方法收集依賴,這顯然是不符合按需加載的原則的。
依賴收集實際開發(fā)中,我們發(fā)現(xiàn)一個計算屬性往往由另一個計算屬性得來。如,
computed: { a: function(){ return this.name; }, b: function(){ return this.a + "123"; } }
對于a而言,它是b的依賴,因此有必要在a的wather執(zhí)行update操作時也更新b,也就意味著,a的watcher里需要收集著b的依賴。而收集的時機是執(zhí)行b的回調時,this.a調用了a的get方法的時候
在computed部分,已經(jīng)對計算屬性的get方法進行了改寫
function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { //調用一個計算屬性的get方法時,會在watcher中收集依賴。 watcher.depend() return watcher.evaluate() } }
我們再修改一下wather代碼:
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的最新的值 //由于可能不是立即更新的,因此根據(jù)dirty再判斷一下,如果數(shù)據(jù)臟了,調用get再獲取一下 evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
在綁定依賴之前(computed的get被觸發(fā)一次),computed用到的data數(shù)據(jù)改變是不會觸發(fā)computed的重新計算的。
路徑解析對于render和computed想要收集依賴,我們只需要執(zhí)行一遍回調函數(shù)就行,但是對于$watch方法,我們并不關心他的回調是什么,而更關心我們需要監(jiān)聽哪個值。
這里的需求多種多樣,
比如單個值監(jiān)聽,監(jiān)聽對象的某個屬性(.),比如多個值混合監(jiān)聽(&&, ||)等。這就需要對監(jiān)聽的路徑進行解析。
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乞丐版的基礎上,根據(jù)實際需求推導出了更健全的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 // 如果是計算屬性,就要收集依賴 //同時根據(jù)按需加載的原則,這邊不會手機依賴,主動執(zhí)行回調函數(shù)。 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的最新的值 //由于可能不是立即更新的,因此根據(jù)dirty再判斷一下,如果數(shù)據(jù)臟了,調用get再獲取一下 evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
可以看到,基本vue的實現(xiàn)一樣了。VUE中有些代碼,比如teardown方法,清除自身的訂閱信息我并沒有加進來,因為沒有想到合適的應用場景。
這種逆推的過程我覺得比直接讀源碼更有意思。直接讀源碼并不難,但很容易造成似是而非的情況。邏輯很容易理解,但是真正為什么這么寫,一些細節(jié)原因很容易漏掉。但是不管什么框架都是為了解決實際問題的,從需求出發(fā),才能更好的學習一個框架,并在自己的工作中加以借鑒。
借VUE的生命周期圖進行展示
局部圖:
從局部圖里可以看出,vue收集依賴的入口只有兩個,一個是在加載之前處理$wacth方法,一個是render生成虛擬dom。
而對于computed,只有在使用到時才會收集依賴。如果我們在watch和render中都沒有使用,而是在methods中使用,那么加載的過程中是不會計算這個computed的,只有在調用methods中方法時才會計算。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97953.html
寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧 【Vue原理】NextTick - 源碼版 之 服務Vue 初次看的兄弟可以先看 【Vue原理】NextTick - 白話版 簡單了解下...
摘要:執(zhí)行當時傳入的回調,并將新值與舊值一并傳入。文章鏈接源碼分析系列源碼分析系列之環(huán)境搭建源碼分析系列之入口文件分析源碼分析系列之響應式數(shù)據(jù)一源碼分析系列之響應式數(shù)據(jù)二源碼分析系列之響應式數(shù)據(jù)三 前言 上一節(jié)著重講述了initComputed中的代碼,以及數(shù)據(jù)是如何從computed中到視圖層的,以及data修改后如何作用于computed。這一節(jié)主要記錄initWatcher中的內容。 ...
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理依賴更新源碼版如果對依賴收集完 寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于...
摘要:當東西發(fā)售時,就會打你的電話通知你,讓你來領取完成更新。其中涉及的幾個步驟,按上面的例子來轉化一下你買東西,就是你要使用數(shù)據(jù)你把電話給老板,電話就是你的,用于通知老板記下電話在電話本,就是把保存在中。剩下的步驟屬于依賴更新 寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【...
摘要:中的觀察者模式觀察者模式一般包含發(fā)布者和訂閱者兩種角色顧名思義發(fā)布者負責發(fā)布消息,訂閱者通過訂閱消息響應動作了。中主要有兩種類型的,一種是另外一種是是通過或者中的屬性定義的。結束好了,基本結束,如有錯漏,望指正。 碎碎念 四月份真是慵懶無比的一個月份,看著手頭上沒啥事干,只好翻翻代碼啥的,看了一會Vue的源碼,忽而有點感悟,于是便記錄一下。 Vue中的觀察者模式 觀察者模式一般包含發(fā)布...
閱讀 3514·2021-11-17 17:01
閱讀 3919·2021-11-08 13:12
閱讀 2477·2021-10-08 10:04
閱讀 687·2021-09-29 09:35
閱讀 1418·2021-09-26 10:12
閱讀 2021·2021-09-07 09:58
閱讀 1953·2019-08-30 15:55
閱讀 2134·2019-08-30 13:14