Evan You 12 лет назад
Родитель
Сommit
a847fddc2f
3 измененных файлов с 351 добавлено и 196 удалено
  1. 1 4
      src/compiler.js
  2. 2 0
      src/directive.js
  3. 348 192
      src/directives/repeat.js

+ 1 - 4
src/compiler.js

@@ -45,7 +45,6 @@ function Compiler (vm, options) {
     compiler.init       = true
     compiler.repeat     = false
     compiler.destroyed  = false
-    compiler.delayReady = false
 
     // process and extend options
     options = compiler.options = options || makeHash()
@@ -141,9 +140,7 @@ function Compiler (vm, options) {
     compiler.init = false
 
     // post compile / ready hook
-    if (!compiler.delayReady) {
-        compiler.execHook('ready')
-    }
+    compiler.execHook('ready')
 }
 
 var CompilerProto = Compiler.prototype

+ 2 - 0
src/directive.js

@@ -1,5 +1,6 @@
 var utils      = require('./utils'),
     directives = require('./directives'),
+    dirId      = 1,
 
     // Regexes!
 
@@ -22,6 +23,7 @@ var utils      = require('./utils'),
  */
 function Directive (dirname, definition, expression, rawKey, compiler, node) {
 
+    this.id             = dirId++
     this.name           = dirname
     this.compiler       = compiler
     this.vm             = compiler.vm

+ 348 - 192
src/directives/repeat.js

@@ -6,71 +6,73 @@ var Observer   = require('../observer'),
  *  Mathods that perform precise DOM manipulation
  *  based on mutator method triggered
  */
-var mutationHandlers = {
-
-    push: function (m) {
-        this.addItems(m.args, this.vms.length)
-    },
-
-    pop: function () {
-        var vm = this.vms.pop()
-        if (vm) this.removeItems([vm])
-    },
-
-    unshift: function (m) {
-        this.addItems(m.args)
-    },
-
-    shift: function () {
-        var vm = this.vms.shift()
-        if (vm) this.removeItems([vm])
-    },
-
-    splice: function (m) {
-        var index = m.args[0],
-            removed = m.args[1],
-            removedVMs = removed === undefined
-                ? this.vms.splice(index)
-                : this.vms.splice(index, removed)
-        this.removeItems(removedVMs)
-        this.addItems(m.args.slice(2), index)
-    },
-
-    sort: function () {
-        var vms = this.vms,
-            col = this.collection,
-            l = col.length,
-            sorted = new Array(l),
-            i, j, vm, data
-        for (i = 0; i < l; i++) {
-            data = col[i]
-            for (j = 0; j < l; j++) {
-                vm = vms[j]
-                if (vm.$data === data) {
-                    sorted[i] = vm
-                    break
-                }
-            }
-        }
-        for (i = 0; i < l; i++) {
-            this.container.insertBefore(sorted[i].$el, this.ref)
-        }
-        this.vms = sorted
-    },
-
-    reverse: function () {
-        var vms = this.vms
-        vms.reverse()
-        for (var i = 0, l = vms.length; i < l; i++) {
-            this.container.insertBefore(vms[i].$el, this.ref)
-        }
-    }
-}
+// var mutationHandlers = {
+
+//     push: function (m) {
+//         this.addItems(m.args, this.vms.length)
+//     },
+
+//     pop: function () {
+//         var vm = this.vms.pop()
+//         if (vm) this.removeItems([vm])
+//     },
+
+//     unshift: function (m) {
+//         this.addItems(m.args)
+//     },
+
+//     shift: function () {
+//         var vm = this.vms.shift()
+//         if (vm) this.removeItems([vm])
+//     },
+
+//     splice: function (m) {
+//         var index = m.args[0],
+//             removed = m.args[1],
+//             removedVMs = removed === undefined
+//                 ? this.vms.splice(index)
+//                 : this.vms.splice(index, removed)
+//         this.removeItems(removedVMs)
+//         this.addItems(m.args.slice(2), index)
+//     },
+
+//     sort: function () {
+//         var vms = this.vms,
+//             col = this.collection,
+//             l = col.length,
+//             sorted = new Array(l),
+//             i, j, vm, data
+//         for (i = 0; i < l; i++) {
+//             data = col[i]
+//             for (j = 0; j < l; j++) {
+//                 vm = vms[j]
+//                 if (vm.$data === data) {
+//                     sorted[i] = vm
+//                     break
+//                 }
+//             }
+//         }
+//         for (i = 0; i < l; i++) {
+//             this.container.insertBefore(sorted[i].$el, this.ref)
+//         }
+//         this.vms = sorted
+//     },
+
+//     reverse: function () {
+//         var vms = this.vms
+//         vms.reverse()
+//         for (var i = 0, l = vms.length; i < l; i++) {
+//             this.container.insertBefore(vms[i].$el, this.ref)
+//         }
+//     }
+// }
 
 module.exports = {
 
     bind: function () {
 
+        this.identifier = '$repeat' + this.id
+
         var el   = this.el,
             ctn  = this.container = el.parentNode
 
@@ -86,28 +88,28 @@ module.exports = {
         this.collection = null
         this.vms = null
 
-        var self = this
-        this.mutationListener = function (path, arr, mutation) {
-            if (self.lock) return
-            var method = mutation.method
-            mutationHandlers[method].call(self, mutation)
-            if (method !== 'push' && method !== 'pop') {
-                // update index
-                var i = arr.length
-                while (i--) {
-                    self.vms[i].$index = i
-                }
-            }
-        }
+        // var self = this
+        // this.mutationListener = function (path, arr, mutation) {
+        //     if (self.lock) return
+        //     var method = mutation.method
+        //     mutationHandlers[method].call(self, mutation)
+        //     if (method !== 'push' && method !== 'pop') {
+        //         // update index
+        //         var i = arr.length
+        //         while (i--) {
+        //             self.vms[i].$index = i
+        //         }
+        //     }
+        // }
 
     },
 
     update: function (collection) {
 
-        if (
-            collection === this.collection ||
-            collection === this.object
-        ) return
+        // if (
+        //     collection === this.collection ||
+        //     collection === this.object
+        // ) return
 
         if (utils.typeOf(collection) === 'Object') {
             collection = this.convertObject(collection)
@@ -123,13 +125,10 @@ module.exports = {
 
         // keep reference of old data and VMs
         // so we can reuse them if possible
-        var oldVMs = this.oldVMs = this.vms
+        this.oldVMs = this.vms
+        this.oldCollection = this.collection
 
         collection = this.collection = collection || []
-        this.vms = []
-        if (this.childId) {
-            this.vm.$[this.childId] = this.vms
-        }
 
         // If the collection is not already converted for observation,
         // we need to convert and watch it.
@@ -137,11 +136,17 @@ module.exports = {
             Observer.watch(collection)
         }
         // listen for collection mutation events
-        collection.__emitter__.on('mutate', this.mutationListener)
+        // collection.__emitter__.on('mutate', this.mutationListener)
+
+        var isObject = collection[0] && utils.typeOf(collection[0]) === 'Object'
+        if (this.oldCollection) {
+            this.vms = this.diff(collection, isObject)
+        } else {
+            this.vms = this.init(collection, isObject)
+        }
 
-        // create new VMs and append to DOM
-        if (collection.length) {
-            collection.forEach(this.build, this)
+        if (this.childId) {
+            this.vm.$[this.childId] = this.vms
         }
 
         // listen for object changes and sync the repeater
@@ -149,33 +154,30 @@ module.exports = {
             this.object.__emitter__.on('set', this.syncRepeater)
             this.object.__emitter__.on('delete', this.deleteProp)
         }
-
-        // destroy unused old VMs
-        if (oldVMs) destroyVMs(oldVMs)
-        this.oldVMs = null
     },
 
-    addItems: function (data, base) {
-        base = base || 0
-        for (var i = 0, l = data.length; i < l; i++) {
-            this.build(data[i], base + i)
-        }
-    },
+    // addItems: function (data, base) {
+    //     base = base || 0
+    //     for (var i = 0, l = data.length; i < l; i++) {
+    //         this.build(data[i], base + i)
+    //     }
+    // },
 
-    removeItems: function (data) {
-        var i = data.length
-        while (i--) {
-            data[i].$destroy()
-        }
-    },
+    // removeItems: function (data) {
+    //     var i = data.length
+    //     while (i--) {
+    //         data[i].$destroy()
+    //     }
+    // },
 
     /**
      *  Run a dry build just to collect bindings
      */
     dryBuild: function () {
-        var Ctor = this.compiler.resolveComponent(this.el)
+        var el = this.el.cloneNode(true),
+            Ctor = this.compiler.resolveComponent(el)
         new Ctor({
-            el     : this.el.cloneNode(true),
+            el     : el,
             parent : this.vm,
             compilerOptions: {
                 repeat: true
@@ -184,105 +186,248 @@ module.exports = {
         this.initiated = true
     },
 
+    init: function (collection, isObject) {
+        var vm, vms = [],
+            data, wrapper,
+            ref = this.ref,
+            ctn = this.container,
+            init = this.compiler.init
+        for (var i = 0, l = collection.length; i < l; i++) {
+            if (isObject) {
+                data = collection[i]
+                data.$index = i
+            } else {
+                data = { $index: i }
+                data.$value = collection[i]
+            }
+            vm = this.build(data, i, isObject)
+            vms.push(vm)
+            if (init) {
+                ctn.insertBefore(vm.$el, ref)
+            } else {
+                vm.$before(ref)
+            }
+        }
+        return vms
+    },
+
     /**
-     *  Create a new child VM from a data object
-     *  passing along compiler options indicating this
-     *  is a v-repeat item.
+     *  Diff the new array with the old
+     *  and determine the minimum amount of DOM manipulations
      */
-    build: function (data, index) {
-
-        var self = this,
-            ctn = self.container,
-            vms = self.vms,
-            el, Ctor, oldIndex, existing, item, nonObject
-
-        // get our DOM insertion reference node
-        var ref = vms.length > index
-            ? vms[index].$el
-            : self.ref
+    diff: function (newCollection, isObject) {
+
+        var i, l, item, vm, oi, ni, wrapper, ref,
+            ctn = this.container,
+            alias = this.arg || '$value',
+            OLD_REUSED = [],
+            NEW_REUSED = [],
+            NEW_DATA = [],
+            oldVMs = this.oldVMs,
+            oldData = this.oldCollection
+
+        // first pass, collect new reused and new created
+        for (i = 0, l = newCollection.length; i < l; i++) {
+            item = newCollection[i]
+            if (isObject) {
+                item.$index = i
+                if (item[this.identifier]) { // existing
+                    item.$newReuseIndex = NEW_REUSED.length++
+                } else {
+                    NEW_DATA.push(item)
+                }
+            } else {
+                // non objects so we can't attach an identifier
+                // oi = oldData.indexOf(item)
+                oi = indexOf(oldVMs, item)
+                if (oi > -1) {
+                    oldVMs[oi].$newReuseIndex = NEW_REUSED.length++
+                } else {
+                    wrapper = { $index: i }
+                    wrapper[alias] = item
+                    NEW_DATA.push(wrapper)
+                }
+            }
+        }
 
-        // check if data already exists in the old array
-        oldIndex = self.oldVMs ? indexOf(self.oldVMs, data) : -1
-        existing = oldIndex > -1
+        // second pass, collect old reused and destroy unused
+        for (i = 0, l = oldVMs.length; i < l; i++) {
+            vm = oldVMs[i]
+            item = vm.$data
+            ni = isObject
+                ? item.$newReuseIndex
+                : vm.$newReuseIndex
+            if (ni != null) {
+                vm.$newReuseIndex = ni
+                vm.$index = item.$index
+                NEW_REUSED[ni] = vm
+                OLD_REUSED.push(vm)
+            } else {
+                delete item[this.identifier]
+                vm.$destroy()
+            }
+        }
 
-        if (existing) {
+        // sort reused
+        var oldNext,
+            newNext,
+            moves = 0
+        for (i = 0, l = OLD_REUSED.length; i < l; i++) {
+            vm = OLD_REUSED[i]
+            oldNext = OLD_REUSED[i + 1]
+            newNext = NEW_REUSED[vm.$newReuseIndex + 1]
+            if (newNext && oldNext !== newNext) {
+                moves++
+                if (!oldNext) {
+                    // I was the last one. move myself to before newNext
+                    ctn.insertBefore(vm.$el, newNext.$el)
+                } else {
+                    // move newNext to after me
+                    ctn.insertBefore(newNext.$el, oldNext.$el)
+                }
+            }
+            // delete temporary data
+            delete vm.$newReuseIndex
+            delete vm.$data.$newReuseIndex
+            delete vm.$data.$index
+        }
 
-            // existing, reuse the old VM
-            item = self.oldVMs[oldIndex]
-            // mark, so it won't be destroyed
-            item.$reused = true
+        // create new
+        for (i = 0, l = NEW_DATA.length; i < l; i++) {
+            item = NEW_DATA[i]
+            ni = item.$index
+            vm = this.build(item, ni, isObject)
+            ref = ni >= NEW_REUSED.length
+                ? this.ref
+                : NEW_REUSED[ni].$el
+            vm.$before(ref)
+            NEW_REUSED.splice(ni, 0, vm)
+        }
 
-        } else {
+        //console.log(OLD_REUSED, NEW_REUSED, NEW_DATA)
+        console.log('moves: ' + moves)
 
-            // new data, need to create new VM.
-            // there's some preparation work to do...
+        console.log(NEW_REUSED.map(function (item, i) {
+            return item.title + ', ' + item.$index
+        }))
 
-            // first clone the template node
-            el = self.el.cloneNode(true)
+        return NEW_REUSED
+    },
 
-            // we have an alias, wrap the data
-            if (self.arg) {
-                var actual = data
-                data = {}
-                data[self.arg] = actual
-            }
+    build: function (data, index, isObject) {
 
-            // wrap non-object value in an object
-            nonObject = utils.typeOf(data) !== 'Object'
-            if (nonObject) {
-                data = { $value: data }
-            }
-            // set index so vm can init with the correct
-            // index instead of undefined
-            data.$index = index
-            // resolve the constructor
-            Ctor = this.compiler.resolveComponent(el, data)
-            // initialize the new VM
-            item = new Ctor({
-                el     : el,
-                data   : data,
-                parent : self.vm,
+        var el = this.el.cloneNode(true),
+            Ctor = this.compiler.resolveComponent(el, data),
+            vm = new Ctor({
+                el: el,
+                data: data,
+                parent: this.vm,
                 compilerOptions: {
                     repeat: true
                 }
             })
-            // for non-object values or aliased items, listen for value change
-            // so we can sync it back to the original Array
-            if (nonObject || self.arg) {
-                var sync = function (val) {
+
+        // attach an ienumerable identifier
+        utils.defProtected(data, this.identifier, true)
+        vm.$index = index
+
+        if (!isObject || this.arg) {
+            var self = this,
+                sync = function (val) {
                     self.lock = true
-                    self.collection.$set(item.$index, val)
+                    self.collection.$set(vm.$index, val)
                     self.lock = false
                 }
-                item.$compiler.observer.on('change:' + (self.arg || '$value'), sync)
-            }
-
+            vm.$compiler.observer.on('change:' + (this.arg || '$value'), sync)
         }
 
-        // put the item into the VM Array
-        vms.splice(index, 0, item)
-        // update the index
-        item.$index = index
-
-        // Finally, DOM operations...
-        el = item.$el
-        if (existing) {
-            // existing vm, we simplify need to re-insert
-            // its element to the new position.
-            ctn.insertBefore(el, ref)
-        } else {
-            if (self.compiler.init) {
-                // do not transition on initial compile,
-                // just manually insert.
-                ctn.insertBefore(el, ref)
-                item.$compiler.execHook('attached')
-            } else {
-                // give it some nice transition.
-                item.$before(ref)
-            }
-        }
+        return vm
+
     },
 
+    /**
+     *  Create a new child VM from a data object
+     *  passing along compiler options indicating this
+     *  is a v-repeat item.
+     */
+    // build: function (data, index) {
+
+    //     var self = this,
+    //         ctn = self.container,
+    //         vms = self.vms,
+    //         el, Ctor, item, nonObject
+
+    //     // get our DOM insertion reference node
+    //     var ref = vms.length > index
+    //         ? vms[index].$el
+    //         : self.ref
+
+    //     // new data, need to create new VM.
+    //     // there's some preparation work to do...
+
+    //     // first clone the template node
+    //     el = self.el.cloneNode(true)
+
+    //     // we have an alias, wrap the data
+    //     if (self.arg) {
+    //         var actual = data
+    //         data = {}
+    //         data[self.arg] = actual
+    //     }
+
+    //     // wrap non-object value in an object
+    //     nonObject = utils.typeOf(data) !== 'Object'
+    //     if (nonObject) {
+    //         data = { $value: data }
+    //     }
+    //     // set index so vm can init with the correct
+    //     // index instead of undefined
+    //     data.$index = index
+    //     // resolve the constructor
+    //     Ctor = this.compiler.resolveComponent(el, data)
+    //     // initialize the new VM
+    //     item = new Ctor({
+    //         el     : el,
+    //         data   : data,
+    //         parent : self.vm,
+    //         compilerOptions: {
+    //             repeat: true
+    //         }
+    //     })
+
+    //     // attach an ienumerable identifier
+    //     utils.defProtected(item, this.identifier, true)
+
+    //     // for non-object values or aliased items, listen for value change
+    //     // so we can sync it back to the original Array
+    //     if (nonObject || self.arg) {
+    //         var sync = function (val) {
+    //             self.lock = true
+    //             self.collection.$set(item.$index, val)
+    //             self.lock = false
+    //         }
+    //         item.$compiler.observer.on('change:' + (self.arg || '$value'), sync)
+    //     }
+
+    //     // put the item into the VM Array
+    //     vms.splice(index, 0, item)
+    //     // update the index
+    //     item.$index = index
+
+    //     // Finally, DOM operations...
+    //     el = item.$el
+
+    //     if (self.compiler.init) {
+    //         // do not transition on initial compile,
+    //         // just manually insert.
+    //         ctn.insertBefore(el, ref)
+    //         item.$compiler.execHook('attached')
+    //     } else {
+    //         // give it some nice transition.
+    //         item.$before(ref)
+    //     }
+    // },
+
     /**
      *  Convert an object to a repeater Array
      *  and make sure changes in the object are synced to the repeater
@@ -347,12 +492,13 @@ module.exports = {
         if (this.object) {
             this.object.__emitter__.off('set', this.updateRepeater)
         }
-        if (this.collection) {
-            this.collection.__emitter__.off('mutate', this.mutationListener)
-            if (destroy) {
-                destroyVMs(this.vms)
-            }
+        if (destroy && this.vms) {
+            destroyVMs(this.vms)
         }
+        // if (this.collection) {
+        //     this.collection.__emitter__.off('mutate', this.mutationListener)
+            
+        // }
     },
 
     unbind: function () {
@@ -362,20 +508,30 @@ module.exports = {
 
 // Helpers --------------------------------------------------------------------
 
-/**
- *  Find an object or a wrapped data object
- *  from an Array
- */
 function indexOf (vms, obj) {
     for (var vm, i = 0, l = vms.length; i < l; i++) {
         vm = vms[i]
-        if (!vm.$reused && (vm.$data === obj || vm.$value === obj)) {
+        if (vm.$newReuseIndex == null && vm.$value === obj) {
             return i
         }
     }
     return -1
 }
 
+/**
+ *  Find an object or a wrapped data object
+ *  from an Array
+ */
+// function indexOf (vms, obj) {
+//     for (var vm, i = 0, l = vms.length; i < l; i++) {
+//         vm = vms[i]
+//         if (!vm.$reused && (vm.$data === obj || vm.$value === obj)) {
+//             return i
+//         }
+//     }
+//     return -1
+// }
+
 /**
  *  Destroy some VMs, yeah.
  */