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

improve data change broadcasting

Evan You 11 лет назад
Родитель
Сommit
58f12db185

+ 1 - 1
src/directives/component.js

@@ -133,7 +133,7 @@ module.exports = {
       }
     }
     if (this.Ctor && !this.childVM) {
-      this.childVM = this.vm._addChild({
+      this.childVM = this.vm.$addChild({
         el: this.el.cloneNode(true),
         _linker: this._linker,
         parent: this.vm

+ 1 - 1
src/directives/if.js

@@ -40,7 +40,7 @@ module.exports = {
   },
 
   build: function () {
-    this.childVM = this.vm._addChild({
+    this.childVM = this.vm.$addChild({
       el: this.el,
       parent: this.vm,
       anonymous: true

+ 1 - 1
src/directives/repeat.js

@@ -240,7 +240,7 @@ module.exports = {
     }
     // resolve constructor
     var Ctor = this.Ctor || this.resolveCtor(data)
-    var vm = this.vm._addChild({
+    var vm = this.vm.$addChild({
       el: this.el.cloneNode(true),
       _linker: this._linker,
       data: data,

+ 24 - 10
src/instance/bindings.js

@@ -14,11 +14,12 @@ exports._initBindings = function () {
   this._bindings = Object.create(null)
   this._bindings.$data = new Binding()
   // setup observer events
+  var update = this._updateBindingAt
   this.$observer
     // simple updates
-    .on('set', updateBindingAt)
-    .on('mutate', updateBindingAt)
-    .on('delete', updateBindingAt)
+    .on('set', update)
+    .on('mutate', update)
+    .on('delete', update)
     // adding properties is a bit different
     .on('add', updateAdd)
     // collect dependency
@@ -29,21 +30,34 @@ exports._initBindings = function () {
  * Trigger update for the binding at given path.
  *
  * @param {String} path
- * @param {String} k - unused
- * @param {*} v - unused
+ * @param {*} v - unused value
+ * @param {*} m - unused mutation
  * @param {Boolean} fromScope
  */
 
-function updateBindingAt (path, k, v, fromScope) {
-  // the '$data' binding updates on any change,
-  // but only if the change is not from parent scopes
+exports._updateBindingAt = function (path, v, m, fromScope) {
   if (!fromScope) {
+    // the '$data' binding updates on any change,
+    // but only if the change is not from parent scopes
     this._bindings.$data._notify()
   }
   var binding = this._bindings[path]
   if (binding) {
-    binding._notify()
+    if (fromScope) {
+      // changes coming from scope upstream, only update
+      // if we don't have a property shadowing it.
+      var i = path.indexOf(Observer.pathDelimiter)
+      var baseKey = i > 0
+        ? path.slice(0, i)
+        : path
+      if (!this.hasOwnProperty(baseKey)) {
+        binding._notify()
+      }
+    } else {
+      binding._notify()
+    }
   }
+  this._notifyChildren(path)
 }
 
 /**
@@ -60,7 +74,7 @@ function updateBindingAt (path, k, v, fromScope) {
 function updateAdd (path) {
   var index = path.lastIndexOf(Observer.pathDelimiter)
   if (index > -1) path = path.slice(0, index)
-  updateBindingAt.call(this, path)
+  this._updateBindingAt(path)
 }
 
 /**

+ 65 - 0
src/instance/children.js

@@ -0,0 +1,65 @@
+var _ = require('../util')
+
+/**
+ * Create a child instance that prototypally inehrits
+ * data on parent. To achieve that we create an intermediate
+ * constructor with its prototype pointing to parent.
+ *
+ * @param {Object} opts
+ * @param {Function} [BaseCtor]
+ * @return {Vue}
+ * @public
+ */
+
+exports.$addChild = function (opts, BaseCtor) {
+  BaseCtor = BaseCtor || _.Vue
+  var ChildVue
+  if (BaseCtor.options.isolated) {
+    ChildVue = BaseCtor
+  } else {
+    var parent = this
+    var ctors = parent._childCtors
+    if (!ctors) {
+      ctors = parent._childCtors = {}
+    }
+    ChildVue = ctors[BaseCtor.cid]
+    if (!ChildVue) {
+      ChildVue = function (options) {
+        this.$parent = parent
+        this.$root = parent.$root || parent
+        this.constructor = ChildVue
+        _.Vue.call(this, options)
+      }
+      ChildVue.options = BaseCtor.options
+      ChildVue.prototype = this
+      ctors[BaseCtor.cid] = ChildVue
+    }
+  }
+  var child = new ChildVue(opts)
+  if (!this._children) {
+    this._children = []
+  }
+  this._children.push(child)
+  return child
+}
+
+/**
+ * Propagate a path update down the scope chain, notifying
+ * all non-isolated child instances.
+ *
+ * @param {String} path
+ */
+
+exports._notifyChildren = function (path) {
+  var children = this._children
+  if (children) {
+    var i = children.length
+    var child
+    while (i--) {
+      child = children[i]
+      if (!child.$options.isolated) {
+        child._updateBindingAt(path, null, null, true)
+      }
+    }
+  }
+}

+ 0 - 19
src/instance/init.js

@@ -70,23 +70,4 @@ exports._init = function (options) {
   if (options.el) {
     this.$mount(options.el)
   }
-}
-
-/**
- * Initialize instance element. Called in the public
- * $mount() method.
- *
- * @param {Element} el
- */
-
-exports._initElement = function (el) {
-  if (el instanceof DocumentFragment) {
-    this._isBlock = true
-    this.$el = this._blockStart = el.firstChild
-    this._blockEnd = el.lastChild
-    this._blockFragment = el
-  } else {
-    this.$el = el
-  }
-  this.$el.__vue__ = this
 }

+ 19 - 0
src/api/lifecycle.js → src/instance/lifecycle.js

@@ -39,6 +39,25 @@ exports.$mount = function (el) {
   }
 }
 
+/**
+ * Initialize instance element. Called in the public
+ * $mount() method.
+ *
+ * @param {Element} el
+ */
+
+exports._initElement = function (el) {
+  if (el instanceof DocumentFragment) {
+    this._isBlock = true
+    this.$el = this._blockStart = el.firstChild
+    this._blockEnd = el.lastChild
+    this._blockFragment = el
+  } else {
+    this.$el = el
+  }
+  this.$el.__vue__ = this
+}
+
 /**
  * Mark an instance as ready.
  */

+ 16 - 96
src/instance/scope.js

@@ -1,8 +1,15 @@
 var _ = require('../util')
 var Emitter = require('../emitter')
 var Observer = require('../observe/observer')
-var scopeEvents = ['set', 'mutate', 'add', 'delete']
-var allEvents = ['get', 'set', 'mutate', 'add', 'delete', 'add:self', 'delete:self']
+var dataEvents = [
+  'get',
+  'set',
+  'mutate',
+  'add',
+  'delete',
+  'add:self',
+  'delete:self'
+]
 
 /**
  * Setup the data scope of an instance.
@@ -22,10 +29,6 @@ exports._initScope = function () {
   this._initData()
   this._initComputed()
   this._initMethods()
-  // listen to parent scope events
-  if (this.$parent && !this.$options.isolated) {
-    this._linkScope()
-  }
 }
 
 /**
@@ -38,18 +41,14 @@ exports._teardownScope = function () {
   // stop relaying data events
   var dataOb = this._data.__ob__
   var proxies = this._dataProxies
-  var i = allEvents.length
+  var i = dataEvents.length
   var event
   while (i--) {
-    event = allEvents[i]
+    event = dataEvents[i]
     dataOb.off(event, proxies[event])
   }
   // unset data reference
   this._data = null
-  // stop propagating parent scope changes
-  if (this._scopeListeners) {
-    this._unlinkScope()
-  }
 }
 
 /**
@@ -61,7 +60,7 @@ exports._initObserver = function () {
   var ob = this.$observer = new Emitter(this)
   // setup data proxy handlers
   var proxies = this._dataProxies = {}
-  allEvents.forEach(function (event) {
+  dataEvents.forEach(function (event) {
     proxies[event] = function (a, b, c) {
       ob.emit(event, a, b, c)
     }
@@ -91,51 +90,13 @@ exports._initData = function () {
   var ob = Observer.create(data)
   var proxies = this._dataProxies
   var event
-  i = allEvents.length
+  i = dataEvents.length
   while (i--) {
-    event = allEvents[i]
+    event = dataEvents[i]
     ob.on(event, proxies[event])
   }
 }
 
-/**
- * Listen to parent scope's events
- */
-
-exports._linkScope = function () {
-  var self = this
-  var ob = this.$observer
-  var pob = this.$parent.$observer
-  var listeners = this._scopeListeners = {}
-  scopeEvents.forEach(function (event) {
-    var cb = listeners[event] = function (key, a, b) {
-      // since these events come from upstream,
-      // we only emit them if we don't have the same keys
-      // shadowing them in current scope.
-      if (!self.hasOwnProperty(key)) {
-        ob.emit(event, key, a, b, true)
-      }
-    }
-    pob.on(event, cb)
-  })
-}
-
-/**
- * Stop listening to parent scope events
- */
-
-exports._unlinkScope = function () {
-  var pob = this.$parent.$observer
-  var listeners = this._scopeListeners
-  var i = scopeEvents.length
-  var event
-  while (i--) {
-    event = scopeEvents[i]
-    pob.off(event, listeners[event])
-  }
-  this._scopeListeners = null
-}
-
 /**
  * Swap the isntance's $data. Called in $data's setter.
  *
@@ -179,9 +140,9 @@ exports._setData = function (newData) {
   var oldOb = oldData.__ob__
   var proxies = this._dataProxies
   var event, proxy
-  i = allEvents.length
+  i = dataEvents.length
   while (i--) {
-    event = allEvents[i]
+    event = dataEvents[i]
     proxy = proxies[event]
     newOb.on(event, proxy)
     oldOb.off(event, proxy)
@@ -267,47 +228,6 @@ exports._initMethods = function () {
   }
 }
 
-/**
- * Create a child instance that prototypally inehrits
- * data on parent. To achieve that we create an intermediate
- * constructor with its prototype pointing to parent.
- *
- * @param {Object} opts
- * @param {Function} [BaseCtor]
- */
-
-exports._addChild = function (opts, BaseCtor) {
-  BaseCtor = BaseCtor || _.Vue
-  var ChildVue
-  if (BaseCtor.options.isolated) {
-    ChildVue = BaseCtor
-  } else {
-    var parent = this
-    var ctors = parent._childCtors
-    if (!ctors) {
-      ctors = parent._childCtors = {}
-    }
-    ChildVue = ctors[BaseCtor.cid]
-    if (!ChildVue) {
-      ChildVue = function (options) {
-        this.$parent = parent
-        this.$root = parent.$root || parent
-        this.constructor = ChildVue
-        _.Vue.call(this, options)
-      }
-      ChildVue.options = BaseCtor.options
-      ChildVue.prototype = this
-      ctors[BaseCtor.cid] = ChildVue
-    }
-  }
-  var child = new ChildVue(opts)
-  if (!this._children) {
-    this._children = []
-  }
-  this._children.push(child)
-  return child
-}
-
 /**
  * Define a meta property, e.g $index, $key, $value
  * which only exists on the vm instance but not in $data.

+ 2 - 1
src/vue.js

@@ -70,6 +70,8 @@ extend(p, require('./instance/init'))
 extend(p, require('./instance/events'))
 extend(p, require('./instance/scope'))
 extend(p, require('./instance/bindings'))
+extend(p, require('./instance/children'))
+extend(p, require('./instance/lifecycle'))
 
 /**
  * Mixin public API methods
@@ -78,6 +80,5 @@ extend(p, require('./instance/bindings'))
 extend(p, require('./api/data'))
 extend(p, require('./api/dom'))
 extend(p, require('./api/events'))
-extend(p, require('./api/lifecycle'))
 
 module.exports = _.Vue = Vue

+ 37 - 47
test/unit/specs/instance/scope_spec.js

@@ -7,6 +7,12 @@ var Vue = require('../../../../src/vue')
 var Observer = require('../../../../src/observe/observer')
 Observer.pathDelimiter = '.'
 
+function mockBinding () {
+  return {
+    _notify: jasmine.createSpy('binding')
+  }
+}
+
 describe('Scope', function () {
   
   describe('basic', function () {
@@ -116,7 +122,7 @@ describe('Scope', function () {
       }
     })
 
-    var child = parent._addChild({
+    var child = parent.$addChild({
       data: {
         a: 'child a'
       }
@@ -137,43 +143,31 @@ describe('Scope', function () {
       expect(parent.c).toBe('modified by child')
     })
 
-    it('events on parent should propagate down to child', function () {
-      // when a shadowed property changed on parent scope,
-      // the event should NOT be propagated down
-      var spy = jasmine.createSpy('inheritance')
-      child.$observer.on('set', spy)
-      parent.c = 'c changed'
-      expect(spy.calls.count()).toBe(1)
-      expect(spy).toHaveBeenCalledWith('c', 'c changed', undefined, true)
-
-      spy = jasmine.createSpy('inheritance')
-      child.$observer.on('add', spy)
+    it('parent event should propagate when child has same binding', function () {
+      // object path
+      var b = child._bindings['b.c'] = mockBinding()
+      parent.b.c = 3
+      expect(b._notify).toHaveBeenCalled()
+      // array path
+      b = child._bindings['arr.0.a'] = mockBinding()
+      parent.arr[0].a = 2
+      expect(b._notify).toHaveBeenCalled()
+      // add
+      b = child._bindings['e'] = mockBinding()
       parent.$add('e', 123)
-      expect(spy.calls.count()).toBe(1)
-      expect(spy).toHaveBeenCalledWith('e', 123, undefined, true)
-
-      spy = jasmine.createSpy('inheritance')
-      child.$observer.on('delete', spy)
+      expect(b._notify).toHaveBeenCalled()
+      // delete
+      b = child._bindings['e'] = mockBinding()
       parent.$delete('e')
-      expect(spy.calls.count()).toBe(1)
-      expect(spy).toHaveBeenCalledWith('e', undefined, undefined, true)
-
-      spy = jasmine.createSpy('inheritance')
-      child.$observer.on('mutate', spy)
-      parent.arr.reverse()
-      expect(spy.calls.mostRecent().args[0]).toBe('arr')
-      expect(spy.calls.mostRecent().args[1]).toBe(parent.arr)
-      expect(spy.calls.mostRecent().args[2].method).toBe('reverse')
-
+      expect(b._notify).toHaveBeenCalled()
     })
 
-    it('shadowed properties change on parent should not propagate down', function () {
-      // when a shadowed property changed on parent scope,
-      // the event should NOT be propagated down
-      var spy = jasmine.createSpy('inheritance')
-      child.$observer.on('set', spy)
-      parent.a = 'a changed'
-      expect(spy.calls.count()).toBe(0)
+    it('parent event should not propagate when child has shadowing key', function () {
+      var b = child._bindings['c'] = mockBinding()
+      child.$add('c', 123)
+      expect(b._notify.calls.count()).toBe(1)
+      parent.c = 456
+      expect(b._notify.calls.count()).toBe(1)
     })
 
   })
@@ -186,27 +180,23 @@ describe('Scope', function () {
       }
     })
 
-    var child = parent._addChild({
+    var child = parent.$addChild({
       data: parent.arr[0]
     })
 
     it('should trigger proper events', function () {
       
-      var parentSpy = jasmine.createSpy('parent')
-      var childSpy = jasmine.createSpy('child')
-      parent.$observer.on('set', parentSpy)
-      child.$observer.on('set', childSpy)
+      var parentSpy = parent._bindings['arr.0.a'] = mockBinding()
+      var childSpy = child._bindings['arr.0.a'] = mockBinding()
+      var childSpy2 = child._bindings['a'] = mockBinding()
       child.a = 3
 
       // make sure data sync is working
       expect(parent.arr[0].a).toBe(3)
 
-      expect(parentSpy.calls.count()).toBe(1)
-      expect(parentSpy).toHaveBeenCalledWith('arr.0.a', 3, undefined, undefined)
-
-      expect(childSpy.calls.count()).toBe(2)
-      expect(childSpy).toHaveBeenCalledWith('a', 3, undefined, undefined)
-      expect(childSpy).toHaveBeenCalledWith('arr.0.a', 3, undefined, true)
+      expect(parentSpy._notify).toHaveBeenCalled()
+      expect(childSpy._notify).toHaveBeenCalled()
+      expect(childSpy2._notify).toHaveBeenCalled()
     })
 
   })
@@ -307,7 +297,7 @@ describe('Scope', function () {
     })
 
     it('inherit', function () {
-      var child = vm._addChild()
+      var child = vm.$addChild()
       expect(child.c).toBe('cd')
 
       child.d = 'e f'
@@ -339,7 +329,7 @@ describe('Scope', function () {
       })
       expect(vm.test()).toBe(1)
 
-      var child = vm._addChild()
+      var child = vm.$addChild()
       expect(child.test()).toBe(1)
     })
 

+ 1 - 1
test/unit/specs/watcher_spec.js

@@ -180,7 +180,7 @@ describe('Watcher', function () {
   })
 
   it('watching parent scope properties', function (done) {
-    var child = vm._addChild()
+    var child = vm.$addChild()
     var spy2 = jasmine.createSpy('watch')
     var watcher1 = new Watcher(child, '$data', spy)
     var watcher2 = new Watcher(child, 'a', spy2)