引言:express
在Vuejs中用watch來偵聽數據變化,computed用來監聽多個屬性的變化並返回計算值,那麼這兩個特性是如何實現的呢?下面講一下二者實現的具體方法以及一些使用經驗。函數
結論:oop
首先給一下結論,由上一篇Vue核心原理的介紹的數據綁定能夠了解到,若是想監聽某個屬性的數據變化,那麼只須要 new 一個 Watcher 並在 watcher 執行的時候用到那個屬性就夠了,使用的過程當中該 watcher 會被加入到對應數據的依賴中,數據變化的時候就會獲得通知該。那麼若是想要實現 Vue 的 watch 和 computed,實際上只須要爲對應的屬性創建 watcher,並構造出執行時使用數據的函數便可,接下來展開講一下。性能
1、watch實現原理:ui
借官網的例子用一下this
<div id="demo">{{ fullName }}</div>
var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' }, watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } } })
watch 這段的意思就是要監聽 firstName 和 lastName的變化,當數據變化時執行對應的函數,給fullName賦值。lua
根據上面的結論,若是監聽 firstName 的變化,那麼只須要在初始化的時候 new 一個 watcher,watcher 的回調裏面使用到 firstName 就能夠了,關鍵是如何構造回調函數,按照這個思路咱們看一下 Vue 的實現。spa
function initState (vm) { vm._watchers = []; var 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); } }
首先 initWatch(vm, opts.watch) ,注意這裏的順序,initwatch 是在 initData 以後執行的,由於 watch 也是在已有的響應式基礎上進行監聽,因此要先初始化數據。prototype
function initWatch (vm, watch) { for (var key in watch) { var handler = watch[key]; if (Array.isArray(handler)) { for (var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { createWatcher(vm, key, handler); //以firstName爲例,此處爲:createWatcher(vm, 'firstName', watch.firstName) } } } function createWatcher ( vm, expOrFn, handler, options ) { if (isPlainObject(handler)) { options = handler; handler = handler.handler; } if (typeof handler === 'string') { handler = vm[handler]; } return vm.$watch(expOrFn, handler, options) //以firstName爲例,此處爲:vm.$watch('firstName',watch.firstName, options)
}
以後調用 $watch 爲 watch 中監聽的每一個屬性創建 warcher ,watch構造函數中會構造函數並執行。代理
Vue.prototype.$watch = function ( expOrFn, cb, options ) { var vm = this; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {}; options.user = true; var watcher = new Watcher(vm, expOrFn, cb, options); //以firstName爲例,此處爲:new watcher('firstName',watch.firstName, undefined) if (options.immediate) { try { cb.call(vm, watcher.value); } catch (error) { handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\"")); } } return function unwatchFn () { watcher.teardown(); } }; }
watcher函數邏輯
var Watcher = function Watcher ( vm, expOrFn, // 'firstName' cb, //watch.firstName 函數 options, isRenderWatcher ) { this.vm = vm;if (isRenderWatcher) { vm._watcher = this; } vm._watchers.push(this); // options 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; } this.cb = cb; this.id = ++uid$2; // uid for batching this.active = true; this.dirty = this.lazy; // for lazy watchers this.deps = []; this.newDeps = []; this.depIds = new _Set(); this.newDepIds = new _Set(); this.expression = expOrFn.toString(); // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop; warn( "Failed watching path: \"" + expOrFn + "\" " + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ); } } this.value = this.lazy ? undefined : this.get(); }; /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var 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 };
能夠看到,new Watcher 時傳入的表達式是 ‘firstName’,非函數類型,Vue 調用 parsePath(expOrFn=‘firstName’) 構造使用 firstName 的一個 getter 函數,從而創建依賴,開啓監聽。
/** * Parse simple path. */ var bailRE = new RegExp(("[^" + unicodeLetters + ".$_\\d]")); function parsePath (path) { //path === 'firstName' if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; //首次循環 obj === vm ,即 vm.fisrtName } return obj } }
能夠看到 parsePath 返回的結果就是一個獲取屬性的函數,簡化下能夠寫成
this.getter = function (){ return vm.firstName; }
執行次函數就能夠將 ‘fisrtName ’ 與當前的 watcher 關聯起來,此時的 this.cb 即爲 watch 中傳入的 firstName 函數。據變化時會通知此 watcher 執行 this.cb。
this.cb = function (val) { this.fullName = val + ' ' + this.lastName }
以上就是 Vue watch 的實現原理,其核心就是如何爲偵聽的屬性構造一個 watcher 以及 watcher 的 getter 函數。
2、computed 實現原理:
一樣藉助官網的例子看下
<div id="example"> <p>Original message: "{{ message }}"</p> <p>Computed reversed message: "{{ reversedMessage }}"</p> </div> var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 計算屬性的 getter reversedMessage: function () { // `this` 指向 vm 實例 return this.message.split('').reverse().join('') } } })
一樣,想要監聽數據( 這裏是 'message' )的變化須要創建一個watcher,構造出使用 ' message' 的函數在watcher執行的過程當中進行使用。這裏很顯然新建watcher須要用到的就是 computed.reverseMessage 函數,不須要構造了。這裏須要考慮一個問題,reversedMessage 是一個新增屬性,vm上並未定義過響應式,因此此處確定須要藉助 Object.defineProperty 將 reverMessage 定義到 vm 上,看一下實現。
function initState (vm) { vm._watchers = []; var 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); } }
var computedWatcherOptions = { lazy: true };
function initComputed (vm, computed) { // $flow-disable-line var watchers = vm._computedWatchers = Object.create(null); // computed properties are just getters during SSR var isSSR = isServerRendering(); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; if (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, // computed.reverseMessage 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 (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); } } } }
初始化首先調用 initComputed(vm, opts.computed) ,在 initComputed 中會執行兩個步驟:
第一,爲每一個 key 創建一個 watcher ,watcher 的 getter 爲對應 key 的函數。
var computedWatcherOptions = { lazy: true }; --- watchers[key] = new Watcher( vm, getter || noop, // computed.reverseMessage noop, // 回調 空 function(){} computedWatcherOptions // lazy: true );
這裏須要注意的是 initComputed 中建立的 watcher 爲 lazy 模式 。
簡單說下,Vue 的 watcher 根據傳參不一樣能夠分爲兩種,一種是常規(當即執行)模式 ,一種是 lazy (懶執行) 模式:常規模式的 watcher 在初始化時會直接調用 getter ,getter 會獲取使用到的響應式數據,進而創建依賴關係;lazy 模式 watcher 中的 getter 不會當即執行,執行的時機是在獲取計算屬性時,稍後會講。
第2、經過 defineComputed 調用 Object.defineProperty 將 key 定義到 vm 上。
function defineComputed ( target, key, userDef ) { var 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 (sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( ("Computed property \"" + key + "\" was assigned to but it has no setter."), this ); }; } Object.defineProperty(target, key, sharedPropertyDefinition); //target === vm key == 'reverseMessage' }
能夠看到,計算屬性的get 是由 createComputedGetter 建立而成,那麼咱們看下 createComputedGetter 的返回值:
ps:經過 sharedPropertyDefinition 的構造過程能夠看到,若是傳入的計算屬性值爲函數,那麼至關於計算屬性的 get ,此時不容許 set,若是須要對計算屬性進行set,那麼須要自定義傳入 set、get 方法。
function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } }
createComputedGetter 返回了一個 computedGetter 函數,這個函數就是獲取計算屬性(reveserMessage)時的 get 函數,當獲取 reveserMessage 的時候會調用 watcher.evaluate() ,看一下watcher.evaluate 的邏輯:
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false; }; /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var 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 };
能夠看到 watcher 在 evaluate 中會直接調用 get 方法,get 方法會直接調用 getter 並返回獲取到的值,而這裏的 getter 就是前面新建 watcher 時早已傳入的計算屬性值,即 computed.reveseMessage 函數,執行getter過程當中就會創建起對數據的依賴。
這裏囉嗦兩個小問題:
一、計算屬性使用了懶執行模式,使用時纔會運算並創建依賴,多是考慮到兩方面:一個是計算屬性不必定會被使用,性能會被白白浪費;另外一個計算屬性中可能會存在比較複雜的運算邏輯,放在相對靠後的生命週期中比較合適。
二、計算屬性函數的傳入是由開發者自行傳入的,須要注意數據監聽開啓的條件是數據被使用過,在使用過程當中須要注意 if 條件語句的使用,最好把須要用到的數據都定義在最上層。
以上就是computed的實現原理。
總結:
本文主要講了 Vuejs 中 watch 和 computed 的實現原理,核心就是要建立 watcher 併爲 watcher 構造相應的 getter 函數,經過 getter 函數的執行進行綁定依賴。根據 getter 執行的時機不一樣 watcher 能夠分爲當即執行以及懶執行兩種模式,當即執行模式 getter 會在構造函數中直接執行,懶執行模式 getter 須要調用 evaluate 來執行。在使用的場景上 watch 適合直接監聽單個屬性,不涉及複雜的監聽邏輯場景,computed 適合涉及多個監聽變化的邏輯,另外 computed 比較適合作數據代理,當某些數據產生的過程比較複雜,不敢下手的時候直接一層 computed 代理就能夠完美解決。