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

work on instantiation with scope inheritance

Evan You 12 лет назад
Родитель
Сommit
e9ecdfe1c0

+ 81 - 0
benchmarks/instantiation.js

@@ -0,0 +1,81 @@
+console.log('\nInstantiation\n')
+
+var Vue = require('../src/vue')
+var sideEffect = null
+var parent = new Vue({
+  data: { a: 1 }
+})
+
+function getNano () {
+  var hr = process.hrtime()
+  return hr[0] * 1e9 + hr[1]
+}
+
+function now () {
+  return process.hrtime
+    ? getNano() / 1e6
+    : window.performence
+      ? window.performence.now()
+      : Date.now()
+}
+
+// warm up
+for (var i = 0; i < 1000; i++) {
+  sideEffect = new Vue()
+}
+
+function bench (desc, n, fn) {
+  var s = now()
+  for (var i = 0; i < n; i++) {
+    fn()
+  }
+  var time = now() - s
+  var opf = (16 / (time / n)).toFixed(2)
+  console.log(desc + ' ' + n + ' times - ' + opf + ' ops/frame')
+}
+
+function simpleInstance () {
+  sideEffect = new Vue({
+    data: {a: 1}
+  })
+}
+
+function simpleInstanceWithInheritance () {
+  sideEffect = new Vue({
+    parent: parent,
+    data: { b:2 }
+  })
+}
+
+function complexInstance () {
+  sideEffect = new Vue({
+    data: {
+      a: {
+        b: {
+          c: 1
+        }
+      },
+      c: {
+        b: {
+          c: { a:1 },
+          d: 2,
+          e: 3,
+          d: 4
+        }
+      },
+      e: [{a:1}, {a:2}, {a:3}]
+    }
+  })
+}
+
+bench('Simple instance', 10, simpleInstance)
+bench('Simple instance', 100, simpleInstance)
+bench('Simple instance', 1000, simpleInstance)
+
+bench('Simple instance with inheritance', 10, simpleInstanceWithInheritance)
+bench('Simple instance with inheritance', 100, simpleInstanceWithInheritance)
+bench('Simple instance with inheritance', 1000, simpleInstanceWithInheritance)
+
+bench('Complex instance', 10, complexInstance)
+bench('Complex instance', 100, complexInstance)
+bench('Complex instance', 1000, complexInstance)

+ 30 - 128
explorations/inheritance.js

