Prechádzať zdrojové kódy

lazy evaluation for computed properties

Evan You 10 rokov pred
rodič
commit
12b4ae2ad6

+ 18 - 2
src/instance/scope.js

@@ -2,6 +2,7 @@ var _ = require('../util')
 var compiler = require('../compiler')
 var Observer = require('../observer')
 var Dep = require('../observer/dep')
+var Watcher = require('../watcher')
 
 /**
  * Setup the scope of an instance, which contains:
@@ -191,11 +192,11 @@ exports._initComputed = function () {
         configurable: true
       }
       if (typeof userDef === 'function') {
-        def.get = _.bind(userDef, this)
+        def.get = makeComputedGetter(userDef, this)
         def.set = noop
       } else {
         def.get = userDef.get
-          ? _.bind(userDef.get, this)
+          ? makeComputedGetter(userDef.get, this)
           : noop
         def.set = userDef.set
           ? _.bind(userDef.set, this)
@@ -206,6 +207,21 @@ exports._initComputed = function () {
   }
 }
 
+function makeComputedGetter (getter, owner) {
+  var watcher = new Watcher(owner, getter, null, {
+    lazy: true
+  })
+  return function computedGetter () {
+    if (watcher.dirty) {
+      watcher.evaluate()
+    }
+    if (Dep.target) {
+      watcher.depend()
+    }
+    return watcher.value
+  }
+}
+
 /**
  * Setup instance methods. Methods must be bound to the
  * instance since they might be called by children

+ 0 - 10
src/observer/index.js

@@ -64,16 +64,6 @@ Observer.create = function (value, vm) {
   return ob
 }
 
-/**
- * Set the target watcher that is currently being evaluated.
- *
- * @param {Watcher} watcher
- */
-
-Observer.setTarget = function (watcher) {
-  Dep.target = watcher
-}
-
 // Instance methods
 
 var p = Observer.prototype

+ 40 - 7
src/watcher.js

@@ -1,6 +1,6 @@
 var _ = require('./util')
 var config = require('./config')
-var Observer = require('./observer')
+var Dep = require('./observer/dep')
 var expParser = require('./parsers/expression')
 var batcher = require('./batcher')
 var uid = 0
@@ -18,6 +18,7 @@ var uid = 0
  *                 - {Boolean} twoWay
  *                 - {Boolean} deep
  *                 - {Boolean} user
+ *                 - {Boolean} lazy
  *                 - {Function} [preProcess]
  * @constructor
  */
