Преглед изворни кода

move computed properties definition to component prototype when possible

Evan You пре 9 година
родитељ
комит
406352baba

+ 1 - 0
flow/component.js

@@ -52,6 +52,7 @@ declare interface Component {
   _renderContext: ?Component;
   _watcher: Watcher;
   _watchers: Array<Watcher>;
+  _computedWatchers: { [key: string]: Watcher };
   _data: Object;
   _props: Object;
   _events: Object;

+ 18 - 0
src/core/global-api/extend.js

@@ -2,6 +2,7 @@
 
 import config from '../config'
 import { warn, mergeOptions } from '../util/index'
+import { defineComputed } from '../instance/state'
 
 export function initExtend (Vue: GlobalAPI) {
   /**
@@ -23,6 +24,7 @@ export function initExtend (Vue: GlobalAPI) {
     if (cachedCtors[SuperId]) {
       return cachedCtors[SuperId]
     }
+
     const name = extendOptions.name || Super.options.name
     if (process.env.NODE_ENV !== 'production') {
       if (!/^[a-zA-Z][\w-]*$/.test(name)) {
@@ -33,6 +35,7 @@ export function initExtend (Vue: GlobalAPI) {
         )
       }
     }
+
     const Sub = function VueComponent (options) {
       this._init(options)
     }
@@ -44,10 +47,16 @@ export function initExtend (Vue: GlobalAPI) {
       extendOptions
     )
     Sub['super'] = Super
+
+    if (Sub.options.computed) {
+      initComputed(Sub)
+    }
+
     // allow further extension/mixin/plugin usage
     Sub.extend = Super.extend
     Sub.mixin = Super.mixin
     Sub.use = Super.use
+
     // create asset registers, so extended classes
     // can have their private assets too.
     config._assetTypes.forEach(function (type) {
@@ -57,13 +66,22 @@ export function initExtend (Vue: GlobalAPI) {
     if (name) {
       Sub.options.components[name] = Sub
     }
+
     // keep a reference to the super options at extension time.
     // later at instantiation we can check if Super's options have
     // been updated.
     Sub.superOptions = Super.options
     Sub.extendOptions = extendOptions
+
     // cache constructor
     cachedCtors[SuperId] = Sub
     return Sub
   }
 }
+
+function initComputed (Comp) {
+  const computed = Comp.options.computed
+  for (const key in computed) {
+    defineComputed(Comp.prototype, key, computed[key])
+  }
+}

+ 45 - 36
src/core/instance/state.js

@@ -1,7 +1,7 @@
 /* @flow */
 
-import Watcher from '../observer/watcher'
 import Dep from '../observer/dep'
+import Watcher from '../observer/watcher'
 
 import {
   set,
@@ -108,6 +108,26 @@ function initData (vm: Component) {
   observe(data, true /* asRootData */)
 }
 
+const computedWatcherOptions = { lazy: true }
+
+function initComputed (vm: Component, computed: Object) {
+  const watchers = vm._computedWatchers = Object.create(null)
+
+  for (const key in computed) {
+    const userDef = computed[key]
+    const getter = typeof userDef === 'function' ? userDef : userDef.get
+    // 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 on-the-fly computed
+    // properties here.
+    if (!(key in vm)) {
+      defineComputed(vm, key, userDef)
+    }
+  }
+}
+
 const computedSharedDefinition = {
   enumerable: true,
   configurable: true,
@@ -115,46 +135,35 @@ const computedSharedDefinition = {
   set: noop
 }
 
-function initComputed (vm: Component, computed: Object) {
-  for (const key in computed) {
-    /* istanbul ignore if */
-    if (process.env.NODE_ENV !== 'production' && key in vm) {
-      warn(
-        `existing instance property "${key}" will be ` +
-        `overwritten by a computed property with the same name.`,
-        vm
-      )
-    }
-    const userDef = computed[key]
-    if (typeof userDef === 'function') {
-      computedSharedDefinition.get = makeComputedGetter(userDef, vm)
-      computedSharedDefinition.set = noop
-    } else {
-      computedSharedDefinition.get = userDef.get
-        ? userDef.cache !== false
-          ? makeComputedGetter(userDef.get, vm)
-          : bind(userDef.get, vm)
-        : noop
-      computedSharedDefinition.set = userDef.set
-        ? bind(userDef.set, vm)
-        : noop
-    }
-    Object.defineProperty(vm, key, computedSharedDefinition)
+export function defineComputed (target: any, key: string, userDef: Object | Function) {
+  if (typeof userDef === 'function') {
+    computedSharedDefinition.get = createComputedGetter(key)
+    computedSharedDefinition.set = noop
+  } else {
+    computedSharedDefinition.get = userDef.get
+      ? userDef.cache !== false
+        ? createComputedGetter(key)
+        : userDef.get
+      : noop
+    computedSharedDefinition.set = userDef.set
+      ? userDef.set
+      : noop
   }
+  Object.defineProperty(target, key, computedSharedDefinition)
 }
 
-function makeComputedGetter (getter: Function, owner: Component): Function {
-  const watcher = new Watcher(owner, getter, noop, {
-    lazy: true
-  })
+function createComputedGetter (key) {
   return function computedGetter () {
-    if (watcher.dirty) {
-      watcher.evaluate()
-    }
-    if (Dep.target) {
-      watcher.depend()
+    const watcher = this._computedWatchers && this._computedWatchers[key]
+    if (watcher) {
+      if (watcher.dirty) {
+        watcher.evaluate()
+      }
+      if (Dep.target) {
+        watcher.depend()
+      }
+      return watcher.value
     }
-    return watcher.value
   }
 }
 

+ 33 - 0
test/unit/features/options/computed.spec.js

@@ -107,4 +107,37 @@ describe('Options computed', () => {
     vm.b
     expect(spy.calls.count()).toBe(2)
   })
+
+  it('as component', done => {
+    const Comp = Vue.extend({
+      template: `<div>{{ b }} {{ c }}</div>`,
+      data () {
+        return { a: 1 }
+      },
+      computed: {
+        // defined on prototype
+        b () {
+          return this.a + 1
+        }
+      }
+    })
+
+    const vm = new Comp({
+      computed: {
+        // defined at instantiation
+        c () {
+          return this.b + 1
+        }
+      }
+    }).$mount()
+    expect(vm.b).toBe(2)
+    expect(vm.c).toBe(3)
+    expect(vm.$el.textContent).toBe('2 3')
+    vm.a = 2
+    expect(vm.b).toBe(3)
+    expect(vm.c).toBe(4)
+    waitForUpdate(() => {
+      expect(vm.$el.textContent).toBe('3 4')
+    }).then(done)
+  })
 })