vue 计算属性_lt;Vue 源码笔记系列6gt;计算属性 computed 的实现
1. 前言
原文發(fā)布在語雀:
<Vue 源碼筆記系列6>計算屬性 computed 的實現(xiàn) · 語雀?www.yuque.com上一章我們已經(jīng)學(xué)習(xí)過 watch,這一章就來看一下計算屬性 computed 的實現(xiàn)。
2. 流程圖
老規(guī)矩,先上圖。
但是本期的流程圖比較簡略,因為 computed 的實現(xiàn)很大程度上依賴了之前我們講的數(shù)據(jù)響應(yīng)式原理的部分,這部分代碼主要是橋梁的作用。而數(shù)據(jù)響應(yīng)式我們花了三章來講,所以這里的流程圖就不再包含重復(fù)的內(nèi)容了。
不過也不用擔(dān)心,代碼講解完畢后我們會根據(jù)一個小的示例來詳細說明每一部分是如何工作的,在那里我們會附上針對性的講解圖,新的講解圖將會覆蓋到之前以講過的內(nèi)容。但是仍然建議不熟悉前三章的同學(xué)先回顧一下,因為他們是基礎(chǔ)中的基礎(chǔ)。
3. computed 初始化
仍然從 initState 講起:
// src/core/instance/state.jsexport function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)} }可以大致看到,initState 的作用是初始化 Prop、 Methods、Data、Computed、Watch。并且是按照順序此順序進行初始化工作的。
前邊我們已經(jīng)了解過 initData 和 initWatch 了,本期我們來看看 initComputed,剩下的內(nèi)容放在后邊的章節(jié)。
如果傳入了 computed 選項,調(diào)用 initComputed,并將 Vue 實例 vm,以及 computed 選項作為參數(shù)。
3.1 initComputed
// src/core/instance/state.jsfunction initComputed (vm: Component, computed: Object) {// $flow-disable-lineconst watchers = vm._computedWatchers = Object.create(null)// computed properties are just getters during SSRconst isSSR = isServerRendering()for (const key in computed) {const userDef = computed[key]const getter = typeof userDef === 'function' ? userDef : userDef.getif (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.watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions)}// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.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)}}} }第 5 行:
const watchers = vm._computedWatchers = Object.create(null)首先聲明變量 watchers,賦值為 vm._computedWatchers,并且初始化值為空對象。
接下來是遍歷 computed:
for (const key in computed) {//... }來看一下遍歷 computed 時做了什么事:
第 10 到 17 行:
聲明 userDef 為 computed 當次遍歷的鍵值。
如果 userDef 為函數(shù)則將其值賦給 getter,否則 getter 值為 userDef.get。
然后在開發(fā)環(huán)境下,getter 如果為 null 打印警告。
如此我們就可以理解 computed 的兩種寫法了:
19 到 27 行:
if (!isSSR) {// create internal watcher for the computed property.watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions) }非服務(wù)端渲染的情況下:
針對當次循環(huán)的 computed,調(diào)用 new Watcher。watchers 保存了 vm._computedWatchers 的引用,所以這里同樣會將該 watcher 保存到 vm._computedWatchers。所以我們可以知道,每一個 computed 的 key,都會生成一個 watcher 實例,并且保存到 vm._computedWatchers 這個對象上。
new Watcher 做的事情,我們在依賴收集的章節(jié)已經(jīng)詳細介紹過:
與之前渲染函數(shù)的觀察者不太相同的地方是在 Watcher 構(gòu)造函數(shù)的最后一部分:
// src/core/observer/watcher.jsif (this.computed) {this.value = undefinedthis.dep = new Dep() } else {this.value = this.get() }我們這里的 watcher 實例稱為計算屬性觀察者,this.computed 為 true,所以在初始化階段并沒有觸發(fā) this.get,另外我們還為 watcher 添加了 dep 屬性。這兩點區(qū)別是非常重要的。
生成渲染函數(shù)觀察者之后,initComputed 剩下的代碼如下:
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)} }if 語句用來檢測 computed 的命名是否與 data,props 沖突,在非生產(chǎn)環(huán)境將會打印警告信息。
不沖突時,調(diào)用 defineComputed 方法。
3.2 defineComputed
// src/core/instance/state.js const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop }export function defineComputed (target: any,key: string,userDef: Object | Function ) {const shouldCache = !isServerRendering()if (typeof userDef === 'function') {sharedPropertyDefinition.get = shouldCache? createComputedGetter(key): userDefsharedPropertyDefinition.set = noop} else {sharedPropertyDefinition.get = userDef.get? shouldCache && userDef.cache !== false? createComputedGetter(key): userDef.get: noopsharedPropertyDefinition.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) }代碼比較簡單,主要是為 sharedPropertyDefinition 添加 get, set 屬性,值為 computed 選項相關(guān)。最后將該 computed 屬性添加到 Vue 實例 vm 上,并使用 sharedPropertyDefinition 作為設(shè)置項。
其中 get 部分涉及到一個方法:
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {watcher.depend()return watcher.evaluate()}} }在這里我們只需要知道 get 被設(shè)置為這個方法的返回值就行,具體的執(zhí)行過程我們在觸發(fā)階段詳細講。
4. computed 依賴收集的觸發(fā)與更新
初始化完畢后我們的準備工作就完成了,那么Vue 是如何收集到依賴,又是如何在 data 變化時更新的呢。為了更好地理解,我們用一個示例來具體講解。
有如下 data 與 computed:
以及如下模板:
<div>{{ compA }}</div>在依賴收集的觸發(fā)中,我們講解過 data 觸發(fā)依賴收集的過程相關(guān)代碼。
我們依然從 $mount 講起,$mount 實際是調(diào)用 mountComponent, 在 mountComponent 中執(zhí)行 new Watcher,這個 watcher 為渲染函數(shù)的觀察者即 renderWatcher。代碼如下:
我們進入 Watcher 中看一下:
// src/core/observer/watcher.jsexport default class Watcher {// ...constructor () {// ...if (this.computed) {this.value = undefinedthis.dep = new Dep()} else {this.value = this.get()}}get () {pushTarget(this)// ...value = this.getter.call(vm, vm)// ...return value} }因為這里是渲染函數(shù)的觀察者,所以會執(zhí)行 this.get,在 get 中我們執(zhí)行了 pushTarget:
export function pushTarget (_target: ?Watcher) {if (Dep.target) targetStack.push(Dep.target)Dep.target = _target }所以此時全局變量 Dep.target 值為渲染函數(shù)觀察者 renderWatcher。
this.get 也執(zhí)行了 this.getter,該方法將生成 VNode,經(jīng)過 patch 再渲染成真實 DOM,所以這里會讀取模板中的值 compA,觸發(fā)我們在computed初始化階段為其設(shè)置的 get 攔截器。我們知道攔截器代碼如下:
function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {watcher.depend()return watcher.evaluate()} }先從 vm._computedWatchers 找到 compA 的計算屬性觀察者 computedWatcher。
接著調(diào)用 computedWatcher 的 depend 方法。
注釋已經(jīng)告訴我們這個方法是專為 computed 設(shè)計的。
前邊我們講到過 computedWatcher 的獨特之處在于沒有調(diào)用 this.get, 為自己添加了 this.dep 屬性。這里調(diào)用了 this.dep.depend:
還記得之前的加粗提示文字嗎,我們說 此時全局變量 Dep.target 值為渲染函數(shù)觀察者 renderWatcher,所以這里 renderWatcher 收集了這個 dep。考慮下為什么 computedWatcher 初始化時不調(diào)用 this.get 嗎,原因之一就是,調(diào)用 this.get 會改變 Dep.target 的值。
// src/core/observer/watcher.jsaddDep (dep: Dep) {// ...dep.addSub(this) }需要注意的是這里的 dep 為 computedWatcher的 dep 屬性
// src/core/observer/dep.jsaddSub (sub: Watcher) {this.subs.push(sub) }執(zhí)行完畢后,computedWatcher 的 dep.subs 包含了 renderWatcher。這就建立了 compA 與渲染函數(shù)的橋梁。
到這里我們?yōu)?vm.compA 設(shè)置的 get 攔截器還沒完呢,下邊還有一句 return watcher.evaluate(),看一下 evaluate:
// src/core/observer/watcher.jsevaluate () {if (this.dirty) {this.value = this.get()this.dirty = false}return this.value }this.dirty 標志是否還沒有求值,因為 computed 是惰性求值所以有此判斷。
我們在這里才調(diào)用了 this.get, 在 get 執(zhí)行中將 Dep.target 設(shè)置為 computedWatcher,然后執(zhí)行 this.getter,這里對應(yīng)為:
這里我們使用了 this.a , 觸發(fā)了其 get 攔截器(前三章有講):
get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()if (Array.isArray(value)) {dependArray(value)}}}return value }可以知道在 a 的 dep.subs 中保存了此時的 computedWatcher,這就建立了 compA 與 a 的聯(lián)系。
綜上,我們可以知道 Vue 在 compA 與 a 與 renderWatcher 之間建立了聯(lián)系,如下圖:
a 的閉包 dep.subs 包含了 compA 對應(yīng)的 computedWatcher, computedWatcher 的 dep.subs 包含了 renderWatcher。
5. data 改變觸發(fā) computed 的改變
接著上邊的示例,當 a 改變時, 如:
this.a = 2改變 a 將觸發(fā)其 set 攔截器:
// src/core/observer/index.js set: function reactiveSetter (newVal) {// ...dep.notify() }dep.notify:
notify () {// stabilize the subscriber list firstconst subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()} }我們知道 a 閉包的 dep.subs 包含了 compA 的 computedWatcher。這里就會調(diào)用computedWatcher的 update 方法:
update () {if (this.computed) {if (this.dep.subs.length === 0) {this.dirty = true} else {this.getAndInvoke(() => {this.dep.notify()})}} else if (this.sync) {this.run()} else {queueWatcher(this)} }computedWatcher 的 computed 屬性為 true。
判斷 computedWatcher.subs 長度不為 0 時,調(diào)用 getAndInvoke,這個函數(shù)將會判斷值是否變化,當compA 的新舊值不同時,執(zhí)行回調(diào) this.dep.notify。
computedWatcher.dep 包含了 renderWatcher,notify 將調(diào)用 renderWatcher 的 update 方法。最終將renderWatcher 加入異步隊列,在合適的時機執(zhí)行,最終更新DOM。
6. 總結(jié)
computed 的初始化工作就是在 computed 與 data、renderWatcher 之間建立聯(lián)系。核心仍然是響應(yīng)式那一套。得益于良好的設(shè)計,這部分代碼并不復(fù)雜。
關(guān)于 initState 函數(shù),我們還剩下 initProps 與 initMethods 沒有介紹,別著急,下一章就是了。
總結(jié)
以上是生活随笔為你收集整理的vue 计算属性_lt;Vue 源码笔记系列6gt;计算属性 computed 的实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java中四类八中_JAVA中的八中基本
- 下一篇: linux修改容器内的mysql端口_L