Skip to content

Commit

Permalink
optimize data event scope propagation
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Sep 2, 2014
1 parent 1667736 commit d6c5190
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 56 deletions.
4 changes: 2 additions & 2 deletions component.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
],
"license": "MIT",
"scripts": [
"src/api/child.js",
"src/api/data.js",
"src/api/dom.js",
"src/api/events.js",
"src/api/global.js",
"src/api/lifecycle.js",
"src/batcher.js",
"src/binding.js",
"src/cache.js",
Expand Down Expand Up @@ -47,10 +49,8 @@
"src/filters/array-filters.js",
"src/filters/index.js",
"src/instance/bindings.js",
"src/instance/children.js",
"src/instance/events.js",
"src/instance/init.js",
"src/instance/lifecycle.js",
"src/instance/scope.js",
"src/observe/array-augmentations.js",
"src/observe/object-augmentations.js",
Expand Down
21 changes: 0 additions & 21 deletions src/instance/children.js → src/api/child.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,4 @@ exports.$addChild = function (opts, BaseCtor) {
}
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)
}
}
}
}
12 changes: 9 additions & 3 deletions src/instance/lifecycle.js → src/api/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,22 @@ exports.$destroy = function (remove) {
}
// teardown data/scope
this._teardownScope()
// teardown all user watchers.
for (i in this._userWatchers) {
this._userWatchers[i].teardown()
// teardown bindings.
// this sets all binding's sub count to 0 so only user
// watchers will need to do a splice() in teardown
for (i in this._bindings) {
this._teardownBindingAt(i)
}
// teardown all directives. this also tearsdown all
// directive-owned watchers.
i = this._directives.length
while (i--) {
this._directives[i]._teardown()
}
// teardown all user watchers.
for (i in this._userWatchers) {
this._userWatchers[i].teardown()
}
// clean up
this._data =
this._watchers =
Expand Down
4 changes: 3 additions & 1 deletion src/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ p._addSub = function (sub) {
*/

p._removeSub = function (sub) {
this._subs.splice(this._subs.indexOf(sub), 1)
if (this._subs.length) {
this._subs.splice(this._subs.indexOf(sub), 1)
}
}

/**
Expand Down
66 changes: 65 additions & 1 deletion src/instance/bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,71 @@ exports._updateBindingAt = function (path, v, m, fromScope) {
binding._notify()
}
}
this._notifyChildren(path)
// only broadcast if there are children actually listening
// on this path
if (this._childBindingCount[path]) {
this._notifyChildren(path)
}
}

/**
* 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)
}
}
}
}

/**
* Create a binding at a given path, and goes up the scope
* chain to increase each parent's binding count at this
* path by 1. This allows us to easily determine whether we
* need to traverse down the scope to broadcast a data
* change. A similar technique is used in Angular's
* $broadcast implementation.
*
* @param {String} path
* @return {Binding}
*/

exports._createBindingAt = function (path) {
var binding = this._bindings[path] = new Binding()
var parent = this.$parent
var count
while (parent) {
count = parent._childBindingCount
count[path] = (count[path] || 0) + 1
parent = parent.$parent
}
return binding
}

/**
* Teardown a binding.
*
* @param {String} path
*/

exports._teardownBindingAt = function (path) {
this._bindings[path]._subs.length = 0
var parent = this.$parent
while (parent) {
parent._childBindingCount[path]--
parent = parent.$parent
}
}

/**
Expand Down
17 changes: 9 additions & 8 deletions src/instance/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ exports._init = function (options) {

options = options || {}

this.$el = null
this.$ = {}
this.$root = this.$root || this
this._emitter = new Emitter(this)
this._watchers = Object.create(null)
this._userWatchers = Object.create(null)
this._bindings = Object.create(null)
this._directives = []
this.$el = null
this.$ = {}
this.$root = this.$root || this
this._emitter = new Emitter(this)
this._watchers = Object.create(null)
this._userWatchers = Object.create(null)
this._bindings = Object.create(null)
this._childBindingCount = Object.create(null)
this._directives = []

// block instance properties
this._blockStart =
Expand Down
4 changes: 2 additions & 2 deletions src/vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ 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
Expand All @@ -80,5 +78,7 @@ extend(p, require('./instance/lifecycle'))
extend(p, require('./api/data'))
extend(p, require('./api/dom'))
extend(p, require('./api/events'))
extend(p, require('./api/child'))
extend(p, require('./api/lifecycle'))

module.exports = _.Vue = Vue
3 changes: 1 addition & 2 deletions src/watcher.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
var _ = require('./util')
var Observer = require('./observe/observer')
var expParser = require('./parse/expression')
var Binding = require('./binding')
var Batcher = require('./batcher')

var batcher = new Batcher()
Expand Down Expand Up @@ -110,7 +109,7 @@ p.addDep = function (path) {
if (!oldDeps[path]) {
var binding =
vm._bindings[path] ||
(vm._bindings[path] = new Binding())
vm._createBindingAt(path)
binding._addSub(this)
}
}
Expand Down
41 changes: 25 additions & 16 deletions test/unit/specs/instance/scope_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ 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 () {
Expand Down Expand Up @@ -145,31 +139,44 @@ describe('Scope', function () {

it('parent event should propagate when child has same binding', function () {
// object path
var b = child._bindings['b.c'] = mockBinding()
var b = child._createBindingAt('b.c')
b._notify = jasmine.createSpy('binding')
parent.b.c = 3
expect(b._notify).toHaveBeenCalled()
// array path
b = child._bindings['arr.0.a'] = mockBinding()
var b = child._createBindingAt('arr.0.a')
b._notify = jasmine.createSpy('binding')
parent.arr[0].a = 2
expect(b._notify).toHaveBeenCalled()
// add
b = child._bindings['e'] = mockBinding()
var b = child._createBindingAt('e')
b._notify = jasmine.createSpy('binding')
parent.$add('e', 123)
expect(b._notify).toHaveBeenCalled()
// delete
b = child._bindings['e'] = mockBinding()
parent.$delete('e')
expect(b._notify).toHaveBeenCalled()
expect(b._notify.calls.count()).toBe(2)
})

it('parent event should not propagate when child has shadowing key', function () {
var b = child._bindings['c'] = mockBinding()
var b = child._createBindingAt('c')
b._notify = jasmine.createSpy('binding')
child.$add('c', 123)
expect(b._notify.calls.count()).toBe(1)
parent.c = 456
expect(b._notify.calls.count()).toBe(1)
})

it('parent event should not even traverse if child doesn\'t have changed binding', function () {
parent._notifyChildren = jasmine.createSpy()
parent.c = 123
expect(parent._notifyChildren.calls.count()).toBe(1)
// should not be called after binding teardown
child._teardownBindingAt('c')
parent.c = 234
expect(parent._notifyChildren.calls.count()).toBe(1)
})

})

describe('inheritance with data sync on parent data', function () {
Expand All @@ -185,10 +192,12 @@ describe('Scope', function () {
})

it('should trigger proper events', function () {

var parentSpy = parent._bindings['arr.0.a'] = mockBinding()
var childSpy = child._bindings['arr.0.a'] = mockBinding()
var childSpy2 = child._bindings['a'] = mockBinding()
var parentSpy = parent._createBindingAt('arr.0.a')
parentSpy._notify = jasmine.createSpy('binding')
var childSpy = child._createBindingAt('arr.0.a')
childSpy._notify = jasmine.createSpy('binding')
var childSpy2 = child._createBindingAt('a')
childSpy2._notify = jasmine.createSpy('binding')
child.a = 3

// make sure data sync is working
Expand Down

0 comments on commit d6c5190

Please sign in to comment.