摘要:并在內(nèi)執(zhí)行了函數(shù),在函數(shù)內(nèi)部,訪問了。至此知道了它依賴于。需要根據(jù)最新的計算。本例中收集到了依賴并且也被告知觀察了他們。文章鏈接源碼分析系列源碼分析系列之環(huán)境搭建源碼分析系列之入口文件分析源碼分析系列之響應(yīng)式數(shù)據(jù)一源碼分析系列之響應(yīng)式數(shù)據(jù)二
前言
上一節(jié)著重講述了initData中的代碼,以及數(shù)據(jù)是如何從data中到視圖層的,以及data修改后如何作用于視圖。這一節(jié)主要記錄initComputed中的內(nèi)容。
正文 前情回顧在demo示例中,我們定義了一個計算屬性。
computed:{ total(){ return this.a + this.b } }
本章節(jié)我們繼續(xù)探究這個計算屬性的相關(guān)流程。
initComputed// initComputed(vm, opts.computed) function initComputed (vm: Component, computed: Object) { // 定義計算屬性相關(guān)的watchers. const watchers = vm._computedWatchers = Object.create(null) // 是否是服務(wù)端渲染,這里贊不考慮。 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. // 為計算屬性創(chuàng)建一個內(nèi)部的watcher。 // 其中computedWatcherOptions的值為lazy,意味著這個wacther內(nèi)部的value,先不用計算。 // 只有在需要的情況下才計算,這里主要是在后期頁面渲染中,生成虛擬dom的時候才會計算。 // 這時候new Watcher只是走一遍watcher的構(gòu)造函數(shù),其內(nèi)部value由于 // lazy為true,先設(shè)置為了undefined.同時內(nèi)部的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. // 組件定義的屬性只是定義在了組件上,這里只是把它翻譯到實(shí)例中。即當(dāng)前的vm對象。 if (!(key in vm)) { // 將計算屬性定義到實(shí)例中。 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 ) { // 是否需要緩存。即非服務(wù)端渲染需要緩存。 // 由于本案例用的demo非服務(wù)端渲染,這里結(jié)果是true const shouldCache = !isServerRendering() if (typeof userDef === "function") { // userDef = total() {...} sharedPropertyDefinition.get = shouldCache // 根據(jù)key創(chuàng)建計算屬性的getter ? createComputedGetter(key) : userDef // 計算屬性是只讀的,所以設(shè)置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 } // 計算屬性是只讀的,所以設(shè)置值得時候需要報錯提示 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 ) } } // 將組件屬性-》實(shí)例屬性,關(guān)鍵的一句,設(shè)置屬性描述符 Object.defineProperty(target, key, sharedPropertyDefinition) }createComputedGetter
// 根據(jù)key創(chuàng)建計算屬性的getter // createComputedGetter(key) function createComputedGetter (key) { return function computedGetter () { // 非服務(wù)端渲染的時候,在上述的initComputed中定義了vm._computedWatchers = {},并根據(jù)組件中的設(shè)定watchers[key] = new Watcher(..),這里只是根據(jù)key取出了當(dāng)時new的watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示這個值是臟值,過期了。所以需要重新計算。 // new Watcher的時候,這個total的watcher中,內(nèi)部的dirty已經(jīng)被置為 // dirty = lazy = true; // 那么這個值什么時候會過期,會臟呢。就是內(nèi)部的依賴更新時候, // 比如我們的total依賴于this.a,this.b,當(dāng)著兩個值任意一個變化時候 // 我們的total就已經(jīng)臟了。需要根據(jù)最新的a,b計算。 if (watcher.dirty) { // 計算watcher中的值,即value屬性. watcher.evaluate() } // 將依賴添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的結(jié)果就是返回getter中的值。 return watcher.value } } }initComputed小結(jié)
繼initComputed之后,所有組件中的computed都被賦值到了vm實(shí)例的屬性上,并設(shè)置好了getter和setter。在非服務(wù)端渲染的情況下,getter會緩存計算結(jié)果。并在需要的時候,才計算。setter則是一個什么都不做的函數(shù),預(yù)示著計算屬性只能被get,不能被set。即只讀的。
接下來的問題就是:
這個計算屬性什么時候會計算,前文{lazy:true}預(yù)示著當(dāng)時new Watcher得到的值是undefined。還沒開始計算。
計算屬性是怎么知道它本身依賴于哪些屬性的。以便知道其什么時候更新。
vue官方文檔的緩存計算結(jié)果怎么理解。
接下來我們繼續(xù)剖析后面的代碼。解決這里提到的三個問題。
用來生成vnode的render函數(shù)下次再見到這個計算屬性total的時候,已是在根據(jù)el選項或者template模板中,生成的render函數(shù),render函數(shù)上一小節(jié)也提到過。長這個樣子。
(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")])])]) } } )
這里可以結(jié)合一下我們的html,看出一些特點(diǎn)。
a:{{a}}
b: {}
a+b: {{total}}
這里使用到計算屬性的主要是這一句
_v("a+b: " + _s(total))
那么對于我們來說的關(guān)鍵就是_s(total)。由于這個函數(shù)的with(this)中,this被設(shè)置為vm實(shí)例,所以這里就可以理解為_s(vm.total)。那么這里就會觸發(fā)之前定義的sharedPropertyDefinition.get
-> initComputed() -> defineComputed() -> Object.defineProperty(target, key, sharedPropertyDefinition)
也就是createComputedGetter返回的函數(shù)中的內(nèi)容,也就是:
watcher細(xì)說const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 由于初始化的時候這個dirty為true,所以會進(jìn)行watcher.evaluate()的計算。 if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } // getter的結(jié)果就是返回getter中的值。 return watcher.value }
這里我們看下watcher.evaluate的部分。
// class Watcher內(nèi)部 /** * 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內(nèi)。
// Dep相關(guān)邏輯,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 相關(guān)邏輯 get () { // 將當(dāng)前的watcher推到Dep.target中 pushTarget(this) let value const vm = this.vm try { // 這里的getter實(shí)際上就是對應(yīng)total的函數(shù)體, // 而這個函數(shù)體內(nèi)藏有很大的貓膩,接下來我們仔細(xì)分析這一段。 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 }
當(dāng)代碼執(zhí)行到this.getter.call,實(shí)際上執(zhí)行的是計算屬性的函數(shù),也就是
total() { return this.a + this.b};當(dāng)代碼執(zhí)行到this.a時候。就會觸發(fā)上一節(jié)我們所講的defineReactive內(nèi)部的代碼。
//// 這里我們以訪問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會觸發(fā)這里的代碼。首先獲得value, // 由于watcher內(nèi)部this.get執(zhí)行total計算屬性時候,已經(jīng)將 // total的watcher設(shè)置為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觸發(fā)了dep.depend()。我們細(xì)看這里的代碼。
class Dep { //省略代碼... depend () { // 由于這里的Dep.target此時對應(yīng)的是total的watcher。 // 而這里的this.是指定義this.a時,生成的dep。 // 所以這里是告訴total依賴于this.a if (Dep.target) { // 通過調(diào)用addDep.讓total的watcher知道total依賴this.a Dep.target.addDep(this) } } } class Watcher { // ...省略代碼 addDep (dep: Dep) { // 此時的this是total的watcher const id = dep.id // 防止重復(fù)收集 if (!this.newDepIds.has(id)) { // 將依賴的可觀察對象記錄。 this.newDepIds.add(id) this.newDeps.push(dep) // 如果這個可觀察對象沒有記錄當(dāng)前watcher, if (!this.depIds.has(id)) { // 則將當(dāng)前的watcher加入到可觀察對象中 // (方便后續(xù)a變化后,告知total) dep.addSub(this) } } } }
至此,上述的第二個問題,計算屬性是怎么知道它本身依賴于哪些屬性的?也有了答案。就是當(dāng)生成虛擬dom的時候,用到了total,由于得到total值的watcher是臟的,需要計算一次,然后就將Dep.target的watcher設(shè)為total相關(guān)的watcher。并在watcher內(nèi)執(zhí)行了total函數(shù),在函數(shù)內(nèi)部,訪問了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。
當(dāng)計算屬性的依賴變更時發(fā)生了什么當(dāng)點(diǎn)擊頁面按鈕的時候,會執(zhí)行我們案例中綁定的this.a += 1的代碼。此時會走
this.a的setter函數(shù)。我們看看setter中所做的事情。
set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 如果舊值與新值相當(dāng),什么都不做。直接返回。 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 無關(guān)代碼,pass if (process.env.NODE_ENV !== "production" && customSetter) { customSetter() } // 有定義過setter的話通過setter設(shè)置新值 if (setter) { setter.call(obj, newVal) } else { // 否則的話直接設(shè)置新值 val = newVal } // 考慮新值是對象的情況。 childOb = !shallow && observe(newVal) // 通知觀察了this.a的觀察者。 // 這里實(shí)際上是有兩個觀察a的觀察者 // 一個是上一篇講的updateComponent。 // 一個是這節(jié)講的total。 dep.notify() }
這里我們看看dep.notify干了什么
class Dep { // **** 其他代碼 notify () { // 這里的subs其實(shí)就是上述的兩個watcher。 // 分別執(zhí)行watcher的update const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } class Watcher{ update () { // 第一個watcher,即關(guān)于updateComponent的。 // 會執(zhí)行queueWatcher。也就是會將處理放到等待隊列里 // 等待隊列中,而第二個watcher由于lazy為true, // 所以只是將watcher標(biāo)記為dirty。 // 由于隊列這個比較復(fù)雜,所以單開話題去講 // 這里我們只需要知道它是一個異步的隊列,最后結(jié)果就是 // 挨個執(zhí)行隊列中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) } } } } }
當(dāng)觸發(fā)了依賴更新時候,第一個watcher(關(guān)于total的)會將自己的dirty標(biāo)記為true,第二個則會執(zhí)行run方法,在其中運(yùn)行this.get導(dǎo)致updateComponent執(zhí)行,進(jìn)而再次計算vnode,這時會再次計算this.total。則會再次觸發(fā)total的getter,這時候我們再復(fù)習(xí)一下之前講過的這個computed的getter:
const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示這個值是臟值,過期了。所以需要重新計算。 // new Watcher的時候,這個total的watcher中,內(nèi)部的dirty已經(jīng)被置為 // dirty = lazy = true; // 那么這個值什么時候會過期,會臟呢。就是內(nèi)部的依賴更新時候, // 比如我們的total依賴于this.a,this.b,當(dāng)著兩個值任意一個變化時候 // 我們的total就已經(jīng)臟了。需要根據(jù)最新的a,b計算。 if (watcher.dirty) { // 計算watcher中的值,即value屬性. watcher.evaluate() } // 將依賴添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的結(jié)果就是返回getter中的值。 return watcher.value }
至此,computed中total的更新流程也結(jié)束了。
所以我們的第3個問題,vue官方文檔的緩存計算結(jié)果怎么理解?也就有了答案。也就是說計算屬性只有其依賴變更的時候才會去計算,依賴不更新的時候,是不會計算的。正文這一小節(jié)提到的,total的更新是由于this.a的更新導(dǎo)致其setter被觸發(fā),因此通知了其依賴,即total這個watcher。如果total的不依賴于this.a,則total相關(guān)的watcher的dirty就不會變?yōu)閠rue,也就不會再次計算了。
本章節(jié)我們以示例程序探究了計算屬性,從initComputed中,計算屬性的初始化到計算屬性的變更,對著代碼做了進(jìn)一步的解釋。整體流程可以歸納為:
initComputed定義了相關(guān)的計算屬性相關(guān)的watcher,以及watcher的getter。
在第一次計算vnode的時候順便執(zhí)行了計算屬性的計算邏輯,順便收集了依賴。本例中total收集到了依賴a,b;并且a,b也被告知total觀察了他們。當(dāng)a,b任何一個改變時的時候,就會將total相關(guān)的watcher.dirty設(shè)置為true,下次需要更新界面時,計算屬性就會被重新計算。當(dāng)然,如果沒有依賴于total的地方。那么total是不會計算的,例如total根本沒被界面或者js代碼用到,就不會計算total;如果total所有的依賴沒有變更,其dirty為false,則也是無需計算的。
vue源碼分析系列
vue源碼分析系列之debug環(huán)境搭建
vue源碼分析系列之入口文件分析
vue源碼分析系列之響應(yīng)式數(shù)據(jù)(一)
vue源碼分析系列之響應(yīng)式數(shù)據(jù)(二)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/101737.html
摘要:執(zhí)行當(dāng)時傳入的回調(diào),并將新值與舊值一并傳入。文章鏈接源碼分析系列源碼分析系列之環(huán)境搭建源碼分析系列之入口文件分析源碼分析系列之響應(yīng)式數(shù)據(jù)一源碼分析系列之響應(yīng)式數(shù)據(jù)二源碼分析系列之響應(yīng)式數(shù)據(jù)三 前言 上一節(jié)著重講述了initComputed中的代碼,以及數(shù)據(jù)是如何從computed中到視圖層的,以及data修改后如何作用于computed。這一節(jié)主要記錄initWatcher中的內(nèi)容。 ...
摘要:代碼初始化部分一個的時候做了什么當(dāng)我們一個時,實(shí)際上執(zhí)行了的構(gòu)造函數(shù),這個構(gòu)造函數(shù)內(nèi)部掛載了很多方法,可以在我的上一篇文章中看到。合并構(gòu)造函數(shù)上掛載的與當(dāng)前傳入的非生產(chǎn)環(huán)境,包裝實(shí)例本身,在后期渲染時候,做一些校驗提示輸出。 概述 在使用vue的時候,data,computed,watch是一些經(jīng)常用到的概念,那么他們是怎么實(shí)現(xiàn)的呢,讓我們從一個小demo開始分析一下它的流程。 dem...
摘要:中引入了中的中引入了中的中,定義了的構(gòu)造函數(shù)中的原型上掛載了方法,用來做初始化原型上掛載的屬性描述符,返回原型上掛載的屬性描述符返回原型上掛載與方法,用來為對象新增刪除響應(yīng)式屬性原型上掛載方法原型上掛載事件相關(guān)的方法。 入口尋找 入口platforms/web/entry-runtime-with-compiler中import了./runtime/index導(dǎo)出的vue。 ./r...
摘要:目標(biāo)是為了可以調(diào)試版本的,也就是下的源碼,所以主要是的開啟。結(jié)語至此就可以開心的研究源碼啦。文章鏈接源碼分析系列源碼分析系列之入口文件分析源碼分析系列之響應(yīng)式數(shù)據(jù)一源碼分析系列之響應(yīng)式數(shù)據(jù)二 概述 為了探究vue的本質(zhì),所以想debug一下源碼,但是怎么開始是個問題,于是有了這樣一篇記錄。目標(biāo)是為了可以調(diào)試es6版本的,也就是src下的源碼,所以主要是sourceMap的開啟。原文來自...
閱讀 1392·2021-10-19 11:42
閱讀 731·2021-09-22 16:04
閱讀 1882·2021-09-10 11:23
閱讀 1862·2021-07-29 14:48
閱讀 1261·2021-07-26 23:38
閱讀 2821·2019-08-30 15:54
閱讀 1037·2019-08-30 11:25
閱讀 1704·2019-08-29 17:23