VueJs计算属性computed 原理解析


一、 什么是computed

定义: 当其依赖的属性的值发生变化的时,计算属性会重新计算。反之则使用缓存中的属性值, 其设计的目的就是为了解决模板中放入太多的逻辑而导致模板过重且难以维护的问题。例如:

<code>

Original message: "{{ message }}"

Computed reversed message: "{{ reversedMessage }}"

var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // `this` 指向 vm 实例 return this.message.split('').reverse().join('') } }})/<code>

你可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 vm.reversedMessage 依赖于 vm.message,因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。

二、 源码解析

先看computed的源码:

<code>function initComputed (vm, computed) { var watchers = vm._computedWatchers = Object.create(null); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; { if (getter === undefined) { warn( ("No getter function has been defined for computed property \"" + key + "\"."), vm ); getter = noop; } } // create internal watcher for the computed property. watchers[key] = new Watcher(vm, getter, 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); } } function defineComputed (target, key, userDef) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key); sharedPropertyDefinition.set = noop; } else { sharedPropertyDefinition.get = userDef.get ? userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop; sharedPropertyDefinition.set = userDef.set ? userDef.set : noop; } Object.defineProperty(target, key, sharedPropertyDefinition);}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 } }}/<code>

从源码中可以看出:

在initComputed中,computed在执行初始化的时候便对组件中定义的计算属性进行遍历,并且对其添加watcher监听并通过执行defineComputed(vm, key, userDef)定义相应的get和set。这时我们就可以发现其实computed的本质就是通过watcher来实现的。

<code>watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions); 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); } }/<code>

在new Watcher这个函数中有几个变量,其中vm是实例、getter和noop都是回掉函,computedWatcherOptions是相关配置。接下来的一段则是判断我们代码中定义的key是否与data、props中定义的变量相冲突。如果没有定义过便调用defineComputed,反之报警。


再看defineComputed,这逻辑比较简单,使用Object.defineProperty 在实例上定义computed 属性,从而使得定义的计算属性可以直接访问。


最后一个createComputedGetter,我们看下watcher.evaluate这个函数实现

<code>Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false;};//顺藤摸瓜 再看get是怎么实现的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};/<code>

这时我们不难发现,computed的计算结果是通过Watcher.prototype.get来得到的,当计算属性中依赖的数据变化的时候就会触发set方法,通知更新,update中会把this.dirty设置为true,当再次调用computed的时候就会重新计算返回新的值.


总结:我们在组件中使用computed计算属性时,当组件初始化的时候系统变会对为个定义的key都创建了对应的watcher, 并有一个特殊的参数lazy,然后调用了自己的getter方法,这样就收集了这个计算属性依赖的所有data,那么所依赖的data会收集这个订阅者同时会针对computed中的key添加属性描述符创建了独有的get方法,当调用计算属性的时候,这个get判断dirty是否为true,为真则表示要要重新计算,反之直接返回value。当依赖的data 变化的时候回触发数据的set方法调用update()通知更新,此时会把dirty设置成true,所以computed 就会重新计算这个值,从而达到动态计算的目的。