@@ -1,123 +1,12 @@
-var Observer = require('../src/observe/observer')
-var _ = require('../src/util')
-
-function Vue (options) {
-
-  var data = options.data
-  var parent = options.parent
-  var scope = this._scope = parent
-    ? Object.create(parent._scope)
-    : {}
-
-  // copy instantiation data into scope
-  for (var key in 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, data[key], true)
-    } else {
-      scope[key] = data[key]
-    }
-  }
-
-  // create observer
-  // pass in noProto:true to avoid mutating the __proto__
-  var ob = this._observer = Observer.create(scope, { noProto: true })
-  var dob = Observer.create(data)
-  var locked = false
-
-  // sync scope and original data.
-  ob
-    .on('set', guard(function (key, val) {
-      data[key] = val
-    }))
-    .on('added', guard(function (key, val) {
-      data.$add(key, val)
-    }))
-    .on('deleted', guard(function (key) {
-      data.$delete(key)
-    }))
-
-  // also need to sync data object changes to scope...
-  // this would cause cycle updates, so we need to lock
-  // stuff when one side updates the other
-  dob
-    .on('set', guard(function (key, val) {
-      scope[key] = val
-    }))
-    .on('added', guard(function (key, val) {
-      scope.$add(key, val)
-    }))
-    .on('deleted', guard(function (key) {
-      scope.$delete(key)
-    }))
-
-  function guard (fn) {
-    return function (key, val) {
-      if (locked || key.indexOf(Observer.pathDelimiter) > -1) {
-        return
-      }
-      locked = true
-      fn(key, val)
-      locked = false
-    }
-  }
-
-  // relay change events from parent scope.
-  // this ensures the current Vue instance is aware of
-  // stuff going on up in the scope chain.
-  if (parent) {
-    var po = 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 scope) {
-    _.proxy(this, scope, key)
-  }
-
-  // also proxy newly added keys.
-  var self = this
-  ob.on('added', function (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)
-}
-
-Vue.prototype.$toJSON = function () {
-  return JSON.stringify(this._scope)
-}
-
-Vue.prototype.$log = function (key) {
-  var data = key
-    ? this._scope[key]
-    : this._scope
-  console.log(JSON.parse(JSON.stringify(data)))
-}
+var Vue = require('../src/vue')
 
 window.model = {
-  a: 'go!',
-  b: 2,
+  a: 'parent a',
+  b: 'parent b',
   c: {
     d: 3
   },
-  arr: [{a:1}, {a:2}, {a:3}],
+  arr: [{a: 'item a'}],
   go: function () {
     console.log(this.a)
   }
@@ -130,7 +19,7 @@ window.vm = new Vue({
 window.child = new Vue({
   parent: vm,
   data: {
-    a: 1,
+    a: 'child a',
     change: function () {
       this.c.d = 4
       this.b = 456 // Unlike Angular, setting primitive values in Vue WILL affect outer scope,
@@ -141,29 +30,42 @@ window.child = new Vue({
 
 window.item = new Vue({
   parent: vm,
-  data: model.arr[0]
+  data: vm.arr[0]
 })
 
 vm._observer.on('set', function (key, val) {
   console.log('vm 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('set', function (key, val) {
   console.log('child set:' + key.replace(/[\b]/g, '.'), val)
 })
 
-child._observer.on('mutate', function (key, val) {
-  console.log('child mutate:' + key.replace(/[\b]/g, '.'), val)
-})
-
 item._observer.on('set', function (key, val) {
   console.log('item set:' + key.replace(/[\b]/g, '.'), val)
 })
 
-item._observer.on('mutate', function (key, val) {
-  console.log('item mutate:' + key.replace(/[\b]/g, '.'), val)
-})
+// TODO turn these into tests
+
+console.log(vm.a) // 'parent a'
+console.log(child.a) // 'child a'
+console.log(child.b) // 'parent b'
+console.log(item.a) // 'item a'
+console.log(item.b) // 'parent b'
+
+// set shadowed parent property
+vm.a = 'haha!' // vm set:a haha!
+
+// set shadowed child property
+child.a = 'hmm' // child set:a hmm
+
+// test parent scope change downward propagation
+vm.b = 'hoho!' // child set:b hoho!
+               // item set:b hoho!
+               // vm set:b hoho!
+
+// set child owning an array item
+item.a = 'wow' // child set:arr.0.a wow
+               // item set:arr.0.a wow
+               // vm set:arr.0.a wow
+               // item set:a wow

+ 8 - 4
src/internal/compile.js

@@ -1,5 +1,9 @@
-function Compiler () {
-    
-}
+/**
+ * Start compilation of an instance.
+ *
+ * @private
+ */
 
-module.exports = Compiler
+exports._compile = function () {
+  
+}

+ 186 - 64
src/internal/init.js

@@ -1,90 +1,212 @@
+var _ = require('../util')
+var Observer = require('../observe/observer')
+var scopeEvents = ['set', 'mutate', 'added', 'deleted', 'added:self', 'deleted:self']
+
+/**
+ * Kick off the initialization process on instance creation.
+ *
+ * @param {Object} options
+ * @private
+ */
+
 exports._init = function (options) {
+  this.$options = options = options || {}
+  // create scope
+  this._initScope(options)
+  // setup initial data.
+  this._initData(options.data || {}, true)
+  // setup property proxying
+  this._initProxy()
+}
+
+/**
+ * Setup scope and listen to parent scope changes.
+ * Only called once during _init().
+ */
 
-  var data = options.data
-  var parent = options.parent
-  var scope = this._scope = parent
+exports._initScope = function (options) {
+
+  var parent = this.$parent = options.parent
+  var scope = this._scope = parent && options._inheritScope !== false
     ? Object.create(parent._scope)
     : {}
+  // create scope observer
+  this._observer = Observer.create(scope, {
+    callbackContext: this,
+    doNotAlterProto: true
+  })
+
+  if (!parent) return
+
+  // relay change events that sent down from
+  // the scope prototype chain.
+  var ob = this._observer
+  var pob = 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 (!scope.hasOwnProperty(key)) {
+        ob.emit(event, key, a, b)
+      }
+    }
+    pob.on(event, cb)
+  })
+}
+
+/**
+ * Teardown scope and remove listeners attached to parent scope.
+ * Only called once during $destroy().
+ */
+
+exports._teardownScope = function () {
+  this._scope = null
+  if (!this.$parent) return
+  var pob = this.$parent._observer
+  var listeners = this._scopeListeners
+  scopeEvents.forEach(function (event) {
+    pob.off(event, listeners[event])
+  })
+}
+
+/**
+ * Set the instances data object. Teasdown previous data
+ * object if necessary, and setup syncing between the scope
+ * and the data object.
+ *
+ * @param {Object} data
+ * @param {Boolean} init
+ */
+
+exports._initData = function (data, init) {
+  var scope = this._scope
+
+  if (!init) {
+    // teardown old sync listeners
+    this._unsync()
+    // delete keys not present in the new data
+    for (var key in scope) {
+      if (scope.hasOwnProperty(key) && !(key in data)) {
+        scope.$delete(key)
+      }
+    }
+  }
 
   // copy instantiation data into scope
   for (var key in 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, data[key], true)
-    } else {
+    if (scope.hasOwnProperty(key)) {
+      // existing property, trigger set
       scope[key] = data[key]
+    } else {
+      // new property
+      scope.$add(key, data[key])
     }
   }
 
-  // create observer
-  // pass in noProto:true to avoid mutating the __proto__
-  var ob = this._observer = Observer.create(scope, { noProto: true })
-  var dob = Observer.create(data)
+  // sync scope and new data
+  this._data = data
+  this._dataObserver = Observer.create(data)
+  this._sync()
+}
+
+/**
+ * Proxy the scope properties on the instance itself.
+ * So that vm.a === vm._scope.a
+ */
+
+exports._initProxy = function () {
+  // proxy every scope property on the instance itself
+  var scope = this._scope
+  for (var key in scope) {
+    _.proxy(this, scope, key)
+  }
+  // keep proxying up-to-date with added/deleted keys.
+  this._observer
+    .on('added:self', function (key) {
+      _.proxy(this, scope, key)
+    })
+    .on('deleted:self', function (key) {
+      delete this[key]
+    })
+}
+
+/**
+ * Setup two-way sync between the instance scope and
+ * the original data. Requires teardown.
+ */
+
+exports._sync = function () {
+  var data = this._data
+  var scope = this._scope
   var locked = false
 
+  var listeners = this._syncListeners = {
+    data: {
+      set: guard(function (key, val) {
+        data[key] = val
+      }),
+      added: guard(function (key, val) {
+        data.$add(key, val)
+      }),
+      deleted: guard(function (key) {
+        data.$delete(key)
+      })
+    },
+    scope: {
+      set: guard(function (key, val) {
+        scope[key] = val
+      }),
+      added: guard(function (key, val) {
+        scope.$add(key, val)
+      }),
+      deleted: guard(function (key) {
+        scope.$delete(key)
+      })
+    }
+  }
+
   // sync scope and original data.
-  ob
-    .on('set', guard(function (key, val) {
-      data[key] = val
-    }))
-    .on('added', guard(function (key, val) {
-      data.$add(key, val)
-    }))
-    .on('deleted', guard(function (key) {
-      data.$delete(key)
-    }))
-
-  // also need to sync data object changes to scope...
-  // this would cause cycle updates, so we need to lock
-  // stuff when one side updates the other
-  dob
-    .on('set', guard(function (key, val) {
-      scope[key] = val
-    }))
-    .on('added', guard(function (key, val) {
-      scope.$add(key, val)
-    }))
-    .on('deleted', guard(function (key) {
-      scope.$delete(key)
-    }))
+  this._observer
+    .on('set:self', listeners.data.set)
+    .on('added:self', listeners.data.added)
+    .on('deleted:self', listeners.data.deleted)
+
+  this._dataObserver
+    .on('set:self', listeners.scope.set)
+    .on('added:self', listeners.scope.added)
+    .on('deleted:self', listeners.scope.delted)
+
+  /**
+   * The guard function prevents infinite loop
+   * when syncing between two observers.
+   */
 
   function guard (fn) {
     return function (key, val) {
-      if (locked || key.indexOf(Observer.pathDelimiter) > -1) {
-        return
-      }
+      if (locked) return
       locked = true
       fn(key, val)
       locked = false
     }
   }
+}
 
-  // relay change events from parent scope.
-  // this ensures the current Vue instance is aware of
-  // stuff going on up in the scope chain.
-  if (parent) {
-    var po = 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)
-        }
-      })
-    })
-  }
+/**
+ * Teardown the sync between scope and previous data object.
+ */
 
