ソースを参照

further improve scheduler

Evan You 9 年 前
コミット
2c6930adfc
2 ファイル変更67 行追加83 行削除
  1. 23 44
      src/core/observer/scheduler.js
  2. 44 39
      test/unit/modules/observer/scheduler.spec.js

+ 23 - 44
src/core/observer/scheduler.js

@@ -8,13 +8,7 @@ import {
   devtools
 } from '../util/index'
 
-// We have two separate queues: one for internal component re-render updates
-// and one for user watcher registered via $watch(). We want to guarantee
-// re-render updates to be called before user watchers so that when user
-// watchers are triggered, the DOM would already be in updated state.
-
 const queue: Array<Watcher> = []
-const userQueue: Array<Watcher> = []
 let has: { [key: number]: ?true } = {}
 let circular: { [key: number]: number } = {}
 let waiting = false
@@ -26,7 +20,6 @@ let index = 0
  */
 function resetSchedulerState () {
   queue.length = 0
-  userQueue.length = 0
   has = {}
   if (process.env.NODE_ENV !== 'production') {
     circular = {}
@@ -39,31 +32,17 @@ function resetSchedulerState () {
  */
 function flushSchedulerQueue () {
   flushing = true
-  runSchedulerQueue(userQueue)
-  runSchedulerQueue(queue.sort(queueSorter))
-  // devtool hook
-  /* istanbul ignore if */
-  if (devtools && config.devtools) {
-    devtools.emit('flush')
-  }
-  resetSchedulerState()
-}
 
-/**
- * Sort queue before flush.
- * This ensures components are updated from parent to child
- * so there will be no duplicate updates, e.g. a child was
- * pushed into the queue first and then its parent's props
- * changed.
- */
-function queueSorter (a: Watcher, b: Watcher) {
-  return a.id - b.id
-}
+  // Sort queue before flush.
+  // This ensures that:
+  // 1. Components are updated from parent to child. (because parent is always
+  //    created before the child)
+  // 2. A component's user watchers are run before its render watcher (because
+  //    user watchers are created before the render watcher)
+  // 3. If a component is destroyed during a parent component's watcher run,
+  //    its watchers can be skipped.
+  queue.sort((a, b) => a.id - b.id)
 
-/**
- * Run the watchers in a single queue.
- */
-function runSchedulerQueue (queue: Array<Watcher>) {
   // do not cache length because more watchers might be pushed
   // as we run existing watchers
   for (index = 0; index < queue.length; index++) {
@@ -87,7 +66,14 @@ function runSchedulerQueue (queue: Array<Watcher>) {
       }
     }
   }
-  queue.length = 0
+
+  // devtool hook
+  /* istanbul ignore if */
+  if (devtools && config.devtools) {
+    devtools.emit('flush')
+  }
+
+  resetSchedulerState()
 }
 
 /**
@@ -98,24 +84,17 @@ function runSchedulerQueue (queue: Array<Watcher>) {
 export function queueWatcher (watcher: Watcher) {
   const id = watcher.id
   if (has[id] == null) {
-    // if already flushing, and all user watchers have already been run,
-    // run the new user watcher immediately.
-    if (flushing && watcher.user && !userQueue.length) {
-      return watcher.run()
-    }
-    // push watcher into appropriate queue
     has[id] = true
-    const q = watcher.user
-      ? userQueue
-      : queue
     if (!flushing) {
-      q.push(watcher)
+      queue.push(watcher)
     } else {
-      let i = q.length - 1
-      while (i >= 0 && q[i].id > watcher.id) {
+      // if already flushing, splice the watcher based on its id
+      // if already past its id, it will be run next immediately.
+      let i = queue.length - 1
+      while (i >= 0 && queue[i].id > watcher.id) {
         i--
       }
-      q.splice(Math.max(i, index) + 1, 0, watcher)
+      queue.splice(Math.max(i, index) + 1, 0, watcher)
     }
     // queue the flush
     if (!waiting) {

+ 44 - 39
test/unit/modules/observer/scheduler.spec.js

@@ -1,3 +1,4 @@
+import Vue from 'vue'
 import config from 'core/config'
 import { queueWatcher } from 'core/observer/scheduler'
 
@@ -46,55 +47,59 @@ describe('Scheduler', () => {
   })
 
   it('call user watchers before component re-render', done => {
-    const vals = []
-    function run () {
-      vals.push(this.id)
-    }
-    queueWatcher({
-      id: 2,
-      user: true,
-      run () {
-        run.call(this)
-        // user watcher triggering another directive update!
-        queueWatcher({
-          id: 3,
-          run: run
-        })
+    const calls = []
+    const vm = new Vue({
+      data: {
+        a: 1
+      },
+      template: '<div>{{ a }}</div>',
+      watch: {
+        a () { calls.push(1) }
+      },
+      beforeUpdate () {
+        calls.push(2)
       }
-    })
-    queueWatcher({
-      id: 1,
-      run: run
-    })
+    }).$mount()
+    vm.a = 2
     waitForUpdate(() => {
-      expect(vals).toEqual([2, 1, 3])
+      expect(calls).toEqual([1, 2])
     }).then(done)
   })
 
   it('call user watcher triggered by component re-render immediately', done => {
     // this happens when a component re-render updates the props of a child
-    const vals = []
-    queueWatcher({
-      id: 1,
-      run () {
-        vals.push(1)
-        queueWatcher({
-          id: 3,
-          user: true,
-          run () {
-            vals.push(3)
+    const calls = []
+    const vm = new Vue({
+      data: {
+        a: 1
+      },
+      watch: {
+        a () {
+          calls.push(1)
+        }
+      },
+      beforeUpdate () {
+        calls.push(2)
+      },
+      template: '<div><test :a="a"></test></div>',
+      components: {
+        test: {
+          props: ['a'],
+          template: '<div>{{ a }}</div>',
+          watch: {
+            a () {
+              calls.push(3)
+            }
+          },
+          beforeUpdate () {
+            calls.push(4)
           }
-        })
+        }
       }
-    })
-    queueWatcher({
-      id: 2,
-      run () {
-        vals.push(2)
-      }
-    })
+    }).$mount()
+    vm.a = 2
     waitForUpdate(() => {
-      expect(vals).toEqual([1, 3, 2])
+      expect(calls).toEqual([1, 2, 3, 4])
     }).then(done)
   })