Ver código fonte

batcher rewrite - batch vm.$watch callbacks

Evan You 12 anos atrás
pai
commit
c8533b8c01
6 arquivos alterados com 147 adições e 49 exclusões
  1. 37 22
      src/batcher.js
  2. 15 4
      src/binding.js
  3. 1 0
      src/compiler.js
  4. 17 5
      src/viewmodel.js
  5. 54 18
      test/unit/specs/batcher.js
  6. 23 0
      test/unit/specs/viewmodel.js

+ 37 - 22
src/batcher.js

@@ -1,31 +1,46 @@
-var utils = require('./utils'),
-    queue, has, waiting
+var utils = require('./utils')
 
-reset()
+function Batcher () {
+    this.reset()
+}
+
+var BatcherProto = Batcher.prototype
 
-exports.queue = function (binding) {
-    if (!has[binding.id]) {
-        queue.push(binding)
-        has[binding.id] = true
-        if (!waiting) {
-            waiting = true
-            utils.nextTick(flush)
+BatcherProto.push = function (job) {
+    if (!this.has[job.id]) {
+        this.queue.push(job)
+        this.has[job.id] = job
+        if (!this.waiting) {
+            this.waiting = true
+            utils.nextTick(utils.bind(this.flush, this))
         }
+    } else if (job.override) {
+        var oldJob = this.has[job.id]
+        oldJob.cancelled = true
+        this.queue.push(job)
+        this.has[job.id] = job
     }
 }
 
-function flush () {
-    for (var i = 0; i < queue.length; i++) {
-        var b = queue[i]
-        if (b.unbound) continue
-        b._update()
-        has[b.id] = false
+BatcherProto.flush = function () {
+    // before flush hook
+    if (this._preFlush) this._preFlush()
+    // do not cache length because more jobs might be pushed
+    // as we execute existing jobs
+    for (var i = 0; i < this.queue.length; i++) {
+        var job = this.queue[i]
+        if (job.cancelled) continue
+        if (job.execute() !== false) {
+            this.has[job.id] = false
+        }
     }
-    reset()
+    this.reset()
+}
+
+BatcherProto.reset = function () {
+    this.has = utils.hash()
+    this.queue = []
+    this.waiting = false
 }
 
-function reset () {
-    queue = []
-    has = utils.hash()
-    waiting = false
-}
+module.exports = Batcher

+ 15 - 4
src/binding.js

@@ -1,5 +1,6 @@
-var batcher = require('./batcher'),
-    id = 0
+var Batcher        = require('./batcher'),
+    bindingBatcher = new Batcher(),
+    bindingId      = 0
 
 /**
  *  Binding class.
@@ -9,7 +10,7 @@ var batcher = require('./batcher'),
  *  and multiple computed property dependents
  */
 function Binding (compiler, key, isExp, isFn) {
-    this.id = id++
+    this.id = bindingId++
     this.value = undefined
     this.isExp = !!isExp
     this.isFn = isFn
@@ -32,7 +33,17 @@ BindingProto.update = function (value) {
         this.value = value
     }
     if (this.dirs.length || this.subs.length) {
-        batcher.queue(this)
+        var self = this
+        bindingBatcher.push({
+            id: this.id,
+            execute: function () {
+                if (!self.unbound) {
+                    self._update()
+                } else {
+                    return false
+                }
+            }
+        })
     }
 }
 

+ 1 - 0
src/compiler.js

@@ -197,6 +197,7 @@ CompilerProto.setupObserver = function () {
     // a hash to hold event proxies for each root level key
     // so they can be referenced and removed later
     observer.proxies = makeHash()
+    observer._ctx = compiler.vm
 
     // add own listeners which trigger binding updates
     observer

+ 17 - 5
src/viewmodel.js

@@ -1,8 +1,14 @@
 var Compiler   = require('./compiler'),
     utils      = require('./utils'),
     transition = require('./transition'),
+    Batcher    = require('./batcher'),
+    slice      = [].slice,
     def        = utils.defProtected,
-    nextTick   = utils.nextTick
+    nextTick   = utils.nextTick,
+
+    // batch $watch callbacks
+    watcherBatcher = new Batcher(),
+    watcherId      = 0
 
 /**
  *  ViewModel exposed to the user that holds data,
@@ -36,11 +42,17 @@ def(VMProto, '$set', function (key, value) {
  *  fire callback with new value
  */
 def(VMProto, '$watch', function (key, callback) {
-    var self = this
+    // save a unique id for each watcher
+    var id = watcherId++,
+        self = this
     function on () {
-        var args = arguments
-        utils.nextTick(function () {
-            callback.apply(self, args)
+        var args = slice.call(arguments)
+        watcherBatcher.push({
+            id: id,
+            override: true,
+            execute: function () {
+                callback.apply(self, args)
+            }
         })
     }
     callback._fn = on

+ 54 - 18
test/unit/specs/batcher.js

@@ -1,13 +1,14 @@
 describe('Batcher', function () {
 
-    var batcher = require('vue/src/batcher'),
+    var Batcher = require('vue/src/batcher'),
+        batcher = new Batcher(),
         nextTick = require('vue/src/utils').nextTick
 
     var updateCount = 0
-    function mockBinding (id, middleware) {
+    function mockJob (id, middleware) {
         return {
             id: id,
-            _update: function () {
+            execute: function () {
                 updateCount++
                 this.updated = true
                 if (middleware) middleware()
@@ -15,13 +16,13 @@ describe('Batcher', function () {
         }
     }
     
-    it('should queue bindings to be updated on nextTick', function (done) {
+    it('should push bindings to be updated on nextTick', function (done) {
         
         updateCount = 0
-        var b1 = mockBinding(1),
-            b2 = mockBinding(2)
-        batcher.queue(b1)
-        batcher.queue(b2)
+        var b1 = mockJob(1),
+            b2 = mockJob(2)
+        batcher.push(b1)
+        batcher.push(b2)
         assert.strictEqual(updateCount, 0)
         assert.notOk(b1.updated)
         assert.notOk(b2.updated)
@@ -35,13 +36,13 @@ describe('Batcher', function () {
 
     })
 
-    it('should not queue dupicate bindings', function (done) {
+    it('should not push dupicate bindings', function (done) {
         
         updateCount = 0
-        var b1 = mockBinding(1),
-            b2 = mockBinding(1)
-        batcher.queue(b1)
-        batcher.queue(b2)
+        var b1 = mockJob(1),
+            b2 = mockJob(1)
+        batcher.push(b1)
+        batcher.push(b2)
 
         nextTick(function () {
             assert.strictEqual(updateCount, 1)
@@ -52,14 +53,14 @@ describe('Batcher', function () {
 
     })
 
-    it('should queue dependency bidnings triggered during flush', function (done) {
+    it('should push dependency bidnings triggered during flush', function (done) {
         
         updateCount = 0
-        var b1 = mockBinding(1),
-            b2 = mockBinding(2, function () {
-                batcher.queue(b1)
+        var b1 = mockJob(1),
+            b2 = mockJob(2, function () {
+                batcher.push(b1)
             })
-        batcher.queue(b2)
+        batcher.push(b2)
 
         nextTick(function () {
             assert.strictEqual(updateCount, 2)
@@ -70,4 +71,39 @@ describe('Batcher', function () {
 
     })
 
+    it('should allow overriding jobs with same ID', function (done) {
+        
+        updateCount = 0
+        var b1 = mockJob(1),
+            b2 = mockJob(1)
+
+        b2.override = true
+        batcher.push(b1)
+        batcher.push(b2)
+
+        nextTick(function () {
+            assert.strictEqual(updateCount, 1)
+            assert.ok(b1.cancelled)
+            assert.notOk(b1.updated)
+            assert.ok(b2.updated)
+            done()
+        })
+
+    })
+
+    it('should execute the _preFlush hook', function (done) {
+        
+        var executed = false
+        batcher._preFlush = function () {
+            executed = true
+        }
+        batcher.push(mockJob(1))
+        
+        nextTick(function () {
+            assert.ok(executed)
+            done()
+        })
+
+    })
+
 })

+ 23 - 0
test/unit/specs/viewmodel.js

@@ -84,6 +84,29 @@ describe('UNIT: ViewModel', function () {
             })
         })
 
+        it('should batch mutiple changes in a single event loop', function (done) {
+            var callbackCount = 0,
+                gotVal,
+                finalValue =  { b: { c: 3} },
+                vm = new Vue({
+                    data: {
+                        a: { b: { c: 0 }}
+                    }
+                })
+            vm.$watch('a', function (newVal) {
+                callbackCount++
+                gotVal = newVal
+            })
+            vm.a.b.c = 1
+            vm.a.b = { c: 2 }
+            vm.a = finalValue
+            nextTick(function () {
+                assert.strictEqual(callbackCount, 1)
+                assert.strictEqual(gotVal, finalValue)
+                done()
+            })
+        })
+
     })
 
     describe('.$unwatch()', function () {