Jelajahi Sumber

working on directive

Evan You 12 tahun lalu
induk
melakukan
fb1a149109
5 mengubah file dengan 231 tambahan dan 42 penghapusan
  1. 13 20
      src/binding.js
  2. 159 4
      src/directive.js
  3. 56 17
      src/instance/bindings.js
  4. 2 0
      src/instance/init.js
  5. 1 1
      src/observe/observer.js

+ 13 - 20
src/binding.js

@@ -1,23 +1,16 @@
-/**
- * Assign unique id to each binding created so that directives
- * can have an easier time avoiding duplicates and refreshing
- * dependencies.
- */
-
-var uid = 0
-
 /**
  * A binding is an observable that can have multiple directives
  * subscribing to it. It can also have multiple other bindings
  * as children to form a trie-like structure.
  *
+ * All binding properties are prefixed with `_` so that they
+ * don't conflict with children keys.
+ *
  * @constructor
  */
 
 function Binding () {
-  this.id = uid++
-  this.children = Object.create(null)
-  this.subs = []
+  this._subs = []
 }
 
 var p = Binding.prototype
@@ -29,9 +22,9 @@ var p = Binding.prototype
  * @param {Binding} child
  */
 
-p.addChild = function (key, child) {
+p._addChild = function (key, child) {
   child = child || new Binding()
-  this.children[key] = child
+  this[key] = child
   return child
 }
 
@@ -41,8 +34,8 @@ p.addChild = function (key, child) {
  * @param {Directive} sub
  */
 
-p.addSub = function (sub) {
-  this.subs.push(sub)
+p._addSub = function (sub) {
+  this._subs.push(sub)
 }
 
 /**
@@ -51,17 +44,17 @@ p.addSub = function (sub) {
  * @param {Directive} sub
  */
 
-p.removeSub = function (sub) {
-  this.subs.splice(this.subs.indexOf(sub), 1)
+p._removeSub = function (sub) {
+  this._subs.splice(this._subs.indexOf(sub), 1)
 }
 
 /**
  * Notify all subscribers of a new value.
  */
 
-p.notify = function () {
-  for (var i = 0, l = this.subs.length; i < l; i++) {
-    this.subs[i]._update(this)
+p._notify = function () {
+  for (var i = 0, l = this._subs.length; i < l; i++) {
+    this._subs[i]._update(this)
   }
 }
 

+ 159 - 4
src/directive.js

@@ -1,3 +1,8 @@
+var _ = require('./util')
+var Path = require('./parse/path')
+var Observer = require('./observe/observer')
+var expParser = require('./parse/expression')
+
 /**
  * A directive links a DOM element with a piece of data, which can
  * be either simple paths or computed properties. It subscribes to
@@ -7,18 +12,168 @@
  * @param {String} type
  * @param {Node} el
  * @param {Vue} vm
- * @param {String} expression
+ * @param {Object} descriptor
+ *                 - {String} arg
+ *                 - {String} expression
+ *                 - {Array<Object>} filters
  * @constructor
  */
 
-function Directive (type, el, vm, expression) {
-  
+function Directive (type, el, vm, descriptor) {
+  // public
+  this.type = type
+  this.el = el
+  this.vm = vm
+  this.arg = descriptor.arg
+  this.expression = descriptor.expression
+  this.filters = descriptor.filters
+  this.value = undefined
+
+  // private
+  this._deps = Object.create(null)
+  this._newDeps = Object.create(null)
+
+  // TODO
+  // test for simple path vs. expression
+  this._getter = expParser.parse(this.expression)
+  this._setter = this._getter.setter
+
+  var self = this
+
+  // add root level path as a dependency.
+  // this is specifically for the case where the expression
+  // references a non-existing root level path, and later
+  // that path is created with `vm.$add`.
+  // e.g. "a && a.b"
+  var paths = this._getter.paths
+  paths.forEach(function (path) {
+    if (path.indexOf('.') < 0 && path.indexOf('[') < 0) {
+      self._addDep(path)
+    }
+  })
+  this._deps = this._newDeps
+
+  // lock/unlock for setter
+  this._locked = false
+  this._unlock = function () {
+    self._locked = false
+  }
+
+  // collect initial dependencies
+  this.get()
 }
 
 var p = Directive.prototype
 
+/**
+ * Add a binding dependency to this directive.
+ *
+ * @param {String} path
+ */
+
+p._addDep = function (path) {
+  var vm = this.vm
+  var newDeps = this._newDeps
+  var oldDeps = this._deps
+  if (!newDeps[path]) {
+    newDeps[path] = true
+    if (!oldDeps[path]) {
+      var binding =
+        vm._getBindingAt(path, true) ||
+        vm._createBindingAt(path, true)
+      binding._addSub(this)
+    }
+  }
+}
+
+/**
+ * Evaluate the getter, and re-collect dependencies.
+ */
+
+p.get = function () {
+  this._beforeGet()
+  var value = this._getter.call(this.vm, this.vm.$scope)
+  if (this.filters) {
+    value = this._applyFilters(value, -1)
+  }
+  this._afterGet()
+  return value
+}
+
+/**
+ * Set the corresponding value with the setter.
+ * This should only be used in two-way bindings like v-model.
+ *
+ * @param {*} value
+ */
+
+p.set = function (value) {
+  if (this._setter) {
+    this._locked = true
+    if (this.filters) {
+      value = this._applyFilters(value, 1)
+    }
+    this._setter.call(this.vm, this.vm.$scope, value)
+    _.nextTick(this._unlock)
+  }
+}
+
+/**
+ * Prepare for dependency collection.
+ */
+
+p._beforeGet = function () {
+  Observer.emitGet = true
+  this.vm._targetDir = this
+  this._newDeps = Object.create(null)
+}
+
+/**
+ * Clean up for dependency collection.
+ */
+
+p._afterGet = function () {
+  this.vm._targetDir = null
+  Observer.emitGet = false
+  _.extend(this._newDeps, this._deps)
+  this._deps = this._newDeps
+}
+
+/**
+ * The exposed subscriber interface.
+ * Will be called when a dependency changes.
+ */
+
 p._update = function () {
-  
+  this.value = this.get()
+  console.log('updated! new value: ' + this.value)
+}
+
+/**
+ * Apply filters to a value.
+ *
+ * @param {*} value
+ * @param {Number} direction - -1 = read, 1 = write.
+ */
+
+p._applyFilters = function (value, direction) {
+  if (direction < 0) {
+    // TODO read
+    return value
+  } else {
+    // TODO write
+    return value
+  }
+}
+
+/**
+ * Remove self from all dependencies' subcriber list.
+ */
+
+p._teardown = function () {
+  for (var p in this._deps) {
+    this._deps[p]._removeSub(this)
+  }
 }
 
 module.exports = Directive

+ 56 - 17
src/instance/bindings.js

@@ -1,5 +1,6 @@
 var Binding = require('../binding')
 var Path = require('../parse/path')
+var Observer = require('../observe/observer')
 
 /**
  * Setup the binding tree.
@@ -21,20 +22,23 @@ var Path = require('../parse/path')
 exports._initBindings = function () {
   var root = this._rootBinding = new Binding()
   // the $data binding points to the root itself!
-  root.addChild('$data', root)
+  root._addChild('$data', root)
   // point $parent and $root bindings to their
   // repective owners.
   if (this.$parent) {
-    root.addChild('$parent', this.$parent._rootBinding)
-    root.addChild('$root', this.$root._rootBinding)
+    root._addChild('$parent', this.$parent._rootBinding)
+    root._addChild('$root', this.$root._rootBinding)
   }
-  // observer already has callback context set to `this`
-  var update = this._updateBindingAt
+  // setup observer events
   this._observer
-    .on('set', update)
-    .on('add', update)
-    .on('delete', update)
-    .on('mutate', update)
+    // simple updates
+    .on('set', this._updateBindingAt)
+    .on('mutate', this._updateBindingAt)
+    .on('delete', this._updateBindingAt)
+    // adding properties is a bit different
+    .on('add', this._updateAdd)
+    // collect dependency
+    .on('get', this._collectDep)
 }
 
 /**
@@ -43,7 +47,8 @@ exports._initBindings = function () {
  * exist yet along the way.
  *
  * @param {String} path
- * @param {Boolean} fromObserver
+ * @param {Boolean} fromObserver - paths coming from the Observer are
+ *                                 strings of segments delimted by "\b".
  * @return {Binding|undefined}
  */
 
@@ -57,16 +62,21 @@ exports._getBindingAt = function (path, fromObserver) {
  * Create a binding at a given path. Will also create
  * all bindings that do not exist yet along the way.
  *
- * @param {Array} path
+ * @param {String} path
+ * @param {Boolean} fromObserver
  * @return {Binding}
  */
 
-exports._createBindingAt = function (path) {
+exports._createBindingAt = function (path, fromObserver) {
+  path = fromObserver
+    ? path.split(Observer.pathDelimiter)
+    : Path.parse(path)
+  if (!path) return
   var b = this._rootBinding
   var child, key
   for (var i = 0, l = path.length; i < l; i++) {
     key = path[i]
-    child = b.children[key] || b.addChild(key)
+    child = b[key] || b._addChild(key)
     b = child
   }
   return b
@@ -75,14 +85,43 @@ exports._createBindingAt = function (path) {
 /**
  * Trigger update for the binding at given path.
  *
- * @param {String} path - this path comes directly from the
- *                        data observer, so it is a single string
- *                        delimited by "\b".
+ * @param {String} path
  */
 
 exports._updateBindingAt = function (path) {
   var binding = this._getBindingAt(path, true)
   if (binding) {
-    binding.notify()
+    binding._notify()
+  }
+}
+
+/**
+ * For newly added properties, since its binding has not been
+ * created yet, directives will not have it as a dependency yet.
+ * However, they will have its parent as a dependency. Therefore
+ * here we remove the last segment from the path and notify the
+ * added property's parent instead.
+ *
+ * @param {String} path
+ */
+
+exports._updateAdd = function (path) {
+  var index = path.lastIndexOf(Observer.pathDelimiter)
+  if (index > -1) path = path.slice(0, index)
+  this._updateBindingAt(path)
+}
+
+/**
+ * Collect dependency for the target directive being evaluated.
+ *
+ * @param {String} path
+ */
+
+exports._collectDep = function (path) {
+  var directive = this._targetDir
+  // the get event might have come from a child vm's directive
+  // so this._targetDir is not guarunteed to be defined
+  if (directive) {
+    directive._addDep(path)
   }
 }

+ 2 - 0
src/instance/init.js

@@ -21,6 +21,8 @@ exports._init = function (options) {
   this._isDestroyed = false
   this._rawContent  = null
   this._emitter     = new Emitter(this)
+  // the current target directive for dependency collection
+  this._targetDir   = null
 
   // setup parent relationship
   this.$parent = options.parent

+ 1 - 1
src/observe/observer.js

@@ -198,6 +198,7 @@ p.convert = function (key, val) {
     set: function (newVal) {
       if (newVal === val) return
       ob.unobserve(val)
+      val = newVal
       ob.observe(key, newVal)
       ob.emit('set:self', key, newVal)
       ob.propagate('set', key, newVal)
@@ -206,7 +207,6 @@ p.convert = function (key, val) {
                      key + Observer.pathDelimiter + 'length',
                      newVal.length)
       }
-      val = newVal
     }
   })
 }