From 0886716b2d8a3a3208d3b2069583c93e4582da5c Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 28 Aug 2014 19:02:32 -0400 Subject: [PATCH] watcher multiple callbacks --- component.json | 3 ++- src/api/data.js | 28 ++++++++++++--------- src/directive.js | 40 +++++++++++++++++++++--------- src/instance/init.js | 17 +++++++------ src/instance/lifecycle.js | 10 +++++--- src/watcher.js | 44 ++++++++++++++++++++++++++++++--- test/unit/specs/watcher_spec.js | 42 +++++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 39 deletions(-) diff --git a/component.json b/component.json index 1f63811fe..0a4f252d7 100644 --- a/component.json +++ b/component.json @@ -15,7 +15,6 @@ "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", @@ -44,8 +43,10 @@ "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", diff --git a/src/api/data.js b/src/api/data.js index 6460eabbe..791f98bc7 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -62,35 +62,41 @@ exports.$delete = function (key) { /** * Watch an expression, trigger callback when its - * value changes. Returns the created watcher's - * id so it can be teardown later. + * value changes. * * @param {String} exp * @param {Function} cb * @param {Boolean} [immediate] - * @return {Number} */ exports.$watch = function (exp, cb, immediate) { - var watcher = new Watcher(this, exp, cb, this) - this._watchers[watcher.id] = watcher + var watcher = this._userWatchers[exp] + if (!watcher) { + watcher = + this._userWatchers[exp] = + new Watcher(this, exp, cb, this) + } else { + watcher.addCb(cb, this) + } if (immediate) { cb.call(this, watcher.value) } - return watcher.id } /** * Teardown a watcher with given id. * - * @param {Number} id + * @param {String} exp + * @param {Function} cb */ -exports.$unwatch = function (id) { - var watcher = this._watchers[id] +exports.$unwatch = function (exp, cb) { + var watcher = this._userWatchers[exp] if (watcher) { - watcher.teardown() - this._watchers[id] = null + watcher.removeCb(cb) + if (!watcher.active) { + this._userWatchers[exp] = null + } } } diff --git a/src/directive.js b/src/directive.js index 430289d10..90e4481c6 100644 --- a/src/directive.js +++ b/src/directive.js @@ -47,7 +47,11 @@ var p = Directive.prototype */ p._bind = function (def) { - _.extend(this, def) + if (typeof def === 'function') { + this.update = def + } else { + _.extend(this, def) + } this._watcherExp = this.expression this._checkDynamicLiteral() if (this.bind) { @@ -58,15 +62,22 @@ p._bind = function (def) { (!this.isLiteral || this._isDynamicLiteral) ) { if (!this._checkExpFn()) { - this._watcher = new Watcher( - this.vm, - this._watcherExp, - this._update, // callback - this, // callback context - this.filters, - this.twoWay // need setter - ) - this.update(this._watcher.value) + var exp = this._watcherExp + var wathcer = this.vm._watchers[exp] + if (!wathcer) { + watcher = this.vm._watchers[exp] = new Watcher( + this.vm, + exp, + this._update, // callback + this, // callback context + this.filters, + this.twoWay // need setter + ) + } else { + watcher.addCb(this._update, this) + } + this._watcher = watcher + this.update(watcher.value) } } this._bound = true @@ -149,10 +160,15 @@ p._teardown = function () { if (this.unbind) { this.unbind() } - if (this._watcher) { - this._watcher.teardown() + var watcher = this._watcher + if (watcher) { + watcher.removeCb(this._update) + if (!watcher.active) { + this.vm._watchers[this.expression] = null + } } this._bound = false + this.vm = this.el = null } } diff --git a/src/instance/init.js b/src/instance/init.js index 281eb185d..1d126a589 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -16,14 +16,15 @@ exports._init = function (options) { options = options || {} - this.$el = null - this.$ = {} - this.$root = this.$root || this - this._data = options.data || {} - this._emitter = new Emitter(this) - this._watchers = {} - this._directives = [] - this._rawContent = null + this.$el = null + this.$ = {} + this.$root = this.$root || this + this._data = options.data || {} + this._emitter = new Emitter(this) + this._watchers = {} + this._userWatchers = {} + this._directives = [] + this._rawContent = null this._activeWatcher = null // block instance properties diff --git a/src/instance/lifecycle.js b/src/instance/lifecycle.js index d2f7d73a4..f88e27fb9 100644 --- a/src/instance/lifecycle.js +++ b/src/instance/lifecycle.js @@ -107,9 +107,12 @@ exports.$destroy = function (remove) { } // teardown data/scope this._teardownScope() - // teardown all user watchers - for (var id in this._watchers) { - this.$unwatch(id) + // teardown all watchers + for (i in this._watchers) { + this._watchers[i].teardown() + } + for (i in this._userWatchers) { + this._userWatchers[i].teardown() } // teardown all directives i = this._directives.length @@ -119,6 +122,7 @@ exports.$destroy = function (remove) { // clean up this._children = this._watchers = + this._userWatchers = this._activeWatcher = this.$el = this.$el.__vue__ = diff --git a/src/watcher.js b/src/watcher.js index d2e16287a..501ec610b 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -15,7 +15,7 @@ var uid = 0 * @param {Vue} vm * @param {String} expression * @param {Function} cb - * @param {Object} [ctx] + * @oaram {Object} ctx * @param {Array} [filters] * @param {Boolean} [needSet] * @constructor @@ -24,8 +24,8 @@ var uid = 0 function Watcher (vm, expression, cb, ctx, filters, needSet) { this.vm = vm this.expression = expression - this.cb = cb // change callback - this.ctx = ctx || vm // change callback context + this.cbs = [cb] + this.ctxs = [ctx] this.id = ++uid // uid for batching this.value = undefined this.active = true @@ -173,11 +173,46 @@ p.run = function () { ) { var oldValue = this.value this.value = value - this.cb.call(this.ctx, value, oldValue) + var cbs = this.cbs + var ctxs = this.ctxs + for (var i = 0, l = cbs.length; i < l; i++) { + cbs[i].call(ctxs[i], value, oldValue) + } } } } +/** + * Add a callback. + * + * @param {Function} cb + * @param {Object} ctx + */ + +p.addCb = function (cb, ctx) { + this.cbs.push(cb) + this.ctxs.push(ctx) +} + +/** + * Remove a callback. + * + * @param {Function} cb + */ + +p.removeCb = function (cb) { + var cbs = this.cbs + if (cbs.length > 1) { + var i = cbs.indexOf(cb) + if (i > -1) { + cbs.splice(i, 1) + this.ctxs.splice(i, 1) + } + } else if (cb === cbs[0]) { + this.teardown() + } +} + /** * Remove self from all dependencies' subcriber list. */ @@ -189,6 +224,7 @@ p.teardown = function () { for (var path in this.deps) { vm._bindings[path]._removeSub(this) } + this.vm = this.cbs = this.ctxs = null } } diff --git a/test/unit/specs/watcher_spec.js b/test/unit/specs/watcher_spec.js index 65e0e1504..4101ae723 100644 --- a/test/unit/specs/watcher_spec.js +++ b/test/unit/specs/watcher_spec.js @@ -265,12 +265,54 @@ describe('Watcher', function () { }) }) + it('add callback', function (done) { + var ctx1 = {} + var ctx2 = {} + var watcher = new Watcher(vm, 'a', function () { + this.called = 1 + }, ctx1) + watcher.addCb(function () { + this.called = 2 + }, ctx2) + vm.a = 99 + nextTick(function () { + expect(ctx1.called).toBe(1) + expect(ctx2.called).toBe(2) + done() + }) + }) + + it('remove callback', function (done) { + // single, should equal teardown + var fn = function () {} + var watcher = new Watcher(vm, 'a', fn) + watcher.removeCb(fn) + expect(watcher.active).toBe(false) + expect(watcher.vm).toBe(null) + expect(watcher.cbs).toBe(null) + expect(watcher.ctxs).toBe(null) + // multiple + watcher = new Watcher(vm, 'a', spy) + var spy2 = jasmine.createSpy() + watcher.addCb(spy2) + watcher.removeCb(spy) + vm.a = 234 + nextTick(function () { + expect(spy.calls.count()).toBe(0) + expect(spy2).toHaveBeenCalledWith(234, 1) + done() + }) + }) + it('teardown', function (done) { var watcher = new Watcher(vm, 'b.c', spy) watcher.teardown() vm.b.c = 3 nextTick(function () { expect(watcher.active).toBe(false) + expect(watcher.vm).toBe(null) + expect(watcher.cbs).toBe(null) + expect(watcher.ctxs).toBe(null) expect(spy.calls.count()).toBe(0) done() })