@@ -34,10 +35,12 @@ function Watcher (vm, expOrFn, cb, options) {
   this.deep = !!options.deep
   this.user = !!options.user
   this.twoWay = !!options.twoWay
+  this.lazy = !!options.lazy
+  this.dirty = this.lazy
   this.filters = options.filters
   this.preProcess = options.preProcess
   this.deps = []
-  this.newDeps = []
+  this.newDeps = null
   // parse expression for getter/setter
   if (isFn) {
     this.getter = expOrFn
@@ -47,7 +50,9 @@ function Watcher (vm, expOrFn, cb, options) {
     this.getter = res.get
     this.setter = res.set
   }
-  this.value = this.get()
+  this.value = this.lazy
+    ? undefined
+    : this.get()
   // state for avoiding false triggers for deep and Array
   // watchers during vm._digest()
   this.queued = this.shallow = false
@@ -147,7 +152,8 @@ p.set = function (value) {
  */
 
 p.beforeGet = function () {
-  Observer.setTarget(this)
+  Dep.target = this
+  this.newDeps = []
 }
 
 /**
@@ -155,7 +161,7 @@ p.beforeGet = function () {
  */
 
 p.afterGet = function () {
-  Observer.setTarget(null)
+  Dep.target = null
   var i = this.deps.length
   while (i--) {
     var dep = this.deps[i]
@@ -164,7 +170,7 @@ p.afterGet = function () {
     }
   }
   this.deps = this.newDeps
-  this.newDeps = []
+  this.newDeps = null
 }
 
 /**
@@ -175,7 +181,9 @@ p.afterGet = function () {
  */
 
 p.update = function (shallow) {
-  if (!config.async) {
+  if (this.lazy) {
+    this.dirty = true
+  } else if (!config.async) {
     this.run()
   } else {
     // if queued, only overwrite shallow with non-shallow,
@@ -214,6 +222,31 @@ p.run = function () {
   }
 }
 
+/**
+ * Evaluate the value of the watcher.
+ * This only gets called for lazy watchers.
+ */
+
+p.evaluate = function () {
+  // avoid overwriting another watcher that is being
+  // collected.
+  var current = Dep.target
+  this.value = this.get()
+  this.dirty = false
+  Dep.target = current
+}
+
+/**
+ * Depend on all deps collected by this watcher.
+ */
+
+p.depend = function () {
+  var i = this.deps.length
+  while (i--) {
+    this.deps[i].depend()
+  }
+}
+
 /**
  * Remove self from all dependencies' subcriber list.
  */

+ 22 - 5
test/unit/specs/instance/scope_spec.js

@@ -162,24 +162,26 @@ describe('Instance Scope', function () {
     var Test = Vue.extend({
       computed: {
         c: function () {
-          expect(this).toBe(vm)
           return this.a + this.b
         },
         d: {
           get: function () {
-            expect(this).toBe(vm)
             return this.a + this.b
           },
           set: function (newVal) {
-            expect(this).toBe(vm)
             var vals = newVal.split(' ')
             this.a = vals[0]
             this.b = vals[1]
           }
+        },
+        // chained computed
+        e: function () {
+          return this.c + 'e'
         }
       }
     })
 
+    var spy = jasmine.createSpy()
     var vm = new Test({
       data: {
         a: 'a',
@@ -187,21 +189,29 @@ describe('Instance Scope', function () {
       }
     })
 
+    vm.$watch('e', spy)
+
     it('get', function () {
       expect(vm.c).toBe('ab')
       expect(vm.d).toBe('ab')
+      expect(vm.e).toBe('abe')
     })
 
-    it('set', function () {
+    it('set', function (done) {
       vm.c = 123 // should do nothing
       vm.d = 'c d'
       expect(vm.a).toBe('c')
       expect(vm.b).toBe('d')
       expect(vm.c).toBe('cd')
       expect(vm.d).toBe('cd')
+      expect(vm.e).toBe('cde')
+      Vue.nextTick(function () {
+        expect(spy).toHaveBeenCalledWith('cde', 'abe')
+        done()
+      })
     })
 
-    it('inherit', function () {
+    it('inherit', function (done) {
       var child = vm.$addChild({
         inherit: true
       })
@@ -212,10 +222,16 @@ describe('Instance Scope', function () {
       expect(vm.b).toBe('f')
       expect(vm.c).toBe('ef')
       expect(vm.d).toBe('ef')
+      expect(vm.e).toBe('efe')
       expect(child.a).toBe('e')
       expect(child.b).toBe('f')
       expect(child.c).toBe('ef')
       expect(child.d).toBe('ef')
+      expect(vm.e).toBe('efe')
+      Vue.nextTick(function () {
+        expect(spy).toHaveBeenCalledWith('efe', 'cde')
+        done()
+      })
     })
 
     it('same definition object bound to different instance', function () {
@@ -232,6 +248,7 @@ describe('Instance Scope', function () {
       expect(vm.b).toBe('D')
       expect(vm.c).toBe('CD')
       expect(vm.d).toBe('CD')
+      expect(vm.e).toBe('CDe')
     })
 
   })

+ 5 - 4
test/unit/specs/observer/observer_spec.js

@@ -1,4 +1,5 @@
 var Observer = require('../../../../src/observer')
+var Dep = require('../../../../src/observer/dep')
 var config = require('../../../../src/config')
 var _ = require('../../../../src/util')
 
@@ -59,9 +60,9 @@ describe('Observer', function () {
       update: jasmine.createSpy()
     }
     // collect dep
-    Observer.setTarget(watcher)
+    Dep.target = watcher
     obj.a.b
-    Observer.setTarget(null)
+    Dep.target = null
     expect(watcher.deps.length).toBe(3) // obj.a + a.b + b
     obj.a.b = 3
     expect(watcher.update.calls.count()).toBe(1)
@@ -69,9 +70,9 @@ describe('Observer', function () {
     obj.a = { b: 4 }
     expect(watcher.update.calls.count()).toBe(2)
     watcher.deps = []
-    Observer.setTarget(watcher)
+    Dep.target = watcher
     obj.a.b
-    Observer.setTarget(null)
+    Dep.target = null
     expect(watcher.deps.length).toBe(3)
     // set on the swapped object
     obj.a.b = 5

+ 21 - 0
test/unit/specs/watcher_spec.js

@@ -329,6 +329,27 @@ describe('Watcher', function () {
     })
   })
 
+  it('lazy mode', function (done) {
+    var watcher = new Watcher(vm, function () {
+      return this.a + this.b.d
+    }, null, { lazy: true })
+    expect(watcher.lazy).toBe(true)
+    expect(watcher.value).toBeUndefined()
+    expect(watcher.dirty).toBe(true)
+    watcher.evaluate()
+    expect(watcher.value).toBe(5)
+    expect(watcher.dirty).toBe(false)
+    vm.a = 2
+    nextTick(function () {
+      expect(watcher.value).toBe(5)
+      expect(watcher.dirty).toBe(true)
+      watcher.evaluate()
+      expect(watcher.value).toBe(6)
+      expect(watcher.dirty).toBe(false)
+      done()
+    })
+  })
+
   it('teardown', function (done) {
     var watcher = new Watcher(vm, 'b.c', spy)
     watcher.teardown()