Explorar el Código

exploring scope inehritance

Evan You hace 12 años
padre
commit
0b70e524b7
Se han modificado 6 ficheros con 173 adiciones y 22 borrados
  1. 6 0
      changes.md
  2. 108 0
      explorations/inheritance.js
  3. 1 0
      explorations/test.html
  4. 2 3
      src/observer/object-augmentations.js
  5. 11 5
      src/observer/observer.js
  6. 45 14
      src/util.js

+ 6 - 0
changes.md

@@ -75,6 +75,12 @@ Vue.filter('format', {
 <!-- v-if-end -->
 ```
 
+## (Experimental) New Scope Inheritance Model
+
+In the previous version, nested Vue instances do not have prototypal inheritance of their data scope. Although you can access parent data properties in templates, you need to explicitly travel up the scope chain with `this.$parent` in JavaScript code or use `this.$get()` to get a property on the scope chain. The expression parser also needs to do a lot of dirty work to determine the correct scope the variables belong to.
+
+In the new model, we provide a scope inehritance system similar to Angular, in which you can directly access properties that exist on parent scopes. The major difference is that setting a primitive value property on a child scope WILL affect that on the parent scope! This is one of the major gotchas in Angular and it's no longer an issue in Vue, although if you are somewhat familiar with how prototype inehritance works, you might be surprised how this is possible. Well, the reason is that all data properties in Vue are getter/setters, and invoking a setter will not cause the child scope shadowing parent scopes.
+
 ## (Experimental) Validators
 
 ``` html

+ 108 - 0
explorations/inheritance.js

@@ -0,0 +1,108 @@
+var Observer = require('../src/observer/observer')
+var _ = require('../src/util')
+
+function Vue (options) {
+
+  // scope prototypal inehritance
+  var scope = this._scope = options.parent
+    ? Object.create(options.parent._scope)
+    : {}
+
+  // copy instantiation data into scope
+  for (var key in options.data) {
+    if (key in scope) {
+      // key exists on the scope prototype chain
+      // cannot use direct set here, because in the parent
+      // scope everything is already getter/setter and we
+      // need to overwrite them with Object.defineProperty.
+      _.define(scope, key, options.data[key], true)
+    } else {
+      scope[key] = options.data[key]
+    }
+  }
+
+  // create observer
+  // pass in noProto:true to avoid mutating the __proto__
+  var ob = this._observer = Observer.create(this._scope, true)
+
+  // relay change events from parent scope.
+  // this ensures the current Vue instance is aware of
+  // stuff going on up in the scope chain.
+  if (options.parent) {
+    var po = options.parent._observer
+    ;['set', 'mutate', 'added', 'deleted'].forEach(function (event) {
+      po.on(event, function (key, a, b) {
+        if (!scope.hasOwnProperty(key)) {
+          ob.emit(event, key, a, b)
+        }
+      })
+    })
+  }
+
+  // proxy everything on self
+  for (var key in this._scope) {
+    _.proxy(this, this._scope, key)
+  }
+
+  // also proxy newly added keys.
+  var self = this
+  ob.on('added', function (key) {
+    if (!self.hasOwnProperty(key)) {
+      _.proxy(self, scope, key)
+    }
+  })
+
+}
+
+Vue.prototype.$add = function (key, val) {
+  this._scope.$add.call(this._scope, key, val)
+}
+
+Vue.prototype.$delete = function (key) {
+  this._scope.$delete.call(this._scope, key)
+}
+
+window.vm = new Vue({
+  data: {
+    a: 'go!',
+    b: 2,
+    c: {
+      d: 3
+    },
+    arr: [{a:1}, {a:2}, {a:3}],
+    get hello () {
+      return 'hello!' + this.a
+    },
+    go: function () {
+      console.log(this.a)
+    }
+  }
+})
+
+window.child = new Vue({
+  parent: vm,
+  data: {
+    a: 1,
+    change: function () {
+      this.c.d = 4
+      this.b = 456 // Unlike Angular, setting primitive values in Vue WILL affect outer scope,
+                   // unless you overwrite it in the instantiation data!
+    }
+  }
+})
+
+vm._observer.on('set', function (key, val) {
+  console.log('vm set:' + key.replace(/[\b]/g, '.'), val)
+})
+
+child._observer.on('set', function (key, val) {
+  console.log('child set:' + key.replace(/[\b]/g, '.'), val)
+})
+
+vm._observer.on('mutate', function (key, val) {
+  console.log('vm mutate:' + key.replace(/[\b]/g, '.'), val)
+})
+
+child._observer.on('mutate', function (key, val) {
+  console.log('child mutate:' + key.replace(/[\b]/g, '.'), val)
+})

+ 1 - 0
explorations/test.html

@@ -0,0 +1 @@
+<script src="build.js"></script>

+ 2 - 3
src/observer/object-augmentations.js

@@ -12,7 +12,8 @@ var objectAgumentations = Object.create(Object.prototype)
 
 _.define(objectAgumentations, '$add', function (key, val) {
   if (this.hasOwnProperty(key)) return
-  this[key] = val
+  // make sure it's defined on itself.
+  _.define(this, key, val, true)
   var ob = this.$observer
   ob.observe(key, val)
   ob.convert(key, val)
@@ -29,8 +30,6 @@ _.define(objectAgumentations, '$add', function (key, val) {
 
 _.define(objectAgumentations, '$delete', function (key) {
   if (!this.hasOwnProperty(key)) return
-  // trigger set events
-  this[key] = undefined
   delete this[key]
   this.$observer.notify('deleted', key)
 })

+ 11 - 5
src/observer/observer.js

@@ -26,7 +26,7 @@ var OBJECT = 1
  * @param {Number} [type]
  */
 
-function Observer (value, type) {
+function Observer (value, type, noProto) {
   Emitter.call(this)
   this.value = value
   this.type = type
@@ -37,7 +37,11 @@ function Observer (value, type) {
       _.augment(value, arrayAugmentations)
       this.link(value)
     } else if (type === OBJECT) {
-      _.augment(value, objectAugmentations)
+      if (noProto) {
+        _.deepMixin(value, objectAugmentations)
+      } else {
+        _.augment(value, objectAugmentations)
+      }
       this.walk(value)
     }
   }
@@ -72,13 +76,15 @@ Observer.emitGet = false
  * @static
  */
 
-Observer.create = function (value) {
-  if (value && value.$observer) {
+Observer.create = function (value, noProto) {
+  if (value &&
+      value.hasOwnProperty('$observer') &&
+      value.$observer instanceof Observer) {
     return value.$observer
   } if (_.isArray(value)) {
     return new Observer(value, ARRAY)
   } else if (_.isObject(value)) {
-    return new Observer(value, OBJECT)
+    return new Observer(value, OBJECT, noProto)
   }
 }
 

+ 45 - 14
src/util.js

@@ -1,18 +1,53 @@
 /**
  * Mix properties into target object.
  *
- * @param {Object} target
- * @param {Object} mixin
+ * @param {Object} to
+ * @param {Object} from
  */
 
-exports.mixin = function (target, mixin) {
-  for (var key in mixin) {
-    if (target[key] !== mixin[key]) {
-      target[key] = mixin[key]
+exports.mixin = function (to, from) {
+  for (var key in from) {
+    if (to[key] !== from[key]) {
+      to[key] = from[key]
     }
   }
 }
 
+/**
+ * Mixin including non-enumerables, and copy property descriptors.
+ *
+ * @param {Object} to
+ * @param {Object} from
+ */
+
+exports.deepMixin = function (to, from) {
+  Object.getOwnPropertyNames(from).forEach(function (key) {
+    var descriptor = Object.getOwnPropertyDescriptor(from, key)
+    Object.defineProperty(to, key, descriptor)
+  })
+}
+
+/**
+ * Proxy a property on one object to another.
+ *
+ * @param {Object} to
+ * @param {Object} from
+ * @param {String} key
+ */
+
+exports.proxy = function (to, from, key) {
+  Object.defineProperty(to, key, {
+    enumerable: true,
+    configurable: true,
+    get: function () {
+      return from[key]
+    },
+    set: function (val) {
+      from[key] = val
+    }
+  })
+}
+
 /**
  * Object type check. Only returns true
  * for plain JavaScript objects.
@@ -42,12 +77,13 @@ exports.isArray = function (obj) {
  * @param {Object} obj
  * @param {String} key
  * @param {*} val
+ * @param {Boolean} [enumerable]
  */
 
-exports.define = function (obj, key, val) {
+exports.define = function (obj, key, val, enumerable) {
   Object.defineProperty(obj, key, {
     value        : val,
-    enumerable   : false,
+    enumerable   : !!enumerable,
     writable     : true,
     configurable : true
   })
@@ -67,10 +103,5 @@ if ('__proto__' in {}) {
     target.__proto__ = proto
   }
 } else {
-  exports.augment = function (target, proto) {
-    Object.getOwnPropertyNames(proto).forEach(function (key) {
-      var descriptor = Object.getOwnPropertyDescriptor(proto, key)
-      Object.defineProperty(target, key, descriptor)
-    })
-  }
+  exports.augment = exports.deepMixin
 }