閱讀目錄javascript
一. 理解Vue中的computed用法html
computed是計算屬性的; 它會根據所依賴的數據動態顯示新的計算結果, 該計算結果會被緩存起來。computed的值在getter執行後是會被緩存的。若是所依賴的數據發生改變時候, 就會從新調用getter來計算最新的結果。vue
下面咱們根據官網中的demo來理解下computed的使用及什麼時候使用computed。java
computed設計的初衷是爲了使模板中的邏輯運算更簡單, 好比在Vue模板中有不少複雜的數據計算的話, 咱們能夠把該計算邏輯放入到computed中去計算。nginx
下面咱們看下官網中的一個demo以下:web
<!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的目的就是能使模板中的運算邏輯更簡單。所以咱們如今須要把上面的代碼改寫成下面以下代碼:express
<!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 的值也會發生改變,以下控制檯打印的信息可知:app
如上打印的信息咱們能夠看獲得, 咱們的 vm.reversedMsg 的值依賴於 vm.msg 的值,當vm.msg的值發生改變時, vm.reversedMsg 的值也會獲得更新。
computed 應用場景
1. 適用於一些重複使用數據或複雜及費時的運算。咱們能夠把它放入computed中進行計算, 而後會在computed中緩存起來, 下次就能夠直接獲取了。
2. 若是咱們須要的數據依賴於其餘的數據的話, 咱們能夠把該數據設計爲computed中。
二:computed 和 methods的區別?
<!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也是同樣的,後續會更深刻的講解到。