Просмотр исходного кода

auto parse dependency for computed properties!!!!!

Evan You 12 лет назад
Родитель
Сommit
7a0172d60b
5 измененных файлов с 101 добавлено и 73 удалено
  1. 1 5
      TODO.md
  2. 2 2
      examples/todos/app.js
  3. 4 4
      examples/todos/index.html
  4. 7 11
      src/directive-parser.js
  5. 87 51
      src/seed.js

+ 1 - 5
TODO.md

@@ -1,11 +1,7 @@
-- use descriptor for computed properties
-- getter setter should emit events
-- auto dependency extraction for computed properties (evaluate, record triggered getters)
-
 - parse textNodes?
 - more directives / filters
     - sd-if
     - sd-with
     - sd-visible
-    - sd-style
+    - sd-style="transform:transform < x y z rotate"
 - nested properties in scope (kinda hard, maybe later)

+ 2 - 2
examples/todos/app.js

@@ -23,7 +23,7 @@ Seed.controller('Todos', function (scope) {
     }}
 
     scope.completed = {get: function () {
-        return scope.total() - scope.remaining
+        return scope.total - scope.remaining
     }}
 
     scope.itemLabel = {get: function () {
@@ -66,7 +66,7 @@ Seed.controller('Todos', function (scope) {
         scope.todos.forEach(function (todo) {
             todo.done = e.el.checked
         })
-        scope.remaining = e.el.checked ? 0 : scope.total()
+        scope.remaining = e.el.checked ? 0 : scope.total
     }
 
     scope.removeCompleted = function () {

+ 4 - 4
examples/todos/index.html

@@ -20,7 +20,7 @@
                 >
             </header>
             
-            <section id="main" sd-show="total < todos">
+            <section id="main" sd-show="total">
                 <input 
                     id="toggle-all"
                     type="checkbox"
@@ -58,10 +58,10 @@
             </section>
 
             <!-- footer controls -->
-            <footer id="footer" sd-show="total < todos">
+            <footer id="footer" sd-show="total">
                 <span id="todo-count">
                     <strong sd-text="remaining"></strong>
-                    <span sd-text="itemLabel < remaining"></span>
+                    <span sd-text="itemLabel"></span>
                     left
                 </span>
                 <ul id="filters">
@@ -70,7 +70,7 @@
                     <li><a href="#/completed" data-filter="completed" sd-on="click:setFilter">Completed</a></li>
                 </ul>
                 <button id="clear-completed" sd-on="click:removeCompleted">
-                    Remove Completed (<span sd-text="completed < total remaining"></span>)
+                    Remove Completed (<span sd-text="completed"></span>)
                 </button>
             </footer>
 

+ 7 - 11
src/directive-parser.js

@@ -91,17 +91,13 @@ function Directive (directiveName, expression) {
     this.filters = filterExps
         ? filterExps.map(parseFilter)
         : null
-
-    var depExp = expression.match(DEPS_RE)
-    this.deps = depExp
-        ? depExp[0].slice(1).trim().split(/\s+/).map(parseKey)
-        : null
 }
 
 // called when a dependency has changed
 Directive.prototype.refresh = function () {
-    if (this.value) {
-        var value = this.value.call(this.seed.scope)
+    var getter = this.value
+    if (getter && typeof getter === 'function') {
+        var value = getter.call(this.seed.scope)
         if (this.inverse) value = !value
         this._update(
             this.filters
@@ -109,9 +105,7 @@ Directive.prototype.refresh = function () {
             : value
         )
     }
-    if (this.binding.refreshDependents) {
-        this.binding.refreshDependents()
-    }
+    this.binding.emitChange()
 }
 
 // called when a new value is set
@@ -128,7 +122,9 @@ Directive.prototype.update = function (value) {
         ? this.applyFilters(value)
         : value
     )
-    if (this.deps) this.refresh()
+    if (this.binding.isComputed) {
+        this.refresh()
+    }
 }
 
 Directive.prototype.applyFilters = function (value) {

+ 87 - 51
src/seed.js

@@ -7,6 +7,13 @@ var slice           = Array.prototype.slice,
     ctrlAttr        = config.prefix + '-controller',
     eachAttr        = config.prefix + '-each'
 
+var depsObserver    = new Emitter(),
+    parsingDeps     = false
+
+/*
+ *  The main ViewModel class
+ *  scans a node and parse it to populate data bindings
+ */
 function Seed (el, options) {
 
     if (typeof el === 'string') {
@@ -16,6 +23,7 @@ function Seed (el, options) {
     this.el         = el
     el.seed         = this
     this._bindings  = {}
+    this._computed  = []
 
     // copy options
     options = options || {}
@@ -37,6 +45,7 @@ function Seed (el, options) {
         scope = this.scope = scope.$dump()
     }
 
+    // expose some useful stuff on the scope
     scope.$seed     = this
     scope.$destroy  = this._destroy.bind(this)
     scope.$dump     = this._dump.bind(this)
@@ -44,13 +53,14 @@ function Seed (el, options) {
     scope.$parent   = options.parentSeed && options.parentSeed.scope
     scope.$refresh  = this._refreshBinding.bind(this)
 
-    // update bindings when a property is set
+    // add event listener to update corresponding binding
+    // when a property is set
     this.on('set', this._updateBinding.bind(this))
 
-    // revursively compile nodes for directives
+    // now parse the DOM
     this._compileNode(el, true)
 
-    // if has controller, apply it
+    // if has controller function, apply it
     var ctrlID = el.getAttribute(ctrlAttr)
     if (ctrlID) {
         el.removeAttribute(ctrlAttr)
@@ -61,8 +71,17 @@ function Seed (el, options) {
             console.warn('controller ' + ctrlID + ' is not defined.')
         }
     }
+
+    // extract dependencies for computed properties
+    parsingDeps = true
+    this._computed.forEach(this._parseDeps.bind(this))
+    delete this._computed
+    parsingDeps = false
 }
 
+/*
+ *  Compile a node (recursive)
+ */
 Seed.prototype._compileNode = function (node, root) {
     var seed = this
 
@@ -77,9 +96,10 @@ Seed.prototype._compileNode = function (node, root) {
 
         if (eachExp) { // each block
 
-            var binding = DirectiveParser.parse(eachAttr, eachExp)
-            if (binding) {
-                seed._bind(node, binding)
+            var directive = DirectiveParser.parse(eachAttr, eachExp)
+            if (directive) {
+                directive.el = node
+                seed._bind(directive)
             }
 
         } else if (ctrlExp && !root) { // nested controllers
@@ -103,7 +123,8 @@ Seed.prototype._compileNode = function (node, root) {
                         var directive = DirectiveParser.parse(attr.name, exp)
                         if (directive) {
                             valid = true
-                            seed._bind(node, directive)
+                            directive.el = node
+                            seed._bind(directive)
                         }
                     })
                     if (valid) node.removeAttribute(attr.name)
@@ -120,13 +141,18 @@ Seed.prototype._compileNode = function (node, root) {
     }
 }
 
+/*
+ *  Compile a text node
+ */
 Seed.prototype._compileTextNode = function (node) {
     return TextNodeParser.parse(node)
 }
 
-Seed.prototype._bind = function (node, directive) {
+/*
+ *  Add a directive instance to the correct binding & scope
+ */
+Seed.prototype._bind = function (directive) {
 
-    directive.el   = node
     directive.seed = this
 
     var key = directive.key,
@@ -159,43 +185,24 @@ Seed.prototype._bind = function (node, directive) {
     // set initial value
     directive.update(binding.value)
 
-    // computed properties
-    if (directive.deps) {
-        directive.deps.forEach(function (dep) {
-            var depScope = determinScope(dep, scope),
-                depBinding =
-                    depScope._bindings[dep.key] ||
-                    depScope._createBinding(dep.key)
-            if (!depBinding.dependents) {
-                depBinding.dependents = []
-                depBinding.refreshDependents = function () {
-                    depBinding.dependents.forEach(function (dept) {
-                        dept.refresh()
-                    })
-                }
-            }
-            depBinding.dependents.push(directive)
-        })
-    }
-
 }
 
 Seed.prototype._createBinding = function (key) {
 
-    var binding = {
-        value: this.scope[key],
-        changed: false,
-        instances: []
-    }
-
+    var binding = new Binding(this.scope[key])
     this._bindings[key] = binding
 
     // bind accessor triggers to scope
     var seed = this
     Object.defineProperty(this.scope, key, {
         get: function () {
+            if (parsingDeps) {
+                depsObserver.emit('get', binding)
+            }
             seed.emit('get', key)
-            return binding.value
+            return binding.isComputed
+                ? binding.value()
+                : binding.value
         },
         set: function (value) {
             if (value === binding.value) return
@@ -209,11 +216,13 @@ Seed.prototype._createBinding = function (key) {
 Seed.prototype._updateBinding = function (key, value) {
 
     var binding = this._bindings[key],
-        type = typeOf(value)
+        type = binding.type = typeOf(value)
 
+    // preprocess the value depending on its type
     if (type === 'Object') {
         if (value.get) { // computed property
-            type = 'Computed'
+            this._computed.push(binding)
+            binding.isComputed = true
             value = value.get
         } else { // normal object
             // TODO watchObject
@@ -221,15 +230,11 @@ Seed.prototype._updateBinding = function (key, value) {
     } else if (type === 'Array') {
         watchArray(value)
         value.on('mutate', function () {
-            if (binding.dependents) {
-                binding.refreshDependents()
-            }
+            binding.emitChange()
         })
     }
 
-    binding.type = type
     binding.value = value
-    binding.changed = true
 
     // update all instances
     binding.instances.forEach(function (instance) {
@@ -237,10 +242,7 @@ Seed.prototype._updateBinding = function (key, value) {
     })
 
     // notify dependents to refresh themselves
-    if (binding.dependents) {
-        binding.refreshDependents()
-    }
-
+    binding.emitChange()
 }
 
 Seed.prototype._refreshBinding = function (key) {
@@ -250,6 +252,17 @@ Seed.prototype._refreshBinding = function (key) {
     })
 }
 
+Seed.prototype._parseDeps = function (binding) {
+    depsObserver.on('get', function (dep) {
+        if (!dep.dependents) {
+            dep.dependents = []
+        }
+        dep.dependents.push.apply(dep.dependents, binding.instances)
+    })
+    binding.value()
+    depsObserver.off('get')
+}
+
 Seed.prototype._unbind = function () {
     var unbind = function (instance) {
         if (instance.unbind) {
@@ -281,7 +294,7 @@ Seed.prototype._dump = function () {
             if (!val) continue
             if (Array.isArray(val)) {
                 dump[key] = val.map(subDump)
-            } else {
+            } else if (typeof val !== 'function') {
                 dump[key] = this._bindings[key].value
             }
         }
@@ -289,10 +302,27 @@ Seed.prototype._dump = function () {
     return dump
 }
 
+/*
+ *  Binding class
+ */
+ function Binding (value) {
+    this.value = value
+    this.instances = []
+    this.dependents = []
+ }
+
+ Binding.prototype.emitChange = function () {
+     this.dependents.forEach(function (dept) {
+         dept.refresh()
+     })
+ }
+
 // Helpers --------------------------------------------------------------------
 
-// determine which scope a key belongs to
-// based on nesting symbols
+/*
+ *  determinScope()
+ *  determine which scope a key belongs to based on nesting symbols
+ */
 function determinScope (key, scope) {
     if (key.nesting) {
         var levels = key.nesting
@@ -307,13 +337,19 @@ function determinScope (key, scope) {
     return scope
 }
 
-// get accurate type of an object
+/* 
+ *  typeOf()
+ *  get accurate type of an object
+ */
 var OtoString = Object.prototype.toString
 function typeOf (obj) {
     return OtoString.call(obj).slice(8, -1)
 }
 
-// augment an Array so that it emit events when mutated
+/*
+ *  watchArray()
+ *  augment an Array so that it emit events when mutated
+ */
 var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
 var arrayAugmentations = {
     remove: function (scope) {