Evan You 13 лет назад
Родитель
Сommit
84538d6dae
11 измененных файлов с 150 добавлено и 2041 удалено
  1. 1 0
      component.json
  2. 4 1757
      dist/seed.js
  3. 9 0
      examples/nested-props.html
  4. 8 3
      examples/simple.html
  5. 1 1
      examples/todomvc/js/todoStorage.js
  6. 5 93
      src/binding.js
  7. 86 4
      src/compiler.js
  8. 1 1
      src/directives/each.js
  9. 35 54
      src/observe.js
  10. 0 103
      src/utils.js
  11. 0 25
      src/viewmodel.js

+ 1 - 0
component.json

@@ -9,6 +9,7 @@
         "src/compiler.js",
         "src/viewmodel.js",
         "src/binding.js",
+        "src/observe.js",
         "src/directive-parser.js",
         "src/text-parser.js",
         "src/deps-parser.js",

Разница между файлами не показана из-за своего большого размера
+ 4 - 1757
dist/seed.js


+ 9 - 0
examples/nested-props.html

@@ -13,7 +13,16 @@
         <button sd-on="click:two">two</button>
         <button sd-on="click:three">three</button>
         <script>
+            Seed.config({debug: true})
             var Demo = Seed.ViewModel.extend({
+                init: function () {
+                    this.a = {
+                        c: 0,
+                        b: {
+                            c: 'zero'
+                        }
+                    }  
+                },
                 props: {
                     one: function () {
                         this.a = {

+ 8 - 3
examples/simple.html

@@ -10,10 +10,15 @@
     </head>
     <body>
         <input type="checkbox" sd-checked="checked">
-        <span sd-text="hello | uppercase" sd-class="red:checked"></span>
+        <span sd-text="hello.msg | uppercase" sd-class="red:checked"></span>
         <script>
-            var demo = new Seed.ViewModel({ el: 'body' })
-            demo.hello = 'hi'
+            Seed.config({ debug: true })
+            var demo = new Seed.ViewModel({
+                el: 'body'
+            })
+            demo.hello = {
+                msg: 'yoyoyo'
+            }
         </script>
     </body>
 </html>

+ 1 - 1
examples/todomvc/js/todoStorage.js

@@ -5,7 +5,7 @@ var todoStorage = (function () {
             return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '[]')
         },
         save: function (todos) {
-            localStorage.setItem(this.STORAGE_KEY, Seed.utils.serialize(todos))
+            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(todos))
         }
     }
 }())

+ 5 - 93
src/binding.js

@@ -1,7 +1,3 @@
-var utils    = require('./utils'),
-    observer = require('./deps-parser').observer,
-    def      = Object.defineProperty
-
 /*
  *  Binding class.
  *
@@ -10,11 +6,10 @@ var utils    = require('./utils'),
  *  and multiple computed property dependents
  */
 function Binding (compiler, key) {
+    this.value = undefined
+    this.root = key.indexOf('.') === -1
     this.compiler = compiler
     this.key = key
-    var path = key.split('.')
-    this.inspect(utils.getNestedValue(compiler.vm, path))
-    this.def(compiler.vm, path)
     this.instances = []
     this.subs = []
     this.deps = []
@@ -22,97 +17,14 @@ function Binding (compiler, key) {
 
 var BindingProto = Binding.prototype
 
-/*
- *  Pre-process a passed in value based on its type
- */
-BindingProto.inspect = function (value) {
-    var type = utils.typeOf(value)
-    // preprocess the value depending on its type
-    if (type === 'Object') {
-        if (value.get) {
-            var l = Object.keys(value).length
-            if (l === 1 || (l === 2 && value.set)) {
-                this.isComputed = true // computed property
-                this.rawGet = value.get
-                value.get = value.get.bind(this.compiler.vm)
-                if (value.set) value.set = value.set.bind(this.compiler.vm)
-            }
-        }
-    } else if (type === 'Array') {
-        value = utils.dump(value)
-        utils.watchArray(value)
-        value.on('mutate', this.pub.bind(this))
-    }
-    this.value = value
-}
-
-/*
- *  Define getter/setter for this binding on viewmodel
- *  recursive for nested objects
- */
-BindingProto.def = function (viewmodel, path) {
-    var key = path[0]
-    if (path.length === 1) {
-        // here we are! at the end of the path!
-        // define the real value accessors.
-        def(viewmodel, key, {
-            get: (function () {
-                if (observer.isObserving) {
-                    observer.emit('get', this)
-                }
-                return this.isComputed
-                    ? this.value.get({
-                        el: this.compiler.el,
-                        vm: this.compiler.vm
-                    })
-                    : this.value
-            }).bind(this),
-            set: (function (value) {
-                if (this.isComputed) {
-                    // computed properties cannot be redefined
-                    // no need to call binding.update() here,
-                    // as dependency extraction has taken care of that
-                    if (this.value.set) {
-                        this.value.set(value)
-                    }
-                } else if (value !== this.value) {
-                    this.update(value)
-                }
-            }).bind(this)
-        })
-    } else {
-        // we are not there yet!!!
-        // create an intermediate object
-        // which also has its own getter/setters
-        var nestedObject = viewmodel[key]
-        if (!nestedObject) {
-            nestedObject = {}
-            def(viewmodel, key, {
-                get: (function () {
-                    return this
-                }).bind(nestedObject),
-                set: (function (value) {
-                    // when the nestedObject is given a new value,
-                    // copy everything over to trigger the setters
-                    for (var prop in value) {
-                        this[prop] = value[prop]
-                    }
-                }).bind(nestedObject)
-            })
-        }
-        // recurse
-        this.def(nestedObject, path.slice(1))
-    }
-}
-
 /*
  *  Process the value, then trigger updates on all dependents
  */
 BindingProto.update = function (value) {
-    this.inspect(value)
+    this.value = value
     var i = this.instances.length
     while (i--) {
-        this.instances[i].update(this.value)
+        this.instances[i].update(value)
     }
     this.pub()
 }
@@ -142,7 +54,7 @@ BindingProto.unbind = function () {
         subs = this.deps[i].subs
         subs.splice(subs.indexOf(this), 1)
     }
-    if (Array.isArray(this.value)) this.value.off('mutate')
+    // TODO if this is a root level binding
     this.compiler = this.pubs = this.subs = this.instances = this.deps = null
 }
 

+ 86 - 4
src/compiler.js

@@ -1,4 +1,6 @@
-var config          = require('./config'),
+var Emitter         = require('emitter'),
+    observe         = require('./observe'),
+    config          = require('./config'),
     utils           = require('./utils'),
     Binding         = require('./binding'),
     DirectiveParser = require('./directive-parser'),
@@ -32,6 +34,7 @@ function Compiler (vm, options) {
     vm.$compiler         = this
     this.el              = vm.$el
     this.bindings        = {}
+    this.observer        = new Emitter()
     this.directives      = []
     this.watchers        = {}
     // list of computed properties that need to parse dependencies for
@@ -39,6 +42,9 @@ function Compiler (vm, options) {
     // list of bindings that has dynamic context dependencies
     this.contextBindings = []
 
+    // setup observer
+    this.setupObserver()
+
     // copy data if any
     var key, data = options.data
     if (data) {
@@ -82,6 +88,27 @@ function Compiler (vm, options) {
 // for better compression
 var CompilerProto = Compiler.prototype
 
+/*
+ *  setup observer
+ */
+CompilerProto.setupObserver = function () {
+    var bindings = this.bindings, compiler = this
+    this.observer
+        .on('get', function (key) {
+            if (DepsParser.observer.isObserving) {
+                DepsParser.observer.emit('get', bindings[key])
+            }
+        })
+        .on('set', function (key, val) {
+            console.log('set:', key, '=>', val)
+            if (!bindings[key]) compiler.createBinding(key)
+            bindings[key].update(val)
+        })
+        .on('mutate', function (key) {
+            bindings[key].refresh()
+        })
+}
+
 /*
  *  Compile a DOM node (recursive)
  */
@@ -147,8 +174,7 @@ CompilerProto.compileNode = function (node, root) {
             // recursively compile childNodes
             if (node.childNodes.length) {
                 var nodes = slice.call(node.childNodes)
-                i = nodes.length
-                while (i--) {
+                for (i = 0, j = nodes.length; i < j; i++) {
                     this.compileNode(nodes[i])
                 }
             }
@@ -187,12 +213,68 @@ CompilerProto.compileTextNode = function (node) {
  */
 CompilerProto.createBinding = function (key) {
     utils.log('  created binding: ' + key)
+
     var binding = new Binding(this, key)
     this.bindings[key] = binding
-    if (binding.isComputed) this.computed.push(binding)
+
+    var baseKey = key.split('.')[0]
+    if (binding.root) {
+        // this is a root level binding. we need to define getter/setters for it.
+        this.define(baseKey, binding)
+    } else if (!this.bindings[baseKey]) {
+        // this is a nested value binding, but the binding for its root
+        // has not been created yet. We better create that one too.
+        this.createBinding(baseKey)
+    }
+
     return binding
 }
 
+/*
+ *  Defines the getter/setter for a top-level binding on the VM
+ *  and observe the initial value
+ */
+CompilerProto.define = function (key, binding) {
+
+    utils.log('    defined root binding: ' + key)
+
+    var compiler = this,
+        value = binding.value = this.vm[key] // save the value before redefinening it
+
+    if (utils.typeOf(value) === 'Object' && value.get) {
+        binding.isComputed = true
+        binding.rawGet = value.get
+        value.get = value.get.bind(this.vm)
+        this.computed.push(binding)
+    } else {
+        observe(value, key, compiler.observer) // start observing right now
+    }
+
+    Object.defineProperty(this.vm, key, {
+        enumerable: true,
+        get: function () {
+            compiler.observer.emit('get', key)
+            return binding.isComputed
+                ? binding.value.get({
+                    el: compiler.el,
+                    vm: compiler.vm
+                })
+                : binding.value
+        },
+        set: function (value) {
+            if (binding.isComputed) {
+                if (binding.value.set) {
+                    binding.value.set(value)
+                }
+            } else if (value !== binding.value) {
+                compiler.observer.emit('set', key, value)
+                observe(value, key, compiler.observer)
+            }
+        }
+    })
+
+}
+
 /*
  *  Add a directive instance to the correct binding & viewmodel
  */

+ 1 - 1
src/directives/each.js

@@ -98,7 +98,7 @@ module.exports = {
 
         // listen for collection mutation events
         // the collection has been augmented during Binding.set()
-        collection.on('mutate', (function (mutation) {
+        collection.__observer__.on('mutate', (function (mutation) {
             mutationHandlers[mutation.method].call(this, mutation)
         }).bind(this))
 

+ 35 - 54
src/watcher.js → src/observe.js

@@ -1,4 +1,6 @@
-var Emitter = require('events').EventEmitter,
+var Emitter = require('emitter'),
+    utils   = require('./utils'),
+    typeOf  = utils.typeOf,
     def     = Object.defineProperty,
     slice   = Array.prototype.slice,
     methods = ['push','pop','shift','unshift','splice','sort','reverse']
@@ -16,11 +18,11 @@ var arrayMutators = {
 
 methods.forEach(function (method) {
     arrayMutators[method] = function () {
-        var result = Array.prototype[method].apply(this, arguments),
-            newElements
+        var result = Array.prototype[method].apply(this, arguments)
 
         // watch new objects - do we need this? maybe do it in each.js
 
+        // var newElements
         // if (method === 'push' || method === 'unshift') {
         //     newElements = arguments
         // } else if (method === 'splice') {
@@ -30,7 +32,7 @@ methods.forEach(function (method) {
         //     var i = newElements.length
         //     while (i--) watch(newElements[i])
         // }
-        this.__observer__.emit('mutate', this.__path__, this, mutation = {
+        this.__observer__.emit('mutate', this.__path__, this, {
             method: method,
             args: slice.call(arguments),
             result: result
@@ -40,18 +42,27 @@ methods.forEach(function (method) {
 
 // EXTERNAL
 function observe (obj, path, observer) {
-    watch(obj)
-    path = path + '.'
-    obj.__observer__
-        .on('get', function (key) {
-            observer.emit('get', path + key)
-        })
-        .on('set', function (key, val) {
-            observer.emit('set', path + key, val)
-        })
-        .on('mutate', function (key, val, mutation) {
-            observer.emit('mutate', path + key, val, mutation)
-        })
+    if (isWatchable(obj)) {
+        path = path + '.'
+        var alreadyConverted = !!obj.__observer__
+        if (!alreadyConverted) {
+            var ob = new Emitter()
+            defProtected(obj, '__observer__', ob)
+        }
+        obj.__observer__
+            .on('get', function (key) {
+                observer.emit('get', path + key)
+            })
+            .on('set', function (key, val) {
+                observer.emit('set', path + key, val)
+            })
+            .on('mutate', function (key, val, mutation) {
+                observer.emit('mutate', path + key, val, mutation)
+            })
+        if (!alreadyConverted) {
+            watch(obj, null, ob)
+        }
+    }
 }
 
 // INTERNAL
@@ -66,7 +77,7 @@ function watch (obj, path, observer) {
 
 function watchObject (obj, path, observer) {
     defProtected(obj, '__values__', {})
-    defProtected(obj, '__observer__', observer || new Emitter())
+    defProtected(obj, '__observer__', observer)
     for (var key in obj) {
         bind(obj, key, path, obj.__observer__)
     }
@@ -74,8 +85,8 @@ function watchObject (obj, path, observer) {
 
 function watchArray (arr, path, observer) {
     defProtected(arr, '__path__', path)
-    defProtected(arr, '__observer__', observer || new Emitter())
-    for (method in arrayMutators) {
+    defProtected(arr, '__observer__', observer)
+    for (var method in arrayMutators) {
         defProtected(arr, method, arrayMutators[method])
     }
     // var i = arr.length
@@ -87,6 +98,7 @@ function bind (obj, key, path, observer) {
         values = obj.__values__,
         fullKey = (path ? path + '.' : '') + key
     values[fullKey] = val
+    observer.emit('set', fullKey, val)
     def(obj, key, {
         enumerable: true,
         get: function () {
@@ -110,40 +122,9 @@ function defProtected (obj, key, val) {
     })
 }
 
-function typeOf (obj) {
-    return toString.call(obj).slice(8, -1)
-}
-
-var data = {
-    id: 1,
-    user: {
-        firstName: 'Jack',
-        lastName: 'Daniels'
-    },
-    posts: [
-        {
-            title: 'hi',
-            content: 'Whaaat up'
-        },
-        {
-            title: 'lol',
-            content: 'This is cool'
-        }
-    ]
+function isWatchable (obj) {
+    var type = typeOf(obj)
+    return type === 'Object' || type === 'Array'
 }
 
-var ob = new Emitter()
-
-observe(data, 'testing', ob)
-ob.on('set', function (key, val) {
-    console.log('set: ' + key + ' =>\n', val)
-})
-ob.on('mutate', function (key, val, mutation) {
-    console.log('mutate: '+ key + ' =>\n', val)
-    console.log(mutation)
-})
-
-data.id = 2
-data.posts.push({ title: 'hola' })
-
-module.exports = data
+module.exports = observe

+ 0 - 103
src/utils.js

@@ -1,35 +1,8 @@
 var config        = require('./config'),
-    Emitter       = require('emitter'),
     toString      = Object.prototype.toString,
-    aproto        = Array.prototype,
     templates     = {},
     VMs           = {}
 
-var arrayAugmentations = {
-    remove: function (index) {
-        if (typeof index !== 'number') index = index.$index
-        this.splice(index, 1)
-    },
-    replace: function (index, data) {
-        if (typeof index !== 'number') index = index.$index
-        this.splice(index, 1, data)
-    }
-}
-
-var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse'],
-    mutationInterceptors = {}
-
-arrayMutators.forEach(function (method) {
-    mutationInterceptors[method] = function () {
-        var result = aproto[method].apply(this, arguments)
-        this.emit('mutate', {
-            method: method,
-            args: aproto.slice.call(arguments),
-            result: result
-        })
-    }
-})
-
 /*
  *  get accurate type of an object
  */
@@ -37,85 +10,9 @@ function typeOf (obj) {
     return toString.call(obj).slice(8, -1)
 }
 
-/*
- *  Recursively dump stuff...
- */
-function dump (val) {
-    var type = typeOf(val)
-    if (type === 'Array') {
-        return val.map(dump)
-    } else if (type === 'Object') {
-        if (val.get) { // computed property
-            return val.get()
-        } else { // object / child viewmodel
-            var ret = {}, prop
-            for (var key in val) {
-                prop = val[key]
-                if (typeof prop !== 'function' &&
-                    val.hasOwnProperty(key) &&
-                    key.charAt(0) !== '$' &&
-                    !isContextual(key, val))
-                {
-                    ret[key] = dump(prop)
-                }
-            }
-            return ret
-        }
-    } else if (type !== 'Function') {
-        return val
-    }
-}
-
-/*
- *  check if a value belongs to a contextual binding
- *  because we do NOT want to dump those.
- */
-function isContextual (key, vm) {
-    if (!vm.$compiler) return false
-    var binding = vm.$compiler.bindings[key]
-    return binding.isContextual
-}
-
 module.exports = {
 
     typeOf: typeOf,
-    dump: dump,
-
-    /*
-     *  shortcut for JSON.stringify-ing a dumped value
-     */
-    serialize: function (val) {
-        return JSON.stringify(dump(val))
-    },
-
-    /*
-     *  Get a value from an object based on a path array
-     */
-    getNestedValue: function (obj, path) {
-        if (path.length === 1) return obj[path[0]]
-        var i = 0
-        /* jshint boss: true */
-        while (obj[path[i]]) {
-            obj = obj[path[i]]
-            i++
-        }
-        return i === path.length ? obj : undefined
-    },
-
-    /*
-     *  augment an Array so that it emit events when mutated
-     */
-    watchArray: function (collection) {
-        Emitter(collection)
-        var method, i = arrayMutators.length
-        while (i--) {
-            method = arrayMutators[i]
-            collection[method] = mutationInterceptors[method]
-        }
-        for (method in arrayAugmentations) {
-            collection[method] = arrayAugmentations[method]
-        }
-    },
 
     getTemplate: function (id) {
         var el = templates[id]

+ 0 - 25
src/viewmodel.js

@@ -64,31 +64,6 @@ VMProto.$unwatch = function (key) {
     }, 0)
 }
 
-/*
- *  load data into viewmodel
- */
-VMProto.$load = function (data) {
-    for (var key in data) {
-        this[key] = data[key]
-    }
-}
-
-/*
- *  Dump a copy of current viewmodel data, excluding compiler-exposed properties.
- *  @param key (optional): key for the value to dump
- */
-VMProto.$dump = function (key) {
-    var bindings = this.$compiler.bindings
-    return utils.dump(key ? bindings[key].value : this)
-}
-
-/*
- *  stringify the result from $dump
- */
-VMProto.$serialize = function (key) {
-    return JSON.stringify(this.$dump(key))
-}
-
 /*
  *  unbind everything, remove everything
  */

Некоторые файлы не были показаны из-за большого количества измененных файлов