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

+ 1 - 1
changes.md

@@ -131,7 +131,7 @@ You can also pass in `isolated: true` to avoid inheriting a parent scope, which
 
   - #### hook usage change: `created`
 
-    This is now called before anything happens to the instance, with only `this.$data` being available, but **not observed** yet. In the past you can do `this.something = 1` to define default data, but it required some weird hack to make it work. Now you should just explicitly do `this.$data.something = 1` to define your instance default data.
+    In the past, you could do `this.something = 1` inside the `created` hook to add observed data to the instance. Now the hook is called after the data observation, so if you wish to add additional data to the instance you should use the new `$add` and `$delete` API methods.
 
   - #### hook usage change: `ready`
 

+ 7 - 5
src/api/lifecycle.js

@@ -17,16 +17,17 @@ exports.$mount = function (el) {
     _.warn('$mount() should be called only once.')
     return
   }
-  this._callHook('beforeCompile')
   var options = this.$options
   if (options._linker) {
     // pre-compiled linker. this means the element has
     // been trancluded and compiled. just link it.
     this._initElement(el)
+    this._callHook('beforeCompile')
     options._linker(this, el)
   } else {
     el = transclude(el, options)
     this._initElement(el)
+    this._callHook('beforeCompile')
     var linker = compile(el, options)
     linker(this, el)
   }
@@ -83,7 +84,7 @@ exports.$destroy = function (remove) {
   }
   this._callHook('beforeDestroy')
   // remove DOM element
