vue 计算属性_lt;Vue 源码笔记系列6gt;计算属性 computed 的实现
1. 前言
原文發布在語雀:
<Vue 源碼筆記系列6>計算屬性 computed 的實現 · 語雀?www.yuque.com上一章我們已經學習過 watch,這一章就來看一下計算屬性 computed 的實現。
2. 流程圖
老規矩,先上圖。
但是本期的流程圖比較簡略,因為 computed 的實現很大程度上依賴了之前我們講的數據響應式原理的部分,這部分代碼主要是橋梁的作用。而數據響應式我們花了三章來講,所以這里的流程圖就不再包含重復的內容了。
不過也不用擔心,代碼講解完畢后我們會根據一個小的示例來詳細說明每一部分是如何工作的,在那里我們會附上針對性的講解圖,新的講解圖將會覆蓋到之前以講過的內容。但是仍然建議不熟悉前三章的同學先回顧一下,因為他們是基礎中的基礎。
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。并且是按照順序此順序進行初始化工作的。
前邊我們已經了解過 initData 和 initWatch 了,本期我們來看看 initComputed,剩下的內容放在后邊的章節。
如果傳入了 computed 選項,調用 initComputed,并將 Vue 實例 vm,以及 computed 選項作為參數。
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 為函數則將其值賦給 getter,否則 getter 值為 userDef.get。
然后在開發環境下,getter 如果為 null 打印警告。
如此我們就可以理解 computed 的兩種寫法了:
19 到 27 行:
if (!isSSR) {// create internal watcher for the computed property.watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions) }非服務端渲染的情況下:
針對當次循環的 computed,調用 new Watcher。watchers 保存了 vm._computedWatchers 的引用,所以這里同樣會將該 watcher 保存到 vm._computedWatchers。所以我們可以知道,每一個 computed 的 key,都會生成一個 watcher 實例,并且保存到 vm._computedWatchers 這個對象上。
new Watcher 做的事情,我們在依賴收集的章節已經詳細介紹過:
與之前渲染函數的觀察者不太相同的地方是在 Watcher 構造函數的最后一部分:
// src/core/observer/watcher.jsif (this.computed) {this.value = undefinedthis.dep = new Dep() } else {this.value = this.get() }我們這里的 watcher 實例稱為計算屬性觀察者,this.computed 為 true,所以在初始化階段并沒有觸發 this.get,另外我們還為 watcher 添加了 dep 屬性。這兩點區別是非常重要的。
生成渲染函數觀察者之后,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 沖突,在非生產環境將會打印警告信息。
不沖突時,調用 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 選項相關。最后將該 computed 屬性添加到 Vue 實例 vm 上,并使用 sharedPropertyDefinition 作為設置項。
其中 get 部分涉及到一個方法:
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {watcher.depend()return watcher.evaluate()}} }在這里我們只需要知道 get 被設置為這個方法的返回值就行,具體的執行過程我們在觸發階段詳細講。
4. computed 依賴收集的觸發與更新
初始化完畢后我們的準備工作就完成了,那么Vue 是如何收集到依賴,又是如何在 data 變化時更新的呢。為了更好地理解,我們用一個示例來具體講解。
有如下 data 與 computed:
以及如下模板:
<div>{{ compA }}</div>在依賴收集的觸發中,我們講解過 data 觸發依賴收集的過程相關代碼。
我們依然從 $mount 講起,$mount 實際是調用 mountComponent, 在 mountComponent 中執行 new Watcher,這個 watcher 為渲染函數的觀察者即 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} }因為這里是渲染函數的觀察者,所以會執行 this.get,在 get 中我們執行了 pushTarget:
export function pushTarget (_target: ?Watcher) {if (Dep.target) targetStack.push(Dep.target)Dep.target = _target }所以此時全局變量 Dep.target 值為渲染函數觀察者 renderWatcher。
this.get 也執行了 this.getter,該方法將生成 VNode,經過 patch 再渲染成真實 DOM,所以這里會讀取模板中的值 compA,觸發我們在computed初始化階段為其設置的 get 攔截器。我們知道攔截器代碼如下:
function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {watcher.depend()return watcher.evaluate()} }先從 vm._computedWatchers 找到 compA 的計算屬性觀察者 computedWatcher。
接著調用 computedWatcher 的 depend 方法。
注釋已經告訴我們這個方法是專為 computed 設計的。
前邊我們講到過 computedWatcher 的獨特之處在于沒有調用 this.get, 為自己添加了 this.dep 屬性。這里調用了 this.dep.depend:
還記得之前的加粗提示文字嗎,我們說 此時全局變量 Dep.target 值為渲染函數觀察者 renderWatcher,所以這里 renderWatcher 收集了這個 dep。考慮下為什么 computedWatcher 初始化時不調用 this.get 嗎,原因之一就是,調用 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) }執行完畢后,computedWatcher 的 dep.subs 包含了 renderWatcher。這就建立了 compA 與渲染函數的橋梁。
到這里我們為 vm.compA 設置的 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 是惰性求值所以有此判斷。
我們在這里才調用了 this.get, 在 get 執行中將 Dep.target 設置為 computedWatcher,然后執行 this.getter,這里對應為:
這里我們使用了 this.a , 觸發了其 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 的聯系。
綜上,我們可以知道 Vue 在 compA 與 a 與 renderWatcher 之間建立了聯系,如下圖:
a 的閉包 dep.subs 包含了 compA 對應的 computedWatcher, computedWatcher 的 dep.subs 包含了 renderWatcher。
5. data 改變觸發 computed 的改變
接著上邊的示例,當 a 改變時, 如:
this.a = 2改變 a 將觸發其 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。這里就會調用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 時,調用 getAndInvoke,這個函數將會判斷值是否變化,當compA 的新舊值不同時,執行回調 this.dep.notify。
computedWatcher.dep 包含了 renderWatcher,notify 將調用 renderWatcher 的 update 方法。最終將renderWatcher 加入異步隊列,在合適的時機執行,最終更新DOM。
6. 總結
computed 的初始化工作就是在 computed 與 data、renderWatcher 之間建立聯系。核心仍然是響應式那一套。得益于良好的設計,這部分代碼并不復雜。
關于 initState 函數,我們還剩下 initProps 與 initMethods 沒有介紹,別著急,下一章就是了。
總結
以上是生活随笔為你收集整理的vue 计算属性_lt;Vue 源码笔记系列6gt;计算属性 computed 的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java中四类八中_JAVA中的八中基本
- 下一篇: vue-router嵌套路由,默认子路由