Ver Fonte

compiler rewrite - observe scope directly and proxy access through vm

Evan You há 12 anos atrás
pai
commit
14d8ce24a3

+ 2 - 0
examples/todomvc/js/app.js

@@ -1,3 +1,5 @@
+Vue.config({debug:true})
+
 var filters = {
     all: function () { return true },
     active: function (completed) { return !completed },

+ 1 - 1
examples/todomvc/js/benchmark.js

@@ -4,7 +4,7 @@
 // then delete them one by one
 
 // do not run when testing in PhantomJS
-if (navigator.userAgent.indexOf('PhantomJS') === -1) {
+if (window.location.search === '?benchmark=1') {
     runBenchmark()
 }
 

+ 47 - 87
src/compiler.js

@@ -37,13 +37,14 @@ function Compiler (vm, options) {
     var el = compiler.setupElement(options)
     log('\nnew VM instance:', el.tagName, '\n')
 
-    // copy scope properties to vm
-    var scope = options.scope
-    if (scope) utils.extend(vm, scope, true)
+    // init scope
+    var scope = compiler.scope = options.scope || {}
+    utils.extend(vm, scope, true)
 
     compiler.vm  = vm
     def(vm, '$', makeHash())
     def(vm, '$el', el)
+    def(vm, '$scope', scope)
     def(vm, '$compiler', compiler)
 
     // keep track of directives and expressions
@@ -85,21 +86,16 @@ function Compiler (vm, options) {
 
     // beforeCompile hook
     compiler.execHook('beforeCompile', 'created')
-
-    // create bindings for things already in scope
-    var key, keyPrefix
-    for (key in vm) {
-        keyPrefix = key.charAt(0)
-        if (keyPrefix !== '$' && keyPrefix !== '_') {
-            compiler.createBinding(key)
-        }
-    }
+    // the user might have set some props on the vm 
+    // so copy it back to the scope...
+    utils.extend(scope, vm)
+    // observe the scope
+    Observer.observe(scope, '', compiler.observer)
 
     // for repeated items, create an index binding
     // which should be inenumerable but configurable
     if (compiler.repeat) {
-        vm.$index = compiler.repeatIndex
-        def(vm, '$collection', compiler.repeatCollection)
+        scope.$index = compiler.repeatIndex
         compiler.createBinding('$index')
     }
 
@@ -107,14 +103,6 @@ function Compiler (vm, options) {
     // and bind the parsed directives
     compiler.compile(el, true)
 
-    // observe root values so that they emit events when
-    // their nested values change (for an Object)
-    // or when they mutate (for an Array)
-    var i = observables.length, binding
-    while (i--) {
-        binding = observables[i]
-        Observer.observe(binding.value, binding.key, compiler.observer)
-    }
     // extract dependencies for computed properties
     if (computed.length) DepsParser.parse(computed)
 
@@ -374,24 +362,25 @@ CompilerProto.bindDirective = function (directive) {
     var binding,
         compiler      = this,
         key           = directive.key,
-        baseKey       = key.split('.')[0],
-        ownerCompiler = traceOwnerCompiler(directive, compiler)
+        baseKey       = key.split('.')[0]
 
     if (directive.isExp) {
         // expression bindings are always created on current compiler
         binding = compiler.createBinding(key, true, directive.isFn)
-    } else if (ownerCompiler.vm.hasOwnProperty(baseKey)) {
-        // If the directive's owner compiler's VM has the key,
-        // it belongs there. Create the binding if it's not already
-        // created, and return it.
-        binding = hasOwn.call(ownerCompiler.bindings, key)
-            ? ownerCompiler.bindings[key]
-            : ownerCompiler.createBinding(key)
+    } else if (
+        hasOwn.call(compiler.scope, baseKey) ||
+        hasOwn.call(compiler.vm, baseKey)
+    ) {
+        // If the directive's compiler's VM has the base key,
+        // it belongs here. Create the binding if it's not created already.
+        binding = hasOwn.call(compiler.bindings, key)
+            ? compiler.bindings[key]
+            : compiler.createBinding(key)
     } else {
         // due to prototypal inheritance of bindings, if a key doesn't exist
-        // on the owner compiler's VM, then it doesn't exist in the whole
+        // on the bindings object, then it doesn't exist in the whole
         // prototype chain. In this case we create the new binding at the root level.
-        binding = ownerCompiler.bindings[key] || compiler.rootCompiler.createBinding(key)
+        binding = compiler.bindings[key] || compiler.rootCompiler.createBinding(key)
     }
 
     binding.instances.push(directive)
@@ -419,6 +408,7 @@ CompilerProto.bindDirective = function (directive) {
 CompilerProto.createBinding = function (key, isExp, isFn) {
 
     var compiler = this,
+        scope = compiler.scope,
         bindings = compiler.bindings,
         binding  = new Binding(compiler, key, isExp, isFn)
 
@@ -443,7 +433,8 @@ CompilerProto.createBinding = function (key, isExp, isFn) {
             // this is a root level binding. we need to define getter/setters for it.
             compiler.define(key, binding)
         } else {
-            Observer.ensurePath(compiler.vm, key)
+            // ensure path in scope so it can be observed
+            Observer.ensurePath(scope, key)
             var parentKey = key.slice(0, key.lastIndexOf('.'))
             if (!hasOwn.call(bindings, parentKey)) {
                 // this is a nested value binding, but the binding for its parent
@@ -464,52 +455,35 @@ CompilerProto.define = function (key, binding) {
     log('    defined root binding: ' + key)
 
     var compiler = this,
+        scope = compiler.scope,
         vm = compiler.vm,
-        ob = compiler.observer,
-        value = binding.value = vm[key], // save the value before redefinening it
-        type = utils.typeOf(value)
+        value = binding.value = scope[key] // save the value before redefinening it
 
-    if (type === 'Object' && value.$get) {
-        // computed property
+    if (utils.typeOf(value) === 'Object' && value.$get) {
         compiler.markComputed(binding)
-    } else if (type === 'Object' || type === 'Array') {
-        // observe objects later, because there might be more keys
-        // to be added to it during Observer.ensurePath().
-        // we also want to emit all the set events after all values
-        // are available.
-        compiler.observables.push(binding)
+    }
+
+    if (!(key in scope)) {
+        scope[key] = undefined
+    }
+
+    if (scope.__observer__) { 
+        Observer.convert(scope, key)
     }
 
     Object.defineProperty(vm, key, {
-        enumerable: true,
         get: function () {
-            var value = binding.value
-            if (depsOb.active && (!binding.isComputed && (!value || !value.__observer__)) ||
-                Array.isArray(value)) {
-                // only emit non-computed, non-observed (primitive) values, or Arrays.
-                // because these are the cleanest dependencies
-                ob.emit('get', key)
-            }
+            var val = scope[key]
             return binding.isComputed
-                ? value.$get()
-                : value
+                ? val.$get()
+                : val
         },
         set: function (newVal) {
-            var value = binding.value
+            var val = scope[key]
             if (binding.isComputed) {
-                if (value.$set) {
-                    value.$set(newVal)
-                }
-            } else if (newVal !== value) {
-                // unwatch the old value
-                Observer.unobserve(value, key, ob)
-                // set new value
-                binding.value = newVal
-                ob.emit('set', key, newVal)
-                Observer.ensurePaths(key, newVal, compiler.bindings)
-                // now watch the new value, which in turn emits 'set'
-                // for all its nested values
-                Observer.observe(newVal, key, ob)
+                if (val.$set) val.$set(newVal)
+            } else {
+                scope[key] = newVal
             }
         }
     })
@@ -631,28 +605,14 @@ CompilerProto.destroy = function () {
 
 // Helpers --------------------------------------------------------------------
 
-/**
- *  determine which viewmodel a key belongs to based on nesting symbols
- */
-function traceOwnerCompiler (key, compiler) {
-    if (key.nesting) {
-        var levels = key.nesting
-        while (compiler.parentCompiler && levels--) {
-            compiler = compiler.parentCompiler
-        }
-    } else if (key.root) {
-        while (compiler.parentCompiler) {
-            compiler = compiler.parentCompiler
-        }
-    }
-    return compiler
-}
-
 /**
  *  shorthand for getting root compiler
  */
 function getRoot (compiler) {
-    return traceOwnerCompiler({ root: true }, compiler)
+    while (compiler.parentCompiler) {
+        compiler = compiler.parentCompiler
+    }
+    return compiler
 }
 
 module.exports = Compiler

+ 4 - 3
src/deps-parser.js

@@ -9,10 +9,11 @@ var Emitter  = require('./emitter'),
 function catchDeps (binding) {
     if (binding.isFn) return
     utils.log('\n─ ' + binding.key)
-    var has = []
+    var got = utils.hash()
     observer.on('get', function (dep) {
-        if (has.indexOf(dep) > -1) return
-        has.push(dep)
+        var has = got[dep.key]
+        if (has && has.compiler === dep.compiler) return
+        got[dep.key] = dep
         utils.log('  └─ ' + dep.key)
         binding.deps.push(dep)
         dep.subs.push(binding)

+ 0 - 16
src/directive.js

@@ -15,7 +15,6 @@ var config     = require('./config'),
     ARG_RE          = /^([\w- ]+):(.+)$/,
     FILTERS_RE      = /\|[^\|]+/g,
     FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
-    NESTING_RE      = /^\^+/,
     SINGLE_VAR_RE   = /^[\w\.\$]+$/
 
 /**
@@ -76,7 +75,6 @@ var DirProto = Directive.prototype
  *  parse a key, extract argument and nesting/root info
  */
 function parseKey (dir, rawKey) {
-
     var key = rawKey
     if (rawKey.indexOf(':') > -1) {
         var argMatch = rawKey.match(ARG_RE)
@@ -87,20 +85,6 @@ function parseKey (dir, rawKey) {
             ? argMatch[1].trim()
             : null
     }
-
-    // nesting
-    var firstChar = key.charAt(0)
-    dir.root = firstChar === '*'
-    dir.nesting = firstChar === '^'
-        ? key.match(NESTING_RE)[0].length
-        : false
-
-    if (dir.nesting) {
-        key = key.slice(dir.nesting)
-    } else if (dir.root) {
-        key = key.slice(1)
-    }
-
     dir.key = key
 }
 

+ 4 - 4
src/directives/component.js

@@ -20,11 +20,11 @@ module.exports = {
         var Ctor = this.compiler.getOption('components', this.arg)
         if (!Ctor) utils.warn('unknown component: ' + this.arg)
         var options = {
-                el: this.el,
-                compilerOptions: {
-                    parentCompiler: this.compiler
-                }
+            el: this.el,
+            compilerOptions: {
+                parentCompiler: this.compiler
             }
+        }
         if (value) {
             options.scope = {
                 model: value

+ 2 - 2
src/directives/on.js

@@ -51,7 +51,7 @@ module.exports = {
                 if (target) {
                     e.el = target
                     e.vm = target.vue_viewmodel
-                    e.item = e.vm[compiler.repeatPrefix]
+                    e.item = e.vm.$scope[compiler.repeatPrefix]
                     handler.call(ownerVM, e)
                 }
             }
@@ -66,7 +66,7 @@ module.exports = {
                 e.el = e.currentTarget
                 e.vm = vm
                 if (compiler.repeat) {
-                    e.item = vm[compiler.repeatPrefix]
+                    e.item = vm.$scope[compiler.repeatPrefix]
                 }
                 handler.call(ownerVM, e)
             }

+ 2 - 2
src/directives/repeat.js

@@ -61,7 +61,7 @@ var mutationHandlers = {
             data = col[i]
             for (j = 0; j < l; j++) {
                 vm = vms[j]
-                if (vm[key] === data) {
+                if (vm.$scope[key] === data) {
                     sorted[i] = vm
                     break
                 }
@@ -201,7 +201,7 @@ module.exports = {
     updateIndexes: function () {
         var i = this.vms.length
         while (i--) {
-            this.vms[i].$index = i
+            this.vms[i].$scope.$index = i
         }
     },
 

+ 4 - 1
src/exp-parser.js

@@ -58,7 +58,10 @@ function getRel (path, compiler) {
             ? path.slice(0, dot)
             : path
     while (true) {
-        if (hasOwn.call(vm, key)) {
+        if (
+            hasOwn.call(vm.$scope, key) ||
+            hasOwn.call(vm, key)
+        ) {
             break
         } else {
             if (vm.$parent) {

+ 57 - 33
src/observer.js

@@ -9,6 +9,10 @@ var Emitter  = require('./emitter'),
     def      = utils.defProtected,
     slice    = Array.prototype.slice,
 
+    // types
+    OBJECT   = 'Object',
+    ARRAY    = 'Array',
+
     // Array mutation methods to wrap
     methods  = ['push','pop','shift','unshift','splice','sort','reverse'],
 
@@ -89,11 +93,11 @@ for (var method in extensions) {
 /**
  *  Watch an Object, recursive.
  */
-function watchObject (obj, path, observer) {
+function watchObject (obj, path) {
     for (var key in obj) {
         var keyPrefix = key.charAt(0)
-        if (keyPrefix !== '$' && keyPrefix !== '_') {
-            convert(obj, key, observer)
+        if ((keyPrefix !== '$' && keyPrefix !== '_') || key === '$index') {
+            convert(obj, key)
         }
     }
 }
@@ -102,8 +106,12 @@ function watchObject (obj, path, observer) {
  *  Watch an Array, overload mutation methods
  *  and add augmentations by intercepting the prototype chain
  */
-function watchArray (arr, path, observer) {
-    def(arr, '__observer__', observer)
+function watchArray (arr, path) {
+    var observer = arr.__observer__
+    if (!observer) {
+        observer = new Emitter()
+        def(arr, '__observer__', observer)
+    }
     observer.path = path
     if (hasProto) {
         arr.__proto__ = ArrayProxy
@@ -119,8 +127,9 @@ function watchArray (arr, path, observer) {
  *  so it emits get/set events.
  *  Then watch the value itself.
  */
-function convert (obj, key, observer) {
-    var val       = obj[key],
+function convert (obj, key) {
+    var observer  = obj.__observer__,
+        val       = obj[key],
         values    = observer.values
     values[key] = val
     // emit set on bind
@@ -128,11 +137,10 @@ function convert (obj, key, observer) {
     // a first batch of set events.
     observer.emit('set', key, val)
     Object.defineProperty(obj, key, {
-        enumerable: true,
         get: function () {
             var value = values[key]
             // only emit get on tip values
-            if (depsOb.active && !isWatchable(value)) {
+            if (depsOb.active && typeOf(value) !== OBJECT) {
                 observer.emit('get', key)
             }
             return value
@@ -141,7 +149,7 @@ function convert (obj, key, observer) {
             var oldVal = values[key]
             unobserve(oldVal, key, observer)
             values[key] = newVal
-            ensurePaths('', newVal, oldVal)
+            copyPaths(newVal, oldVal)
             observer.emit('set', key, newVal)
             observe(newVal, key, observer)
         }
@@ -155,7 +163,7 @@ function convert (obj, key, observer) {
 function isWatchable (obj) {
     ViewModel = ViewModel || require('./viewmodel')
     var type = typeOf(obj)
-    return (type === 'Object' || type === 'Array') && !(obj instanceof ViewModel)
+    return (type === OBJECT || type === ARRAY) && !(obj instanceof ViewModel)
 }
 
 /**
@@ -167,9 +175,9 @@ function isWatchable (obj) {
 function emitSet (obj) {
     var type = typeOf(obj),
         emitter = obj.__observer__
-    if (type === 'Array') {
+    if (type === ARRAY) {
         emitter.emit('set', 'length', obj.length)
-    } else if (type === 'Object') {
+    } else if (type === OBJECT) {
         var key, val
         for (key in obj) {
             val = obj[key]
@@ -180,16 +188,28 @@ function emitSet (obj) {
 }
 
 /**
- *  Sometimes when a binding is found in the template, the value might
- *  have not been set on the VM yet. To ensure computed properties and
- *  dependency extraction can work, we have to create a dummy value for
- *  any given path.
+ *  Make sure all the paths in an old object exists
+ *  in a new object.
+ *  So when an object changes, all missing keys will
+ *  emit a set event with undefined value.
  */
-function ensurePaths (key, val, paths) {
-    key = key ? key + '.' : ''
-    for (var path in paths) {
-        if (!key || !path.indexOf(key)) {
-            ensurePath(val, key ? path.replace(key, '') : path)
+function copyPaths (newObj, oldObj) {
+    if (typeOf(oldObj) !== OBJECT || typeOf(newObj) !== OBJECT) {
+        return
+    }
+    var path, type, oldVal, newVal
+    for (path in oldObj) {
+        if (!(path in newObj)) {
+            oldVal = oldObj[path]
+            type = typeOf(oldVal)
+            if (type === OBJECT) {
+                newVal = newObj[path] = {}
+                copyPaths(newVal, oldVal)
+            } else if (type === ARRAY) {
+                newObj[path] = []
+            } else {
+                newObj[path] = undefined
+            }
         }
     }
 }
@@ -199,19 +219,22 @@ function ensurePaths (key, val, paths) {
  *  and enumerated in that object
  */
 function ensurePath (obj, key) {
-    if (typeOf(obj) !== 'Object') return
     var path = key.split('.'), sec
     for (var i = 0, d = path.length - 1; i < d; i++) {
         sec = path[i]
-        if (!obj[sec]) obj[sec] = {}
+        if (!obj[sec]) {
+            obj[sec] = {}
+            if (obj.__observer__) convert(obj, sec)
+        }
         obj = obj[sec]
     }
-    var type = typeOf(obj)
-    if (type === 'Object' || type === 'Array') {
+    if (typeOf(obj) === OBJECT) {
         sec = path[i]
-        if (!(sec in obj)) obj[sec] = undefined
+        if (!(sec in obj)) {
+            obj[sec] = undefined
+            if (obj.__observer__) convert(obj, sec)
+        }
     }
-    return obj[sec]
 }
 
 /**
@@ -255,10 +278,10 @@ function observe (obj, rawPath, observer) {
         emitSet(obj)
     } else {
         var type = typeOf(obj)
-        if (type === 'Object') {
-            watchObject(obj, null, ob)
-        } else if (type === 'Array') {
-            watchArray(obj, null, ob)
+        if (type === OBJECT) {
+            watchObject(obj)
+        } else if (type === ARRAY) {
+            watchArray(obj)
         }
     }
 }
@@ -270,6 +293,7 @@ function unobserve (obj, path, observer) {
     if (!obj || !obj.__observer__) return
     path = path + '.'
     var proxies = observer.proxies[path]
+    if (!proxies) return
     obj.__observer__
         .off('get', proxies.get)
         .off('set', proxies.set)
@@ -281,7 +305,7 @@ module.exports = {
     observe     : observe,
     unobserve   : unobserve,
     ensurePath  : ensurePath,
-    ensurePaths : ensurePaths,
+    convert     : convert,
     // used in v-repeat
     watchArray  : watchArray,
 }

+ 1 - 0
test/functional/fixtures/extend.html

@@ -18,6 +18,7 @@
             <div class="cvm" v-component="vm-test">{{vmMsg}}</div>
         </div>
         <script>
+            Vue.config({debug:true})
             var log = document.getElementById('log')
             var T = Vue.extend({
                 created: function () {

+ 2 - 0
test/functional/fixtures/nested-viewmodels.html

@@ -55,6 +55,8 @@
         var Man = Vue.extend({
             created: function () {
                 this.name = this.$el.dataset.name
+                console.log(this.$el)
+                console.log(this.name)
                 var family = this.$el.dataset.family
                 if (family) {
                     this.family = family

+ 1 - 1
test/functional/fixtures/repeated-items.html

@@ -27,7 +27,7 @@
         </div>
         <script>
 
-            Vue.config({debug: true})
+            //Vue.config({debug: true})
 
             var items = [
                 { title: 'A'},

+ 1 - 0
test/functional/fixtures/share-data.html

@@ -15,6 +15,7 @@
             <pre>{{source}}</pre>
         </div>
         <script>
+            Vue.config({debug:true})
             var shared = {
                 msg: 'hello'
             }

+ 2 - 0
test/functional/fixtures/template.html

@@ -16,6 +16,8 @@
 
         <script>
 
+            Vue.config({debug:true})
+
             // direct usage
             var china  = new Vue({
                 id: 'china',

+ 0 - 11
test/unit/specs/directive.js

@@ -162,17 +162,6 @@ describe('UNIT: Directive', function () {
             assert.strictEqual(f.arg, 'todo', 'with filters')
         })
 
-        it('should extract correct nesting info', function () {
-            var d = Directive.parse('v-text', 'abc', compiler),
-                e = Directive.parse('v-text', '^abc', compiler),
-                f = Directive.parse('v-text', '^^^abc', compiler),
-                g = Directive.parse('v-text', '*abc', compiler)
-            assert.ok(d.nesting === false && d.root === false && d.key === 'abc', 'no nesting')
-            assert.ok(e.nesting === 1 && e.root === false && e.key === 'abc', '1 level')
-            assert.ok(f.nesting === 3 && f.root === false && f.key === 'abc', '3 levels')
-            assert.ok(g.root === true && g.nesting === false && g.key === 'abc', 'root')
-        })
-
         it('should be able to determine whether the key is an expression', function () {
             var d = Directive.parse('v-text', 'abc', compiler),
                 e = Directive.parse('v-text', '!abc', compiler),

+ 3 - 5
test/unit/specs/exp-parser.js

@@ -69,6 +69,7 @@ describe('UNIT: Expression Parser', function () {
             var caughtMissingPaths = [],
                 compilerMock = {
                     vm:{
+                        $scope: {},
                         $compiler:{
                             bindings:{},
                             createBinding: function (path) {
@@ -81,10 +82,6 @@ describe('UNIT: Expression Parser', function () {
                 vm     = testCase.vm,
                 vars   = testCase.paths || Object.keys(vm)
 
-            // mock the $get().
-            // the real $get() will be tested in integration tests.
-            vm.$get = function (key) { return this[key] }
-
             it('should get correct paths', function () {
                 if (!vars.length) return
                 assert.strictEqual(caughtMissingPaths.length, vars.length)
@@ -116,7 +113,8 @@ describe('UNIT: Expression Parser', function () {
                     $compiler: {
                         bindings: {},
                         createBinding: function () {}
-                    }
+                    },
+                    $scope: {}
                 }
             })
             assert.ok(warned)

+ 18 - 19
test/unit/specs/observer.js

@@ -81,7 +81,6 @@ describe('UNIT: Observer', function () {
                 { key: 'test.b.c', val: obj.b.c }
             ]
             ob2.on('set', function (key, val) {
-                console.log(key)
                 var exp = expects[i]
                 assert.strictEqual(key, exp.key)
                 assert.strictEqual(val, exp.val)
@@ -435,25 +434,25 @@ describe('UNIT: Observer', function () {
 
     })
 
-    describe('.ensurePaths()', function () {
+    // describe('.copyPaths()', function () {
         
-        it('should ensure path for all paths that start with the given key', function () {
-            var key = 'a',
-                obj = {},
-                paths = {
-                    'a.b.c': 1,
-                    'a.d': 2,
-                    'e.f': 3,
-                    'g': 4
-                }
-            Observer.ensurePaths(key, obj, paths)
-            assert.strictEqual(obj.b.c, undefined)
-            assert.strictEqual(obj.d, undefined)
-            assert.notOk('f' in obj)
-            assert.strictEqual(Object.keys(obj).length, 2)
-        })
-
-    })
+    //     it('should ensure path for all paths that start with the given key', function () {
+    //         var key = 'a',
+    //             obj = {},
+    //             paths = {
+    //                 'a.b.c': 1,
+    //                 'a.d': 2,
+    //                 'e.f': 3,
+    //                 'g': 4
+    //             }
+    //         Observer.ensurePaths(key, obj, paths)
+    //         assert.strictEqual(obj.b.c, undefined)
+    //         assert.strictEqual(obj.d, undefined)
+    //         assert.notOk('f' in obj)
+    //         assert.strictEqual(Object.keys(obj).length, 2)
+    //     })
+
+    // })
 
     function setTestFactory (opts) {
         return function () {