vue系列---理解Vue中的computed,watch,methods的區別及原始碼實現(六)
閱讀目錄
- 一. 理解Vue中的computed用法
- 二:computed 和 methods的區別?
- 三:Vue中的watch的用法
- 四:computed的基本原理及原始碼實現
一. 理解Vue中的computed用法
computed是計算屬性的; 它會根據所依賴的資料動態顯示新的計算結果, 該計算結果會被快取起來。computed的值在getter執行後是會被快取的。如果所依賴的資料發生改變時候, 就會重新呼叫getter來計算最新的結果。
下面我們根據官網中的demo來理解下computed的使用及何時使用computed。
computed設計的初衷是為了使模板中的邏輯運算更簡單, 比如在Vue模板中有很多複雜的資料計算的話, 我們可以把該計算邏輯放入到computed中去計算。
下面我們看下官網中的一個demo如下:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> {{ msg.split('').reverse().join('') }} </div> <script type="text/javascript"> new Vue({ el: '#app', data: { msg: 'hello' } }); </script> </body> </html>
如上程式碼, 我們的data屬性中的msg預設值為 'hello'; 然後我們在vue模板中會對該資料值進行反轉操作後輸出資料, 因此在頁面上就會顯示 'olleh'; 這樣的資料。這是一個簡單的運算, 但是如果頁面中的運算比這個還更復雜的話, 這個時候我們可以使用computed來進行計算屬性值, computed的目的就是能使模板中的運算邏輯更簡單。因此我們現在需要把上面的程式碼改寫成下面如下程式碼:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>原來的資料: {{ msg }}</p> <p>反轉後的資料為: {{ reversedMsg }}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } }); </script> </body> </html>
如上程式碼, 我們在computed中聲明瞭一個計算屬性 reversedMsg。我們提供的 reversedMsg 函式, 將用作屬性 vm.reversedMsg 的getter函式; 我們可以在上面例項化後代碼中, 列印如下資訊:
console.log(vm);
列印資訊如下所示, 我們可以看到 vm.reversedMsg = 'olleh';
我們也可以開啟控制檯, 當我們修改 vm.msg 的值後, vm.reversedMsg 的值也會發生改變,如下控制檯列印的資訊可知:
如上列印的資訊我們可以看得到, 我們的 vm.reversedMsg 的值依賴於 vm.msg 的值,當vm.msg的值發生改變時, vm.reversedMsg 的值也會得到更新。
computed 應用場景
1. 適用於一些重複使用資料或複雜及費時的運算。我們可以把它放入computed中進行計算, 然後會在computed中快取起來, 下次就可以直接獲取了。
2. 如果我們需要的資料依賴於其他的資料的話, 我們可以把該資料設計為computed中。
回到頂部二:computed 和 methods的區別?
如上demo程式碼, 如果我們通過在表示式中呼叫方法也可以達到同樣的效果, 現在我們把程式碼改成方法, 如下程式碼:<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>原來的資料: {{ msg }}</p> <p>反轉後的資料為: {{ reversedMsg() }}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, /* computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } */ methods: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } }); console.log(vm); </script> </body> </html>
如上程式碼, 我們反轉後的資料在模板中呼叫的是方法 reversedMsg(); 該方法在methods中也定義了。那麼也可以實現同樣的效果, 那麼他們之間到底有什麼區別呢?
區別是:
1. computed 是基於響應性依賴來進行快取的。只有在響應式依賴發生改變時它們才會重新求值, 也就是說, 當msg屬性值沒有發生改變時, 多次訪問 reversedMsg 計算屬性會立即返回之前快取的計算結果, 而不會再次執行computed中的函式。但是methods方法中是每次呼叫, 都會執行函式的, methods它不是響應式的。
2. computed中的成員可以只定義一個函式作為只讀屬性, 也可以定義成 get/set變成可讀寫屬性, 但是methods中的成員沒有這樣的。
我們可以再看下如下demo:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <div>第一次呼叫computed屬性: {{ reversedMsg }}</div> <div>第二次呼叫computed屬性: {{ reversedMsg }}</div> <div>第三次呼叫computed屬性: {{ reversedMsg }}</div> <!-- 下面是methods呼叫 --> <div>第一次呼叫methods方法: {{ reversedMsg1() }}</div> <div>第二次呼叫methods方法: {{ reversedMsg1() }}</div> <div>第三次呼叫methods方法: {{ reversedMsg1() }}</div> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, computed: { reversedMsg() { console.log(1111); // this 指向 vm 例項 return this.msg.split('').reverse().join('') } }, methods: { reversedMsg1() { console.log(2222); // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } }); console.log(vm); </script> </body> </html>
執行後的結果如下所示:
如上程式碼我們可以看到, 在computed中有屬性reversedMsg, 然後在該方法中會列印 1111; 資訊出來, 在methods中的方法reversedMsg1也會列印 2222 資訊出來, 但是在computed中, 我們除了第一次之後,再次獲取reversedMsg值後拿得是快取裡面的資料, 因此就不會再執行該reversedMsg函數了。但是在methods中, 並沒有快取, 每次執行reversedMsg1()方法後,都會列印資訊。
從上面截圖資訊我們就可以驗證的。
那麼我們現在再來理解下快取的作用是什麼呢? computed為什麼需要快取呢? 我們都知道我們的http也有快取, 對於一些靜態資源, 我們nginx伺服器會快取我們的靜態資源,如果靜態資源沒有發生任何改變的話, 會直接從快取裡面去讀取,這樣就不會重新去請求伺服器資料, 也就是避免了一些無畏的請求, 提高了訪問速度, 優化了使用者體驗。
對於我們computed的也是一樣的。如上面程式碼, 我們呼叫了computed中的reversedMsg方法一共有三次,如果我們也有上百次呼叫或上千次呼叫的話, 如果依賴的資料沒有改變, 那麼每次呼叫都要去計算一遍, 那麼肯定會造成很大的浪費。因此computed就是來優化這件事的。
回到頂部三:Vue中的watch的用法
watch它是一個對data的資料監聽回撥, 當依賴的data的資料變化時, 會執行回撥。在回撥中會傳入newVal和oldVal兩個引數。
Vue實列將會在例項化時呼叫$watch(), 他會遍歷watch物件的每一個屬性。
watch的使用場景是:當在data中的某個資料發生變化時, 我們需要做一些操作, 或者當需要在資料變化時執行非同步或開銷較大的操作時. 我們就可以使用watch來進行監聽。
watch普通監聽和深度監聽
如下普通監聽資料的基本測試程式碼如下:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智個人資訊情況: {{ basicMsg }}</p> <p>空智今年的年齡: <input type="text" v-model="age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { basicMsg: '', age: 31, single: '單身' }, watch: { age(newVal, oldVal) { this.basicMsg = '今年' + newVal + '歲' + ' ' + this.single; } } }); </script> </body> </html>
顯示效果如下:
如上程式碼, 當我們在input輸入框中輸入年齡後, 比如32, 那麼watch就能對 'age' 這個屬性進行監聽,當值發生改變的時候, 就會把最新的計算結果賦值給 'basicMsg' 屬性值, 因此最後在頁面上就會顯示 'basicMsg' 屬性值了。
理解handler方法及immediate屬性
如上watch有一個特點是: 第一次初始化頁面的時候, 是不會去執行age這個屬性監聽的, 只有當age值發生改變的時候才會執行監聽計算. 因此我們上面第一次初始化頁面的時候, 'basicMsg' 屬性值預設為空字串。那麼我們現在想要第一次初始化頁面的時候也希望它能夠執行 'age' 進行監聽, 最後能把結果返回給 'basicMsg' 值來。因此我們需要修改下我們的 watch的方法,需要引入handler方法和immediate屬性, 程式碼如下所示:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智個人資訊情況: {{ basicMsg }}</p> <p>空智今年的年齡: <input type="text" v-model="age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { basicMsg: '', age: 31, single: '單身' }, watch: { age: { handler(newVal, oldVal) { this.basicMsg = '今年' + newVal + '歲' + ' ' + this.single; }, immediate: true } } }); </script> </body> </html>
如上程式碼, 我們給我們的age屬性綁定了一個handler方法。其實我們之前的watch當中的方法預設就是這個handler方法。但是在這裡我們使用了immediate: true; 屬性,含義是: 如果在watch裡面聲明瞭age的話, 就會立即執行裡面的handler方法。如果 immediate 值為false的話,那麼效果就和之前的一樣, 就不會立即執行handler這個方法的。因此設定了 immediate:true的話,第一次頁面載入的時候也會執行該handler函式的。即第一次 basicMsg 有值。
因此第一次頁面初始化效果如下:
理解deep屬性
watch裡面有一個屬性為deep,含義是:是否深度監聽某個物件的值, 該值預設為false。
如下測試程式碼:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智個人資訊情況: {{ basicMsg }}</p> <p>空智今年的年齡: <input type="text" v-model="obj.age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { obj: { basicMsg: '', age: 31, single: '單身' } }, watch: { 'obj': { handler(newVal, oldVal) { this.basicMsg = '今年' + newVal.age + '歲' + ' ' + this.obj.single; }, immediate: true, deep: true // 需要新增deep為true即可對obj進行深度監聽 } } }); </script> </body> </html>
如上測試程式碼, 如果我們不把 deep: true新增的話,當我們在輸入框中輸入值的時候,改變obj.age值後,obj物件中的handler函式是不會被執行到的。受JS的限制, Vue不能檢測到物件屬性的新增或刪除的。它只能監聽到obj這個物件的變化,比如說對obj賦值操作會被監聽到。比如在mounted事件鉤子函式中對我們的obj進行重新賦值操作, 如下程式碼:
mounted() { this.obj = { age: 22, basicMsg: '', single: '單身' }; }
最後我們的頁面會被渲染到 age 為 22; 因此這樣我們的handler函式才會被執行到。如果我們需要監聽物件中的某個屬性值的話, 我們可以使用 deep設定為true即可生效。deep實現機制是: 監聽器會一層層的往下遍歷, 給物件的所有屬性都加上這個監聽器。當然效能開銷會非常大的。
當然我們可以直接對物件中的某個屬性進行監聽的,比如就對 'obj.age' 來進行監聽, 如下程式碼也是可以生效的。
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智個人資訊情況: {{ basicMsg }}</p> <p>空智今年的年齡: <input type="text" v-model="obj.age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { obj: { basicMsg: '', age: 31, single: '單身' } }, watch: { 'obj.age': { handler(newVal, oldVal) { this.basicMsg = '今年' + newVal + '歲' + ' ' + this.obj.single; }, immediate: true, // deep: true // 需要新增deep為true即可對obj進行深度監聽 } } }); </script> </body> </html>
watch 和 computed的區別是:
相同點:他們兩者都是觀察頁面資料變化的。
不同點:computed只有當依賴的資料變化時才會計算, 當資料沒有變化時, 它會讀取快取資料。
watch每次都需要執行函式。watch更適用於資料變化時的非同步操作。
四:computed的基本原理及原始碼實現
computed上面我們也已經說過, 它設計的初衷是: 為了使模板中的邏輯運算更簡單。它有兩大優勢:
1. 使模板中的邏輯更清晰, 方便程式碼管理。
2. 計算之後的值會被快取起來, 依賴的data值改變後會重新計算。
因此我們要理解computed的話, 我們只需要理解如下幾個問題:
1. computed是如何初始化的, 初始化之後做了那些事情?
2. 為什麼我們改變了data中的屬性值後, computed會重新計算, 它是如何實現的?
3. computed它是如何快取值的, 當我們下次訪問該屬性的時候, 是怎樣讀取快取資料的?
理解Vue原始碼中computed實現流程
computed初始化
在理解如何初始化之前, 我們來看如下簡單的demo, 然後一步步看看他們的原始碼是如何做的。
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>原來的資料: {{ msg }}</p> <p>反轉後的資料為: {{ reversedMsg }}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } }); </script> </body> </html>
如上程式碼, 我們看到程式碼入口就是vue的例項化, new Vue({}) 作為入口, 因此會呼叫 vue/src/core/instance/index.js 中的init函式程式碼, 如下所示:
......... 更多程式碼省略 /* @param {options} Object options = { el: '#app', data: { msg: 'hello' }, computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } }; */ import { initMixin } from './init' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue); ..... 更多程式碼省略 export default Vue;
如上程式碼, 會執行 this._init(options); 方法內部,因此會呼叫 vue/src/core/instance/init.js 檔案中的_init方法, 基本程式碼如下所示:
import { initState } from './state'; export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { .... 更多程式碼省略 initState(vm); .... 更多程式碼省略 } }
因此繼續執行 initState(vm); 中的程式碼了, 因此會呼叫 vue/src/core/instance/state.js 中的檔案程式碼, 基本程式碼如下:
import config from '../config' import Watcher from '../observer/watcher' import Dep, { pushTarget, popTarget } from '../observer/dep' ..... 更多程式碼省略 /* @param {vm} vm = { $attrs: {}, $children: [], $listeners: {}, $options: { components: {}, computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } }, el: '#app', ..... 更多屬性值 }, .... 更多屬性 }; */ export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (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) } } ..... 更多程式碼省略
如上程式碼, 形參上的vm引數值基本值如上註釋。程式碼內部先判斷 vm.$options.props 是否有該屬性, 有的話, 就呼叫 initProps()方法進行初始化, 接著會判斷 vm.$options.methods; 是否有該方法, 有的話,呼叫 initMethods() 方法進行初始化。這些所有的我們先不看, 我們這邊最主要的是看 if (opts.computed) initComputed(vm, opts.computed) 這句程式碼; 判斷 vm.$options.computed 是否有, 如果有的話, 就執行 initComputed(vm, opts.computed); 函式。因此我們找到 initComputed函式程式碼如下:
/* @param {vm} 值如下: vm = { $attrs: {}, $children: [], $listeners: {}, $options: { components: {}, computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } }, el: '#app', ..... 更多屬性值 }, .... 更多屬性 }; @param {computed} Object computed = { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } }; */ const computedWatcherOptions = { lazy: true }; function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null); // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { 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. 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) } } } }
如上程式碼, 首先使用 Object.create(null); 建立一個空物件, 分別賦值給 watchers; 和 vm._computedWatchers; 接著執行程式碼:
const isSSR = isServerRendering(); 判斷是否是伺服器端渲染, 我們這邊肯定不是伺服器端渲染,因此 const isSSR = false;
接著使用 for in 迴圈遍歷 computed; 程式碼:for (const key in computed) { const userDef = computed[key] };
接著判斷 userDef 該值是否是一個函式, 或者也可以是一個物件, 因此我們可以推斷我們的 computed 可以如下編寫程式碼:
computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } }
或如下初始化程式碼也是可以的:
computed: { reversedMsg: { get() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } } }
當我們拿不到我們的getter的時候, vue會報出一個警告資訊。
接著程式碼, 如下所示:
if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) }
如上程式碼, 我們會根據computed中的key來例項化watcher,因此我們可以理解為其實computed就是watcher的實現, 通過一個釋出訂閱模式來監聽的。給Watch方法傳遞了四個引數, 分別為VM實列, 上面我們獲取到的getter方法, noop 是一個回撥函式。computedWatcherOptions引數我們在原始碼初始化該值為:const computedWatcherOptions = { lazy: true }; 我們再來看下 Watcher函式程式碼, 該函式程式碼在:
vue/src/core/observer/watcher.js 中; 基本原始碼如下:
/* vm = { $attrs: {}, $children: [], $listeners: {}, $options: { components: {}, computed: { reversedMsg() { // this 指向 vm 例項 return this.msg.split('').reverse().join('') } }, el: '#app', ..... 更多屬性值 }, .... 更多屬性 }; expOrFn = function reversedMsg() {}; expOrFn 是我們上面獲取到的getter函式. cb的值是一個回撥函式。 options = {lazy: true}; isRenderWatcher = undefined; */ export default class Watcher { .... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } /* 當前的watcher新增到vue的實列上, 因此: vm._watchers = [ Watcher ]; 即 vm._watchers[0].vm = { $attrs: {}, $children: [], $listeners: {}, $options: { components: {}, computed: { reversedMsg() {} } } } .... */ vm._watchers.push(this); // options /* options = {lazy: true}; 因此: // 如果deep為true的話,會對getter返回的物件再做一次深度的遍歷 this.deep = !!options.deep; 即 this.deep = false; // user 是用於標記這個監聽是否由使用者通過$watch呼叫的 this.user = !!options.user; 即: this.user = false; // lazy用於標記watcher是否為懶執行,該屬性是給 computed data 用的,當 data 中的值更改的時候,不會立即計算 getter // 獲取新的數值,而是給該 watcher 標記為dirty,當該 computed data 被引用的時候才會執行從而返回新的 computed // data,從而減少計算量。 this.lazy = !!options.lazy; 即: this.lazy = true; // 表示當 data 中的值更改的時候,watcher 是否同步更新資料,如果是 true,就會立即更新數值,否則在 nextTick 中更新。 this.sync = !!options.sync; 即: this.sync = false; this.before = options.before; 即: this.before = undefined; */ if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } // cb 為回撥函式 this.cb = cb this.id = ++uid // uid for batching this.active = true // this.dirty = true; this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set(); /* 把函式轉換成字串的形式(不是正式環境下) this.expression = "reversedMsg() { return this.msg.split('').reverse().join('') }" */ this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter /* 判斷expOrFn是否是一個函式, 如果是一個函式, 直接賦值給 this.getter; 否則的話, 它是一個表示式的話, 比如 'a.b.c' 這樣的,因此呼叫 this.getter = parsePath(expOrFn); parsePath函式的程式碼在:vue/src/core/util/lang.js 中。 */ if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } // 不是懶載入型別呼叫get this.value = this.lazy ? undefined : this.get() } }
因此如上程式碼執行完成後, 我們的 vue/src/core/instance/state.js 中的 initComputed() 函式中,如下這句程式碼執行後:
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions );
watchers["reversedMsg"] 的值變為如下:
watchers["reversedMsg"] = { active: true, before: false, cb: f noop(a, b, c) {}, deep: false, depIds: Set, deps: [], dirty: true, expression: 'reversedMsg() { return this.msg.split('').reverse().join('') }', getter: f reversedMsg() { return this.msg.split('').reverse().join('') }, id: 1, lazy: true, newDepIds: Set, newDeps: [], sync: false, user: false, value: undefined, vm: { // Vue的實列物件 } };
如果computed中有更多的方法的話, 就會返回更多的 watchers['xxxx'] 這樣的物件了。
現在我們再回到 vue/src/core/instance/state.js 中的 initComputed() 函式中,繼續執行如下程式碼:
// component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 如果 computed中的key沒有在vm中, 則通過defineComputed掛載上去。第一次執行的時候, vm中沒有該屬性的 if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { // 如果我們的 computed中的key在data中或在props有同名的屬性的話,則直接發出警告。 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函式程式碼如下:
export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.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) }
如上程式碼, 首先執行 const shouldCache = !isServerRendering(); 判斷是不是伺服器端渲染, 我們這邊肯定不是的, 因此 shouldCache 為 true, 該引數的作用是否需要被快取資料, 為true是需要被快取的。也就是說我們的這裡的computed只要不是伺服器端渲染的話, 預設會快取資料的。
接著會判斷 userDef 是否是一個函式, 如果是函式的話,說明是我們的computed的用法。因此 sharedPropertyDefinition.get = createComputedGetter(key); 的返回值。如果不是函式, 有可能就是表示式, 比如 watch 中的監聽 'a.b.c' 這樣的話, 就執行else語句程式碼了。
現在我們來看下 createComputedGetter 函式程式碼如下:
/* @param key = "reversedMsg" */ function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
因此 sharedPropertyDefinition.get,其實返回的是 computedGetter()函式的,即: function computedGetter() {};
最後我們再回到 export function defineComputed() 函式程式碼中:執行程式碼:Object.defineProperty(target, key, sharedPropertyDefinition); 使用Object.defineProperty來監聽物件屬性值的變化;
/* @param {target} vm實列物件 @param {key} "reversedMsg" @param {sharedPropertyDefinition} sharedPropertyDefinition = { configurable: true, enumerable: true, get: function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } }, set: function noop(a, b, c) {} } */ Object.defineProperty(target, key, sharedPropertyDefinition);
如上程式碼我們可以看到, 我們會使用 Object.defineProperty來監聽Vue實列上的 reversedMsg 屬性. 然後會執行sharedPropertyDefinition中的get或set函式的。因此只要我們的data物件中的某個屬性發生改變的話, 我們的reversedMsg方法中依賴了該屬性的話, 也會呼叫sharedPropertyDefinition方法中的get/set方法的。
但是在我們的頁面第一次初始化的時候, 我們要如何初始化執行 computed中的對應方法呢?
因此我們現在需要再回到 vue/src/core/instance/init.js 中的_init()方法中,接著需要看下面的程式碼; 如下程式碼:
Vue.prototype._init = function (options?: Object) { ...... 更多的程式碼已省略 /* vm = { $attrs: {}, $children: [], $listeners: {}, $options: { components: {}, computed: { reversedMsg: f reversedMsg(){} }, data: function mergedInstanceDataFn () { ..... }, el: '#app', ..... 更多引數 } }; */ if (vm.$options.el) { vm.$mount(vm.$options.el) } ...... 更多的程式碼已省略 }
因此執行 vm.$mount(vm.$options.el); 這句程式碼了; 該程式碼的作用是對我們的頁面中的模板進行編譯操作。
該程式碼在 vue/src/platforms/web/entry-runtime-with-compiler.js 中。具體的內部程式碼我們先不看, 在下一個章節中我們會有講解該內部程式碼的。我們只需要看該js中的最後一句程式碼即可, 如下程式碼:
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component{ ..... 省略很多很多程式碼 return mount.call(this, el, hydrating); }
最後一句程式碼, 會呼叫 mount.call(this, el, hydrating); 這句程式碼; 因此會找到 vue/src/platforms/web/runtime/index.js 中的程式碼:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
接著執行程式碼 mountComponent(this, el, hydrating); 會找到 vue/src/core/instance/lifecycle.js 中程式碼
export function mountComponent() { ..... 省略很多程式碼 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) .... 省略很多程式碼 }
在這裡我們就可以看到, 我們對Watcher進行實列化了, new Watcher(); 因此我們又回到了vue/src/core/observer/watcher.js 中對程式碼進行初始化;
export default class Watcher { ..... 省略很多程式碼 constructor() { .... 省略很多程式碼 this.value = this.lazy ? undefined : this.get(); } }
此時this.lazy = false; 因此會執行 this.get()函式, 該函式程式碼如下:
get () { pushTarget(this) let value const vm = this.vm try { 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(vm, vm)方法; 最後就執行到 vue/src/core/instance/state.js中如下程式碼:
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
因此最後就返回 watcher.value 值了, 就是我們的computed的reversedMsg返回的值了。如上就是整個computed執行的過程,它最主要也是通過事件的釋出-訂閱模式來監聽物件資料的變化實現的。如上只是簡單的理解下原始碼如何做到的, 等稍後會有章節 講解 new Vue({}) 實列話,到底做了那些事情, 我們會深入講解到的。
對於methods及watcher也是一樣的,後續會更深入的講解到。