摘要:并在內執行了函數,在函數內部,訪問了。至此知道了它依賴于。需要根據最新的計算。本例中收集到了依賴并且也被告知觀察了他們。文章鏈接源碼分析系列源碼分析系列之環境搭建源碼分析系列之入口文件分析源碼分析系列之響應式數據一源碼分析系列之響應式數據二
前言
上一節著重講述了initData中的代碼,以及數據是如何從data中到視圖層的,以及data修改后如何作用于視圖。這一節主要記錄initComputed中的內容。
正文 前情回顧在demo示例中,我們定義了一個計算屬性。
computed:{ total(){ return this.a + this.b } }
本章節我們繼續探究這個計算屬性的相關流程。
initComputed// initComputed(vm, opts.computed) function initComputed (vm: Component, computed: Object) { // 定義計算屬性相關的watchers. const watchers = vm._computedWatchers = Object.create(null) // 是否是服務端渲染,這里贊不考慮。 const isSSR = isServerRendering() for (const key in computed) { // 獲得用戶定義的計算屬性中的item,通常是一個方法 // 在示例程序中,僅有一個key為total的計算a+b的方法。 const userDef = computed[key] const 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. // 為計算屬性創建一個內部的watcher。 // 其中computedWatcherOptions的值為lazy,意味著這個wacther內部的value,先不用計算。 // 只有在需要的情況下才計算,這里主要是在后期頁面渲染中,生成虛擬dom的時候才會計算。 // 這時候new Watcher只是走一遍watcher的構造函數,其內部value由于 // lazy為true,先設置為了undefined.同時內部的dirty = lazy; watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions // 上文定義過,值為{lazy: true} ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 組件定義的屬性只是定義在了組件上,這里只是把它翻譯到實例中。即當前的vm對象。 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) } } } }defineComputed
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } // defineComputed(vm, key, userDef) export function defineComputed ( target: any, key: string, userDef: Object | Function ) { // 是否需要緩存。即非服務端渲染需要緩存。 // 由于本案例用的demo非服務端渲染,這里結果是true const shouldCache = !isServerRendering() if (typeof userDef === "function") { // userDef = total() {...} sharedPropertyDefinition.get = shouldCache // 根據key創建計算屬性的getter ? createComputedGetter(key) : userDef // 計算屬性是只讀的,所以設置setter為noop. 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) }createComputedGetter
// 根據key創建計算屬性的getter // createComputedGetter(key) function createComputedGetter (key) { return function computedGetter () { // 非服務端渲染的時候,在上述的initComputed中定義了vm._computedWatchers = {},并根據組件中的設定watchers[key] = new Watcher(..),這里只是根據key取出了當時new的watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示這個值是臟值,過期了。所以需要重新計算。 // new Watcher的時候,這個total的watcher中,內部的dirty已經被置為 // dirty = lazy = true; // 那么這個值什么時候會過期,會臟呢。就是內部的依賴更新時候, // 比如我們的total依賴于this.a,this.b,當著兩個值任意一個變化時候 // 我們的total就已經臟了。需要根據最新的a,b計算。 if (watcher.dirty) { // 計算watcher中的值,即value屬性. watcher.evaluate() } // 將依賴添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的結果就是返回getter中的值。 return watcher.value } } }initComputed小結
繼initComputed之后,所有組件中的computed都被賦值到了vm實例的屬性上,并設置好了getter和setter。在非服務端渲染的情況下,getter會緩存計算結果。并在需要的時候,才計算。setter則是一個什么都不做的函數,預示著計算屬性只能被get,不能被set。即只讀的。
接下來的問題就是:
這個計算屬性什么時候會計算,前文{lazy:true}預示著當時new Watcher得到的值是undefined。還沒開始計算。
計算屬性是怎么知道它本身依賴于哪些屬性的。以便知道其什么時候更新。
vue官方文檔的緩存計算結果怎么理解。
接下來我們繼續剖析后面的代碼。解決這里提到的三個問題。
用來生成vnode的render函數下次再見到這個計算屬性total的時候,已是在根據el選項或者template模板中,生成的render函數,render函數上一小節也提到過。長這個樣子。
(function anonymous() { with (this) { return _c("div", { attrs: { "id": "demo" } }, [_c("div", [_c("p", [_v("a:" + _s(a))]), _v(" "), _c("p", [_v("b: " + _s(b))]), _v(" "), _c("p", [_v("a+b: " + _s(total))]), _v(" "), _c("button", { on: { "click": addA } }, [_v("a+1")])])]) } } )
這里可以結合一下我們的html,看出一些特點。
a:{{a}}
b: {{b}}
a+b: {{total}}
這里使用到計算屬性的主要是這一句
_v("a+b: " + _s(total))
那么對于我們來說的關鍵就是_s(total)。由于這個函數的with(this)中,this被設置為vm實例,所以這里就可以理解為_s(vm.total)。那么這里就會觸發之前定義的sharedPropertyDefinition.get
-> initComputed() -> defineComputed() -> Object.defineProperty(target, key, sharedPropertyDefinition)
也就是createComputedGetter返回的函數中的內容,也就是:
watcher細說const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 由于初始化的時候這個dirty為true,所以會進行watcher.evaluate()的計算。 if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } // getter的結果就是返回getter中的值。 return watcher.value }
這里我們看下watcher.evaluate的部分。
// class Watcher內部 /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false }
這里this.get即得到了value的值,這就是第一個問題的答案。
1.計算屬性何時會計算。
即用到的時候會計算,精確的說,就是在計算vnode的時候會用到它,從而計算它。
對于第二個問題,計算屬性是怎么知道它本身依賴于哪些屬性的?則是在這個
this.get內。
// Dep相關邏輯,Dep Class用來收集依賴某個值的watcher Dep.target = null const targetStack = [] export function pushTarget (_target: Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() } // Watcher class 相關邏輯 get () { // 將當前的watcher推到Dep.target中 pushTarget(this) let value const vm = this.vm try { // 這里的getter實際上就是對應total的函數體, // 而這個函數體內藏有很大的貓膩,接下來我們仔細分析這一段。 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 }
當代碼執行到this.getter.call,實際上執行的是計算屬性的函數,也就是
total() { return this.a + this.b};當代碼執行到this.a時候。就會觸發上一節我們所講的defineReactive內部的代碼。
//// 這里我們以訪問this.a為例 export function defineReactive ( obj: Object, // {a:1,b:1} key: string, // "a" val: any, // 1 customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // this.a會觸發這里的代碼。首先獲得value, // 由于watcher內部this.get執行total計算屬性時候,已經將 // total的watcher設置為Dep.target if (Dep.target) { // 所以這里開始收集依賴。 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, 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() } }) }
上述代碼中,this.a觸發了dep.depend()。我們細看這里的代碼。
class Dep { //省略代碼... depend () { // 由于這里的Dep.target此時對應的是total的watcher。 // 而這里的this.是指定義this.a時,生成的dep。 // 所以這里是告訴total依賴于this.a if (Dep.target) { // 通過調用addDep.讓total的watcher知道total依賴this.a Dep.target.addDep(this) } } } class Watcher { // ...省略代碼 addDep (dep: Dep) { // 此時的this是total的watcher const id = dep.id // 防止重復收集 if (!this.newDepIds.has(id)) { // 將依賴的可觀察對象記錄。 this.newDepIds.add(id) this.newDeps.push(dep) // 如果這個可觀察對象沒有記錄當前watcher, if (!this.depIds.has(id)) { // 則將當前的watcher加入到可觀察對象中 // (方便后續a變化后,告知total) dep.addSub(this) } } } }
至此,上述的第二個問題,計算屬性是怎么知道它本身依賴于哪些屬性的?也有了答案。就是當生成虛擬dom的時候,用到了total,由于得到total值的watcher是臟的,需要計算一次,然后就將Dep.target的watcher設為total相關的watcher。并在watcher內執行了total函數,在函數內部,訪問了this.a。this.a的getter中,通過dep.depend(),將this.a的getter上方的dep,加入到total的watcher.dep中,再通過watcher中的dep.addSub(this),將total的watcher加入到了this.a的getter上方中的dep中。至此total知道了它依賴于this.a。this.a也知道了,total需要this.a。
當計算屬性的依賴變更時發生了什么當點擊頁面按鈕的時候,會執行我們案例中綁定的this.a += 1的代碼。此時會走
this.a的setter函數。我們看看setter中所做的事情。
set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 如果舊值與新值相當,什么都不做。直接返回。 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 無關代碼,pass if (process.env.NODE_ENV !== "production" && customSetter) { customSetter() } // 有定義過setter的話通過setter設置新值 if (setter) { setter.call(obj, newVal) } else { // 否則的話直接設置新值 val = newVal } // 考慮新值是對象的情況。 childOb = !shallow && observe(newVal) // 通知觀察了this.a的觀察者。 // 這里實際上是有兩個觀察a的觀察者 // 一個是上一篇講的updateComponent。 // 一個是這節講的total。 dep.notify() }
這里我們看看dep.notify干了什么
class Dep { // **** 其他代碼 notify () { // 這里的subs其實就是上述的兩個watcher。 // 分別執行watcher的update const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } class Watcher{ update () { // 第一個watcher,即關于updateComponent的。 // 會執行queueWatcher。也就是會將處理放到等待隊列里 // 等待隊列中,而第二個watcher由于lazy為true, // 所以只是將watcher標記為dirty。 // 由于隊列這個比較復雜,所以單開話題去講 // 這里我們只需要知道它是一個異步的隊列,最后結果就是 // 挨個執行隊列中watcher的run方法。 if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { 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(關于total的)會將自己的dirty標記為true,第二個則會執行run方法,在其中運行this.get導致updateComponent執行,進而再次計算vnode,這時會再次計算this.total。則會再次觸發total的getter,這時候我們再復習一下之前講過的這個computed的getter:
const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示這個值是臟值,過期了。所以需要重新計算。 // new Watcher的時候,這個total的watcher中,內部的dirty已經被置為 // dirty = lazy = true; // 那么這個值什么時候會過期,會臟呢。就是內部的依賴更新時候, // 比如我們的total依賴于this.a,this.b,當著兩個值任意一個變化時候 // 我們的total就已經臟了。需要根據最新的a,b計算。 if (watcher.dirty) { // 計算watcher中的值,即value屬性. watcher.evaluate() } // 將依賴添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的結果就是返回getter中的值。 return watcher.value }
至此,computed中total的更新流程也結束了。
所以我們的第3個問題,vue官方文檔的緩存計算結果怎么理解?也就有了答案。也就是說計算屬性只有其依賴變更的時候才會去計算,依賴不更新的時候,是不會計算的。正文這一小節提到的,total的更新是由于this.a的更新導致其setter被觸發,因此通知了其依賴,即total這個watcher。如果total的不依賴于this.a,則total相關的watcher的dirty就不會變為true,也就不會再次計算了。
本章節我們以示例程序探究了計算屬性,從initComputed中,計算屬性的初始化到計算屬性的變更,對著代碼做了進一步的解釋。整體流程可以歸納為:
initComputed定義了相關的計算屬性相關的watcher,以及watcher的getter。
在第一次計算vnode的時候順便執行了計算屬性的計算邏輯,順便收集了依賴。本例中total收集到了依賴a,b;并且a,b也被告知total觀察了他們。當a,b任何一個改變時的時候,就會將total相關的watcher.dirty設置為true,下次需要更新界面時,計算屬性就會被重新計算。當然,如果沒有依賴于total的地方。那么total是不會計算的,例如total根本沒被界面或者js代碼用到,就不會計算total;如果total所有的依賴沒有變更,其dirty為false,則也是無需計算的。
vue源碼分析系列
vue源碼分析系列之debug環境搭建
vue源碼分析系列之入口文件分析
vue源碼分析系列之響應式數據(一)
vue源碼分析系列之響應式數據(二)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/101737.html
摘要:執行當時傳入的回調,并將新值與舊值一并傳入。文章鏈接源碼分析系列源碼分析系列之環境搭建源碼分析系列之入口文件分析源碼分析系列之響應式數據一源碼分析系列之響應式數據二源碼分析系列之響應式數據三 前言 上一節著重講述了initComputed中的代碼,以及數據是如何從computed中到視圖層的,以及data修改后如何作用于computed。這一節主要記錄initWatcher中的內容。 ...
摘要:代碼初始化部分一個的時候做了什么當我們一個時,實際上執行了的構造函數,這個構造函數內部掛載了很多方法,可以在我的上一篇文章中看到。合并構造函數上掛載的與當前傳入的非生產環境,包裝實例本身,在后期渲染時候,做一些校驗提示輸出。 概述 在使用vue的時候,data,computed,watch是一些經常用到的概念,那么他們是怎么實現的呢,讓我們從一個小demo開始分析一下它的流程。 dem...
摘要:中引入了中的中引入了中的中,定義了的構造函數中的原型上掛載了方法,用來做初始化原型上掛載的屬性描述符,返回原型上掛載的屬性描述符返回原型上掛載與方法,用來為對象新增刪除響應式屬性原型上掛載方法原型上掛載事件相關的方法。 入口尋找 入口platforms/web/entry-runtime-with-compiler中import了./runtime/index導出的vue。 ./r...
摘要:目標是為了可以調試版本的,也就是下的源碼,所以主要是的開啟。結語至此就可以開心的研究源碼啦。文章鏈接源碼分析系列源碼分析系列之入口文件分析源碼分析系列之響應式數據一源碼分析系列之響應式數據二 概述 為了探究vue的本質,所以想debug一下源碼,但是怎么開始是個問題,于是有了這樣一篇記錄。目標是為了可以調試es6版本的,也就是src下的源碼,所以主要是sourceMap的開啟。原文來自...
閱讀 1396·2021-10-19 11:42
閱讀 733·2021-09-22 16:04
閱讀 1885·2021-09-10 11:23
閱讀 1864·2021-07-29 14:48
閱讀 1264·2021-07-26 23:38
閱讀 2824·2019-08-30 15:54
閱讀 1038·2019-08-30 11:25
閱讀 1706·2019-08-29 17:23