-  // proxy everything on self
-  for (var key in scope) {
-    _.proxy(this, scope, key)
-  }
+exports._unsync = function () {
+  var listeners = this._syncListeners
 
-  // also proxy newly added keys.
-  var self = this
-  ob.on('added', function (key) {
-    _.proxy(self, scope, key)
-  })
-  
+  this._observer
+    .off('set:self', listeners.data.set)
+    .off('added:self', listeners.data.added)
+    .off('deleted:self', listeners.data.deleted)
+
+  this._dataObserver
+    .off('set:self', listeners.scope.set)
+    .off('added:self', listeners.scope.added)
+    .off('deleted:self', listeners.scope.delted)
 }

+ 34 - 0
src/internal/properties.js

@@ -0,0 +1,34 @@
+/**
+ * Prototype properties on every Vue instance.
+ */
+
+module.exports = function (p) {
+
+  /**
+   * The $root recursively points to the root instance.
+   *
+   * @readonly
+   */
+
+  Object.defineProperty(p, '$root', {
+    get: function () {
+      return this.$parent
+        ? this.$parent.$root
+        : this
+    }
+  })
+
+  /**
+   * $data has a setter which does a bunch of teardown/setup work
+   */
+
+  Object.defineProperty(p, '$data', {
+    get: function () {
+      return this._data
+    },
+    set: function (newData) {
+      this._initData(newData)
+    }
+  })
+
+}