-  if (remove) {
+  if (remove && this.$el) {
     if (this.$el === document.body) {
       this.$el.innerHTML = ''
     } else {
@@ -116,20 +117,21 @@ exports.$destroy = function (remove) {
     this._userWatchers[i].teardown()
   }
   // clean up
+  if (this.$el) {
+    this.$el.__vue__ = null
+  }
   this._data =
   this._watchers =
   this._userWatchers =
   this._activeWatcher =
   this.$el =
-  this.$el.__vue__ =
   this.$parent =
-  this.$observer =
   this._children =
   this._bindings =
   this._directives = null
   // call the last hook...
   this._isDestroyed = true
-  this._callHook('afterDestroy')
+  this._callHook('destroyed')
   // turn off all instance listeners.
   this.$off()
 }

+ 34 - 6
src/instance/events.js

@@ -1,4 +1,5 @@
-var inDoc = require('../util').inDoc
+var _ = require('../util')
+var inDoc = _.inDoc
 
 /**
  * Setup the instance's option events.
@@ -14,16 +15,43 @@ exports._initEvents = function () {
     var handlers, e, i, j
     for (e in events) {
       handlers = events[e]
-      for (i = 0, j = handlers.length; i < j; i++) {
-        var handler = typeof handlers[i] === 'string'
-          ? methods && methods[handlers[i]]
-          : handlers[i]
-        this.$on(e, handler)
+      if (_.isArray(handlers)) {
+        for (i = 0, j = handlers.length; i < j; i++) {
+          register(this, e, handlers[i], methods)
+        }
+      } else {
+        register(this, e, handlers, methods)
       }
     }
   }
 }
 
+/**
+ * Helper to register an event.
+ *
+ * @param {Vue} vm
+ * @param {String} event
+ * @param {*} handler
+ * @param {Object|undefined} methods
+ */
+
+function register (vm, event, handler, methods) {
+  var type = typeof handler
+  if (type === 'function') {
+    vm.$on(event, handler)
+  } else if (type === 'string') {
+    var method = methods && methods[handler]
+    if (method) {
+      vm.$on(event, method)
+    } else {
+      _.warn(
+        'Unknown method: "' + handler + '" when ' +
+        'registering callback for event: "' + event + '".'
+      )
+    }
+  }
+}
+
 /**
  * Setup recursive attached/detached calls
  */

+ 4 - 6
src/instance/init.js

@@ -46,8 +46,7 @@ exports._init = function (options) {
   this._children =
   this._childCtors = null
 
-  // anonymous instances are created by flow-control
-  // directives such as v-if and v-repeat
+  // anonymous instances are created by v-if
   this._isAnonymous = options._anonymous
 
   // merge options.
@@ -63,13 +62,12 @@ exports._init = function (options) {
   // setup event system and option events.
   this._initEvents()
 
-  // the `created` hook is called after basic properties
-  // have been set up & before data observation happens.
-  this._callHook('created')
-
   // initialize data observation and scope inheritance.
   this._initScope()
 
+  // call created hook
+  this._callHook('created')
+
   // if `el` option is passed, start compilation.
   if (options.el) {
     this.$mount(options.el)

+ 24 - 27
src/instance/scope.js

@@ -3,16 +3,11 @@ var Observer = require('../observer')
 var Binding = require('../binding')
 
 /**
- * Setup the data scope of an instance.
- *
- * We need to setup the instance $observer, which emits
- * data change events. The $observer relays events from
- * the $data's observer, because $data might be swapped
- * and the data observer might change.
- *
- * If the instance has a parent and is not isolated, we
- * also need to listen to parent scope events and propagate
- * changes down here.
+ * Setup the scope of an instance, which contains:
+ * - observed data
+ * - computed properties
+ * - user methods
+ * - meta properties
  */
 
 exports._initScope = function () {
@@ -31,8 +26,12 @@ exports._initData = function () {
   var data = this._data
   var keys = Object.keys(data)
   var i = keys.length
+  var key
   while (i--) {
-    this._proxy(keys[i])
+    key = keys[i]
+    if (!_.isReserved(key)) {
+      this._proxy(key)
+    }
   }
   // observe data
   Observer.create(data)
@@ -80,22 +79,20 @@ exports._setData = function (newData) {
  */
 
 exports._proxy = function (key) {
-  if (!_.isReserved(key)) {
-    // need to store ref to self here
-    // because these getter/setters might
-    // be called by child instances!
-    var self = this
-    Object.defineProperty(self, key, {
-      configurable: true,
-      enumerable: true,
-      get: function proxyGetter () {
-        return self._data[key]
-      },
-      set: function proxySetter (val) {
-        self._data[key] = val
-      }
-    })
-  }
+  // need to store ref to self here
+  // because these getter/setters might
+  // be called by child instances!
+  var self = this
+  Object.defineProperty(self, key, {
+    configurable: true,
+    enumerable: true,
+    get: function proxyGetter () {
+      return self._data[key]
+    },
+    set: function proxySetter (val) {
+      self._data[key] = val
+    }
+  })
 }
 
 /**

+ 4 - 4
test/unit/specs/api/events_spec.js

@@ -33,7 +33,7 @@ describe('Events API', function () {
     vm.$off()
     vm.$emit('test1')
     vm.$emit('test2')
-    expect(spy.calls.count()).toBe(0)
+    expect(spy).not.toHaveBeenCalled()
   })
 
   it('$off event', function () {
@@ -53,7 +53,7 @@ describe('Events API', function () {
     vm.$on('test', spy2)
     vm.$off('test', spy)
     vm.$emit('test', 1, 2, 3)
-    expect(spy.calls.count()).toBe(0)
+    expect(spy).not.toHaveBeenCalled()
     expect(spy2.calls.count()).toBe(1)
     expect(spy2).toHaveBeenCalledWith(1, 2, 3)
   })
@@ -93,7 +93,7 @@ describe('Events API', function () {
     })
     child2.$on('test', spy)
     vm.$broadcast('test')
-    expect(spy.calls.count()).toBe(0)
+    expect(spy).not.toHaveBeenCalled()
   })
 
   it('$dispatch', function () {
@@ -113,7 +113,7 @@ describe('Events API', function () {
     })
     vm.$on('test', spy)
     child2.$dispatch('test')
-    expect(spy.calls.count()).toBe(0)
+    expect(spy).not.toHaveBeenCalled()
   })
 
 })

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

@@ -47,7 +47,7 @@ describe('Batcher', function () {
       override: true
     })
     nextTick(function () {
-      expect(spy.calls.count()).toBe(0)
+      expect(spy).not.toHaveBeenCalled()
       expect(spy2.calls.count()).toBe(1)
       done()
     })

+ 173 - 0
test/unit/specs/instance/events_spec.js

@@ -0,0 +1,173 @@
+var Vue = require('../../../../src/vue')
+
+describe('Instance Events', function () {
+
+  describe('events', function () {
+
+    var spy
+    beforeEach(function () {
+      spy = jasmine.createSpy()
+    })
+
+    it('normal events', function () {
+      var vm = new Vue({
+        events: {
+          test: spy,
+          test2: [spy, spy]
+        }
+      })
+      vm.$emit('test', 123)
+      expect(spy).toHaveBeenCalledWith(123)
+      vm.$emit('test2')
+      expect(spy.calls.count()).toBe(3)
+    })
+
+    it('hook events', function () {
+      var vm = new Vue({
+        events: {
+          'hook:created': spy
+        }
+      })
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('method name strings', function () {
+      var vm = new Vue({
+        events: {
+          'test': 'doSomething'
+        },
+        methods: {
+          doSomething: spy
+        }
+      })
+      vm.$emit('test', 123)
+      expect(spy).toHaveBeenCalledWith(123)
+    })
+
+  })
+
+  describe('hooks', function () {
+
+    var spy
+    beforeEach(function () {
+      spy = jasmine.createSpy()
+    })
+    
+    it('created', function () {
+      var ctx
+      var vm = new Vue({
+        created: function () {
+          // can't assert this === vm here
+          // because the constructor hasn't returned yet
+          ctx = this
+          // should have observed data
+          expect(this._data.__ob__).toBeTruthy()
+          spy()
+        }
+      })
+      expect(ctx).toBe(vm)
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('beforeDestroy', function () {
+      var vm = new Vue({
+        beforeDestroy: function () {
+          expect(this).toBe(vm)
+          expect(this._isDestroyed).toBe(false)
+          spy()
+        }
+      })
+      vm.$destroy()
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('destroyed', function () {
+      var vm = new Vue({
+        destroyed: function () {
+          expect(this).toBe(vm)
+          expect(this._isDestroyed).toBe(true)
+          expect(this._data).toBeNull()
+          spy()
+        }
+      })
+      vm.$destroy()
+      expect(spy).toHaveBeenCalled()
+    })
+
+    if (Vue.util.inBrowser) {
+
+      it('beforeCompile', function () {
+        var vm = new Vue({
+          template: '{{a}}',
+          data: { a: 1 },
+          beforeCompile: function () {
+            expect(this).toBe(vm)
+            expect(this.$el).toBe(el)
+            expect(this.$el.textContent).toBe('{{a}}')
+            spy()
+          }
+        })
+        var el = document.createElement('div')
+        vm.$mount(el)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('compiled', function () {
+        var vm = new Vue({
+          template: '{{a}}',
+          data: { a: 1 },
+          compiled: function () {
+            expect(this.$el).toBe(el)
+            expect(this.$el.textContent).toBe('1')
+            spy()
+          }
+        })
+        var el = document.createElement('div')
+        vm.$mount(el)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('ready', function () {
+        var vm = new Vue({
+          ready: spy
+        })
+        expect(spy).not.toHaveBeenCalled()
+        var el = document.createElement('div')
+        vm.$mount(el)
+        expect(spy).not.toHaveBeenCalled()
+        vm.$appendTo(document.body)
+        expect(spy).toHaveBeenCalled()
+        vm.$remove()
+        // try mounting on something already in dom
+        el = document.createElement('div')
+        document.body.appendChild(el)
+        var spy2 = jasmine.createSpy()
+        vm = new Vue({
+          el: el,
+          ready: spy2
+        })
+        expect(spy2).toHaveBeenCalled()
+        vm.$remove()
+      })
+
+      describe('attached/detached', function () {
+
+        it('in DOM', function () {
+          // body...
+        })
+
+        it('create then attach', function () {
+          // body...
+        })
+
+        it('should not fire on detached child', function () {
+          // body...
+        })
+
+      })
+
+    }
+
+  })
+
+})

+ 52 - 0
test/unit/specs/instance/init_spec.js

@@ -0,0 +1,52 @@
+var init = require('../../../../src/instance/init')._init
+
+describe('Instance Init', function () {
+
+  var stub = {
+    constructor: {
+      options: { a: 1, b: 2 }
+    },
+    _initEvents: jasmine.createSpy(),
+    _callHook: jasmine.createSpy(),
+    _initScope: jasmine.createSpy(),
+    $mount: jasmine.createSpy()
+  }
+
+  init.call(stub, {
+    a: 2,
+    _anonymous: true,
+    el: {}
+  })
+
+  it('should setup properties', function () {
+    expect(stub.$el).toBe(null)
+    expect(stub.$root).toBe(stub)
+    expect(stub.$).toBeTruthy()
+    expect(stub._watcherList).toBeTruthy()
+    expect(stub._watchers).toBeTruthy()
+    expect(stub._userWatchers).toBeTruthy()
+    expect(stub._directives).toBeTruthy()
+    expect(stub._events).toBeTruthy()
+    expect(stub._eventsCount).toBeTruthy()
+    expect(stub._isAnonymous).toBe(true)
+  })
+
+  it('should merge options', function () {
+    expect(stub.$options.a).toBe(2)
+    expect(stub.$options.b).toBe(2)
+  })
+
+  it('should call other init methods', function () {
+    expect(stub._initEvents).toHaveBeenCalled()
+    expect(stub._initScope).toHaveBeenCalled()
+  })
+
+  it('should call created hook', function () {
+    expect(stub._callHook).toHaveBeenCalledWith('created')
+  })
+
+  it('should call $mount when options.el is present', function () {
+    expect(stub.$mount).toHaveBeenCalledWith(stub.$options.el)
+  })
+
+})

+ 48 - 10
test/unit/specs/instance/scope_spec.js

@@ -1,15 +1,35 @@
-/**
- * Test property proxy, scope inheritance,
- * data event propagation and data sync
- */
-
 var Vue = require('../../../../src/vue')
-var Observer = require('../../../../src/observer')
-Observer.pathDelimiter = '.'
 
-describe('Scope', function () {
-  
-  // TODO
+describe('Instance Scope', function () {
+
+  describe('data proxy', function () {
+
+    var data = {
+      a: 0,
+      b: 0
+    }
+    var vm = new Vue({
+      data: data
+    })
+
+    it('initial', function () {
+      expect(vm.a).toBe(data.a)
+      expect(vm.b).toBe(data.b)
+    })
+
+    it('vm => data', function () {
+      vm.a = 1
+      expect(data.a).toBe(1)
+      expect(vm.a).toBe(data.a)
+    })
+
+    it('data => vm', function () {
+      data.b = 2
+      expect(vm.b).toBe(2)
+      expect(vm.b).toBe(data.b)
+    })
+
+  })
 
   describe('computed', function () {
     
@@ -91,4 +111,22 @@ describe('Scope', function () {
 
   })
 
+  describe('meta', function () {
+
+    var vm = new Vue({
+      _meta: {
+        $index: 0,
+        $value: 'test'
+      }
+    })
+
+    it('should define metas only on vm', function () {
+      expect(vm.$index).toBe(0)
+      expect(vm.$value).toBe('test')
+      expect('$index' in vm.$data).toBe(false)
+      expect('$value' in vm.$data).toBe(false)
+    })
+
+  })
+
 })

+ 6 - 5
test/unit/specs/observer_spec.js

@@ -23,7 +23,7 @@ describe('Observer', function () {
       a: {},
       b: {}
     }
-    ob = Observer.create(obj)
+    var ob = Observer.create(obj)
     expect(ob instanceof Observer).toBe(true)
     expect(ob.active).toBe(true)
     expect(ob.value).toBe(obj)
@@ -39,7 +39,7 @@ describe('Observer', function () {
   it('create on array', function () {
     // on object
     var arr = [{}, {}]
-    ob = Observer.create(arr)
+    var ob = Observer.create(arr)
     expect(ob instanceof Observer).toBe(true)
     expect(ob.active).toBe(true)
     expect(ob.value).toBe(arr)
@@ -61,12 +61,13 @@ describe('Observer', function () {
       },
       update: jasmine.createSpy()
     }
+    var dump
     // collect dep
     Observer.target = watcher
-    obj.a.b
+    dump = obj.a.b
     Observer.target = null
     expect(watcher.deps.length).toBe(2)
-    obj.a.b = 3
+    dump = obj.a.b = 3
     expect(watcher.update.calls.count()).toBe(1)
     // swap object
     obj.a = { b: 4 }
@@ -75,7 +76,7 @@ describe('Observer', function () {
     var oldDeps = watcher.deps
     watcher.deps = []
     Observer.target = watcher
-    obj.a.b
+    dump = obj.a.b
     Observer.target = null
     expect(watcher.deps.length).toBe(2)
     // make sure we picked up the new bindings

+ 2 - 2
test/unit/specs/util/debug_spec.js

@@ -25,7 +25,7 @@ if (typeof console !== 'undefined') {
     it('not log when debug is false', function () {
       config.debug = false
       _.log('bye')
-      expect(console.log.calls.count()).toBe(0)
+      expect(console.log).not.toHaveBeenCalled()
     })
 
     it('warn when silent is false', function () {
@@ -37,7 +37,7 @@ if (typeof console !== 'undefined') {
     it('not warn when silent is ture', function () {
       config.silent = true
       _.warn('oops')
-      expect(console.warn.calls.count()).toBe(0)
+      expect(console.warn).not.toHaveBeenCalled()
     })
 
     if (console.trace) {

+ 15 - 4
test/unit/specs/watcher_spec.js

@@ -109,6 +109,17 @@ describe('Watcher', function () {
     })
   })
 
+  it('meta properties', function (done) {
+    vm._defineMeta('$index', 1)
+    var watcher = new Watcher(vm, '$index + 1', spy)
+    expect(watcher.value).toBe(2)
+    vm.$index = 2
+    nextTick(function () {
+      expect(watcher.value).toBe(3)
+      done()
+    })
+  })
+
   it('non-existent path, $add later', function (done) {
     var watcher = new Watcher(vm, 'd.e', spy)
     var watcher2 = new Watcher(vm, 'b.e', spy)
@@ -206,7 +217,7 @@ describe('Watcher', function () {
     nextTick(function () {
       // $data should only be called on self data change
       expect(watcher1.value).toBe(child.$data)
-      expect(spy.calls.count()).toBe(0)
+      expect(spy).not.toHaveBeenCalled()
       expect(watcher2.value).toBe(123)
       expect(spy2).toHaveBeenCalledWith(123, 1)
       done()
@@ -249,7 +260,7 @@ describe('Watcher', function () {
     nextTick(function () {
       expect(vm.b.c).toBe(2)
       expect(watcher.value).toBe(2)
-      expect(spy.calls.count()).toBe(0)
+      expect(spy).not.toHaveBeenCalled()
       watcher.set(6)
       nextTick(function () {
         expect(vm.b.c).toBe(6)
@@ -299,7 +310,7 @@ describe('Watcher', function () {
     watcher.removeCb(spy)
     vm.a = 234
     nextTick(function () {
-      expect(spy.calls.count()).toBe(0)
+      expect(spy).not.toHaveBeenCalled()
       expect(spy2).toHaveBeenCalledWith(234, 1)
       done()
     })
@@ -313,7 +324,7 @@ describe('Watcher', function () {
       expect(watcher.active).toBe(false)
       expect(watcher.vm).toBe(null)
       expect(watcher.cbs).toBe(null)
-      expect(spy.calls.count()).toBe(0)
+      expect(spy).not.toHaveBeenCalled()
       done()
     })
   })