Преглед на файлове

ensure user watchers are triggered after directive updates

Evan You преди 11 години
родител
ревизия
714c1a1e4e
променени са 3 файла, в които са добавени 76 реда и са изтрити 18 реда
  1. 2 1
      src/api/data.js
  2. 45 17
      src/batcher.js
  3. 29 0
      test/unit/specs/batcher_spec.js

+ 2 - 1
src/api/data.js

@@ -78,7 +78,8 @@ exports.$watch = function (exp, cb, deep, immediate) {
   if (!watcher) {
     watcher = vm._userWatchers[key] =
       new Watcher(vm, exp, wrappedCb, {
-        deep: deep
+        deep: deep,
+        user: true
       })
   } else {
     watcher.addCb(wrappedCb)

+ 45 - 17
src/batcher.js

@@ -11,6 +11,24 @@ function Batcher () {
 
 var p = Batcher.prototype
 
+/**
+ * Reset the batcher's state.
+ */
+
+p.reset = function () {
+  this.has = {}
+  // we have two separate queues: one for directive updates
+  // and one for user watcher registered via $watch().
+  // we want to guarantee directive updates to be called
+  // before user watchers so that when user watchers are
+  // triggered, the DOM would have already been in updated
+  // state.
+  this.queue = []
+  this.userQueue = []
+  this.waiting = false
+  this.flushing = false
+}
+
 /**
  * Push a job into the job queue.
  * Jobs with duplicate IDs will be skipped unless it's
@@ -24,7 +42,18 @@ var p = Batcher.prototype
 
 p.push = function (job) {
   if (!job.id || !this.has[job.id] || this.flushing) {
-    this.queue.push(job)
+    // A user watcher callback could trigger another
+    // directive update during the flushing; at that time
+    // the directive queue would already have been run, so
+    // we call that update immediately as it is pushed.
+    if (this.flushing && !job.user) {
+      job.run()
+      return
+    }
+    var queue = job.user
+      ? this.userQueue
+      : this.queue
+    queue.push(job)
     this.has[job.id] = job
     if (!this.waiting) {
       this.waiting = true
@@ -34,32 +63,31 @@ p.push = function (job) {
 }
 
 /**
- * Flush the queue and run the jobs.
- * Will call a preFlush hook if has one.
+ * Flush both queues and run the jobs.
  */
 
 p.flush = function () {
   this.flushing = true
-  // do not cache length because more jobs might be pushed
-  // as we run existing jobs
-  for (var i = 0; i < this.queue.length; i++) {
-    var job = this.queue[i]
-    if (!job.cancelled) {
-      job.run()
-    }
-  }
+  this.run(this.queue)
+  this.run(this.userQueue)
   this.reset()
 }
 
 /**
- * Reset the batcher's state.
+ * Run the jobs in a single queue.
+ *
+ * @param {Array} queue
  */
 
-p.reset = function () {
-  this.has = {}
-  this.queue = []
-  this.waiting = false
-  this.flushing = false
+p.run = function (queue) {
+  // do not cache length because more jobs might be pushed
+  // as we run existing jobs
+  for (var i = 0; i < queue.length; i++) {
+    var job = queue[i]
+    if (!job.cancelled) {
+      job.run()
+    }
+  }
 }
 
 module.exports = Batcher

+ 29 - 0
test/unit/specs/batcher_spec.js

@@ -52,4 +52,33 @@ describe('Batcher', function () {
     })
   })
 
+  it('calls user watchers after directive updates', function (done) {
+    var vals = []
+    function run () {
+      vals.push(this.id)
+    }
+    batcher.push({
+      id: 2,
+      user: true,
+      run: function () {
+        run.call(this)
+        // user watcher triggering another directive update!
+        batcher.push({
+          id: 3,
+          run: run
+        })
+      }
+    })
+    batcher.push({
+      id: 1,
+      run: run
+    })
+    nextTick(function () {
+      expect(vals[0]).toBe(1)
+      expect(vals[1]).toBe(2)
+      expect(vals[2]).toBe(3)
+      done()
+    })
+  })
+
 })