+ 2 - 2
src/observe/array-augmentations.js

@@ -61,11 +61,11 @@ var arrayAugmentations = Object.create(Array.prototype)
 
     // emit length change
     if (inserted || removed) {
-      ob.notify('set', 'length', this.length)
+      ob.propagate('set', 'length', this.length)
     }
 
     // empty path, value is the Array itself
-    ob.notify('mutate', '', this, {
+    ob.propagate('mutate', '', this, {
       method   : method,
       args     : args,
       result   : result,

+ 5 - 2
src/observe/object-augmentations.js

@@ -17,7 +17,8 @@ _.define(objectAgumentations, '$add', function (key, val) {
   var ob = this.$observer
   ob.observe(key, val)
   ob.convert(key, val)
-  ob.notify('added', key, val)
+  ob.emit('added:self', key, val)
+  ob.propagate('added', key, val)
 })
 
 /**
@@ -31,7 +32,9 @@ _.define(objectAgumentations, '$add', function (key, val) {
 _.define(objectAgumentations, '$delete', function (key) {
   if (!this.hasOwnProperty(key)) return
   delete this[key]
-  this.$observer.notify('deleted', key)
+  var ob = this.$observer
+  ob.emit('deleted:self', key)
+  ob.propagate('deleted', key)
 })
 
 module.exports = objectAgumentations

+ 14 - 11
src/observe/observer.js

@@ -25,10 +25,12 @@ var OBJECT = 1
  * @param {Array|Object} value
  * @param {Number} type
  * @param {Object} [options]
+ *                 - doNotAlterProto: if true, do not alter object's __proto__
+ *                 - callbackContext: `this` context for callbacks
  */
 
 function Observer (value, type, options) {
-  Emitter.call(this)
+  Emitter.call(this, options && options.callbackContext)
   this.value = value
   this.type = type
   this.parents = null
@@ -38,7 +40,7 @@ function Observer (value, type, options) {
       _.augment(value, arrayAugmentations)
       this.link(value)
     } else if (type === OBJECT) {
-      if (options && options.noProto) {
+      if (options && options.doNotAlterProto) {
         _.deepMixin(value, objectAugmentations)
       } else {
         _.augment(value, objectAugmentations)
@@ -73,7 +75,7 @@ Observer.emitGet = false
  * or the existing observer if the value already has one.
  *
  * @param {*} value
- * @param {Object} [options]
+ * @param {Object} [options] - see the Observer constructor.
  * @return {Observer|undefined}
  * @static
  */
@@ -187,7 +189,7 @@ p.convert = function (key, val) {
     configurable: true,
     get: function () {
       if (Observer.emitGet) {
-        ob.notify('get', key)
+        ob.propagate('get', key)
       }
       return val
     },
@@ -195,11 +197,12 @@ p.convert = function (key, val) {
       if (newVal === val) return
       ob.unobserve(val)
       ob.observe(key, newVal)
-      ob.notify('set', key, newVal)
+      ob.emit('set:self', key, newVal)
+      ob.propagate('set', key, newVal)
       if (_.isArray(newVal)) {
-        ob.notify('set',
-                  key + Observer.pathDelimiter + 'length',
-                  newVal.length)
+        ob.propagate('set',
+                     key + Observer.pathDelimiter + 'length',
+                     newVal.length)
       }
       val = newVal
     }
@@ -207,7 +210,7 @@ p.convert = function (key, val) {
 }
 
 /**
- * Emit event on self and recursively notify all parents.
+ * Emit event on self and recursively propagate all parents.
  *
  * @param {String} event
  * @param {String} path
@@ -215,7 +218,7 @@ p.convert = function (key, val) {
  * @param {Object|undefined} mutation
  */
 
-p.notify = function (event, path, val, mutation) {
+p.propagate = function (event, path, val, mutation) {
   this.emit(event, path, val, mutation)
   if (!this.parents) return
   for (var i = 0, l = this.parents.length; i < l; i++) {
@@ -225,7 +228,7 @@ p.notify = function (event, path, val, mutation) {
     var parentPath = path
       ? key + Observer.pathDelimiter + path
       : key
-    ob.notify(event, parentPath, val, mutation)
+    ob.propagate(event, parentPath, val, mutation)
   }
 }
 

+ 11 - 0
src/vue.js

@@ -3,6 +3,11 @@ var _ = require('./util')
 /**
  * The exposed Vue constructor.
  *
+ * API conventions:
+ * - public API methods/properties are prefiexed with `$`
+ * - internal methods/properties are prefixed with `_`
+ * - non-prefixed properties are assumed to be proxied user data.
+ *
  * @constructor
  * @param {Object} [options]
  * @public
@@ -14,6 +19,12 @@ function Vue (options) {
 
 var p = Vue.prototype
 
+/**
+ * Define prototype properties
+ */
+
+require('./internal/properties')(p)
+
 /**
  * Mixin internal instance methods
  */

+ 13 - 7
tasks/bench.js

@@ -3,7 +3,7 @@
  */
 
 module.exports = function (grunt) {
-  grunt.registerTask('bench', function () {
+  grunt.registerTask('bench', function (target) {
 
     // polyfill window/document for old Vue
     global.window = {
@@ -14,12 +14,18 @@ module.exports = function (grunt) {
       documentElement: {}
     }
 
-    require('fs')
-      .readdirSync('./benchmarks')
-      .forEach(function (mod) {
-        if (mod === 'browser.js' || mod === 'runner.html') return
-        require('../benchmarks/' + mod)
-      })
+    if (target) {
+      run(target)
+    } else {
+      require('fs')
+        .readdirSync('./benchmarks')
+        .forEach(run)
+    }
+
+    function run (mod) {
+      if (mod === 'browser.js' || mod === 'runner.html') return
+      require('../benchmarks/' + mod)
+    }
 
   })
 }