Ver código fonte

lift root compilation as per-instance so that component linkers are globally cachable

Evan You 11 anos atrás
pai
commit
5aa5c1144f

+ 1 - 0
component.json

@@ -21,6 +21,7 @@
     "src/cache.js",
     "src/compiler/compile.js",
     "src/compiler/content.js",
+    "src/compiler/index.js",
     "src/compiler/transclude.js",
     "src/config.js",
     "src/directive.js",

+ 1 - 0
src/api/child.js

@@ -33,6 +33,7 @@ exports.$addChild = function (opts, BaseCtor) {
         'this._init(options) }'
       )()
       ChildVue.options = BaseCtor.options
+      ChildVue.linker = BaseCtor.linker
       ChildVue.prototype = this
       ctors[BaseCtor.cid] = ChildVue
     }

+ 1 - 5
src/api/global.js

@@ -8,11 +8,7 @@ var config = require('../config')
 exports.util = _
 exports.nextTick = _.nextTick
 exports.config = require('../config')
-
-exports.compiler = {
-  compile: require('../compiler/compile'),
-  transclude: require('../compiler/transclude')
-}
+exports.compiler = require('../compiler')
 
 exports.parsers = {
   path: require('../parsers/path'),

+ 2 - 2
src/api/lifecycle.js

@@ -1,5 +1,5 @@
 var _ = require('../util')
-var compile = require('../compiler/compile')
+var compiler = require('../compiler')
 
 /**
  * Set instance target element and kick off the compilation
@@ -69,5 +69,5 @@ exports.$destroy = function (remove, deferCleanup) {
  */
 
 exports.$compile = function (el, host) {
-  return compile(el, this.$options, true, host)(this, el)
+  return compiler.compile(el, this.$options, true, host)(this, el)
 }

+ 56 - 18
src/compiler/compile.js

@@ -15,8 +15,6 @@ var terminalDirectives = [
   'if'
 ]
 
-module.exports = compile
-
 /**
  * Compile a template and return a reusable composite link
  * function, which recursively contains more link functions
@@ -30,11 +28,11 @@ module.exports = compile
  * @return {Function}
  */
 
-function compile (el, options, partial, host) {
+exports.compile = function (el, options, partial, host) {
   // link function for the node itself.
-  var nodeLinkFn = !partial
-    ? compileRoot(el, options)
-    : compileNode(el, options)
+  var nodeLinkFn = partial || !options._asComponent
+    ? compileNode(el, options)
+    : null
   // link function for the childNodes
   var childLinkFn =
     !(nodeLinkFn && nodeLinkFn.terminal) &&
@@ -73,13 +71,25 @@ function compile (el, options, partial, host) {
      * @param {Boolean} destroying
      */
     return function unlink (destroying) {
-      var i = dirs.length
-      while (i--) {
-        dirs[i]._teardown()
-        if (!destroying) {
-          vm._directives.$remove(dirs[i])
-        }
-      }
+      teardownDirs(vm, dirs, destroying)
+    }
+  }
+}
+
+/**
+ * Teardown partial linked directives.
+ *
+ * @param {Vue} vm
+ * @param {Array} dirs
+ * @param {Boolean} destroying
+ */
+
+function teardownDirs (vm, dirs, destroying) {
+  var i = dirs.length
+  while (i--) {
+    dirs[i]._teardown()
+    if (!destroying) {
+      vm._directives.$remove(dirs[i])
     }
   }
 }
@@ -96,12 +106,18 @@ function compile (el, options, partial, host) {
  * Also, if this is a block instance, we only need to
  * compile 1 & 2 here.
  *
+ * This function does compile and link at the same time,
+ * since root linkers can not be reused. It returns the
+ * unlink function for potential parent directives on the
+ * container.
+ *
+ * @param {Vue} vm
  * @param {Element} el
  * @param {Object} options
  * @return {Function}
  */
 
-function compileRoot (el, options) {
+ exports.compileRoot = function (vm, el, options) {
   var containerAttrs = options._containerAttrs
   var replacerAttrs = options._replacerAttrs
   var props = options.props
@@ -129,12 +145,34 @@ function compileRoot (el, options) {
       replacerLinkFn = compileDirectives(el, options)
     }
   }
-  return function rootLinkFn (vm, el, host) {
+
+  // link props
+  if (propsLinkFn) {
     // explicitly passing null to props
     // linkers because they don't need a real element
-    if (propsLinkFn) propsLinkFn(vm, null)
-    if (parentLinkFn) parentLinkFn(vm.$parent, el, host)
-    if (replacerLinkFn) replacerLinkFn(vm, el, host)
+    propsLinkFn(vm, null)
+  }
+
+  // link parent dirs
+  var parentDirs
+  var parent = vm.$parent
+  if (parent && parentLinkFn) {
+    var originalParentDirCount = parent._directives.length
+    parentLinkFn(parent, el)
+    parentDirs = parent._directives.slice(originalParentDirCount)
+  }
+
+  // link self
+  if (replacerLinkFn) {
+    replacerLinkFn(vm, el)
+  }
+
+  // return the unlink function that tearsdown parent
+  // container directives.
+  return function rootUnlinkFn () {
+    if (parentDirs) {
+      teardownDirs(parent, parentDirs)
+    }
   }
 }
 

+ 4 - 0
src/compiler/index.js

@@ -0,0 +1,4 @@
+var _ = require('../util')
+
+_.extend(exports, require('./compile'))
+_.extend(exports, require('./transclude'))

+ 1 - 1
src/compiler/transclude.js

@@ -14,7 +14,7 @@ var templateParser = require('../parsers/template')
  * @return {Element|DocumentFragment}
  */
 
-module.exports = function transclude (el, options) {
+exports.transclude = function (el, options) {
   // extract container attributes to pass them down
   // to compiler, because they need to be compiled in
   // parent scope. we are mutating the options object here

+ 3 - 0
src/directives/component.js

@@ -152,6 +152,9 @@ module.exports = {
       var child = vm.$addChild({
         el: el,
         template: this.template,
+        // if no inline-template, then the compiled
+        // linker can be cached for better performance.
+        _linkerCachable: !this.template,
         _asComponent: true,
         _host: this._host
       }, this.Ctor)

+ 2 - 2
src/directives/if.js

@@ -1,5 +1,5 @@
 var _ = require('../util')
-var compile = require('../compiler/compile')
+var compiler = require('../compiler')
 var templateParser = require('../parsers/template')
 var transition = require('../transition')
 
@@ -19,7 +19,7 @@ module.exports = {
         this.template.appendChild(templateParser.clone(el))
       }
       // compile the nested partial
-      this.linker = compile(
+      this.linker = compiler.compile(
         this.template,
         this.vm.$options,
         true

+ 15 - 23
src/directives/repeat.js

@@ -4,8 +4,7 @@ var isPlainObject = _.isPlainObject
 var textParser = require('../parsers/text')
 var expParser = require('../parsers/expression')
 var templateParser = require('../parsers/template')
-var compile = require('../compiler/compile')
-var transclude = require('../compiler/transclude')
+var compiler = require('../compiler')
 var uid = 0
 
 // async component resolution states
@@ -93,10 +92,10 @@ module.exports = {
       this.inherit = true
       // important: transclude with no options, just
       // to ensure block start and block end
-      this.template = transclude(this.template)
+      this.template = compiler.transclude(this.template)
       var copy = _.extend({}, options)
       copy._asComponent = false
-      this._linkFn = compile(this.template, copy)
+      this._linkFn = compiler.compile(this.template, copy)
     } else {
       this.Ctor = null
       this.asComponent = true
@@ -125,19 +124,6 @@ module.exports = {
         return
       }
       this.Ctor = Ctor
-      var merged = _.mergeOptions(Ctor.options, {}, {
-        $parent: this.vm
-      })
-      merged.template = this.inlineTempalte || merged.template
-      merged._asComponent = true
-      merged._parent = this.vm
-      this.template = transclude(this.template, merged)
-      this.content = merged._content
-      // Important: mark the template as a root node so that
-      // custom element components don't get compiled twice.
-      // fixes #822
-      this.template.__vue__ = true
-      this._linkFn = compile(this.template, merged)
       this.componentState = RESOLVED
       this.realUpdate(this.pendingData)
       this.pendingData = null
@@ -362,15 +348,21 @@ module.exports = {
     var Ctor = this.Ctor || this.resolveDynamicComponent(data, meta)
     var vm = this.vm.$addChild({
       el: templateParser.clone(this.template),
+      data: data,
+      inherit: this.inherit,
+      template: this.inlineTempalte,
+      // repeater meta, e.g. $index, $key
+      _meta: meta,
+      // mark this as an inline-repeat instance
+      _repeat: this.inherit,
+      // is this a component?
       _asComponent: this.asComponent,
+      // linker cachable if no inline-template
+      _linkerCachable: !this.inlineTempalte,
+      // transclusion host
       _host: this._host,
+      // pre-compiled linker for simple repeats
       _linkFn: this._linkFn,
-      _meta: meta,
-      _content: this.content,
-      _repeat: this.inherit,
-      data: data,
-      inherit: this.inherit,
-      template: this.inlineTempalte
     }, Ctor)
     // cache instance
     if (needCache) {

+ 34 - 9
src/instance/compile.js

@@ -1,7 +1,6 @@
 var _ = require('../util')
 var Directive = require('../directive')
-var compile = require('../compiler/compile')
-var transclude = require('../compiler/transclude')
+var compiler = require('../compiler')
 
 /**
  * Transclude, compile and link element.
@@ -18,19 +17,47 @@ var transclude = require('../compiler/transclude')
 
 exports._compile = function (el) {
   var options = this.$options
+  var host = this._host
   if (options._linkFn) {
     // pre-transcluded with linker, just use it
     this._initElement(el)
-    this._unlinkFn = options._linkFn(this, el)
+    this._unlinkFn = options._linkFn(this, el, host)
   } else {
     // transclude and init element
     // transclude can potentially replace original
-    // so we need to keep reference
+    // so we need to keep reference; this step also injects
+    // the template and caches the original attributes
+    // on the container node and replacer node.
     var original = el
-    el = transclude(el, options)
+    el = compiler.transclude(el, options)
     this._initElement(el)
+
+    // root is always compiled per-instance, because
+    // container attrs and props can be different every time.
+    var rootUnlinkFn = compiler.compileRoot(this, el, options)
+
     // compile and link the rest
-    this._unlinkFn = compile(el, options)(this, el)
+    var linker
+    var ctor = this.constructor
+    // component compilation can be cached
+    // as long as it's not using inline-template
+    if (options._linkerCachable) {
+      linker = ctor.linker
+      if (!linker) {
+        linker = ctor.linker = compiler.compile(el, options)
+      }
+    }
+    var contentUnlinkFn = linker
+      ? linker(this, el)
+      : compiler.compile(el, options)(this, el, host)
+
+    this._unlinkFn = function () {
+      rootUnlinkFn()
+      // passing destroying: true to avoid searching and
+      // splicing the directives
+      contentUnlinkFn(true)
+    }
+
     // finally replace original
     if (options.replace) {
       _.replace(original, el)
@@ -114,9 +141,7 @@ exports._destroy = function (remove, deferCleanup) {
   // teardown all directives. this also tearsdown all
   // directive-owned watchers.
   if (this._unlinkFn) {
-    // passing destroying: true to avoid searching and
-    // splicing the directives
-    this._unlinkFn(true)
+    this._unlinkFn()
   }
   i = this._watchers.length
   while (i--) {

+ 2 - 2
test/unit/specs/api/lifecycle_spec.js

@@ -1,6 +1,6 @@
 var Vue = require('../../../../src/vue')
 var _ = require('../../../../src/util')
-var compile = require('../../../../src/compiler/compile')
+var compiler = require('../../../../src/compiler')
 
 if (_.inBrowser) {
   describe('Lifecycle API', function () {
@@ -78,7 +78,7 @@ if (_.inBrowser) {
       })
       
       it('precompiled linker', function () {
-        var linker = compile(el, Vue.options)
+        var linker = compiler.compile(el, Vue.options)
         var vm = new Vue({
           _linker: linker,
           data: {

+ 29 - 8
test/unit/specs/compiler/compile_spec.js

@@ -1,8 +1,9 @@
 var Vue = require('../../../../src/vue')
 var _ = require('../../../../src/util')
 var dirParser = require('../../../../src/parsers/directive')
-var compile = require('../../../../src/compiler/compile')
-var transclude = require('../../../../src/compiler/transclude')
+var compiler = require('../../../../src/compiler')
+var compile = compiler.compile
+var transclude = compiler.transclude
 
 if (_.inBrowser) {
   describe('Compile', function () {
@@ -167,8 +168,7 @@ if (_.inBrowser) {
       el.setAttribute('with-filter', '{{a | filter}}')
       el.setAttribute('boolean-literal', '{{true}}')
       transclude(el, options)
-      var linker = compile(el, options)
-      linker(vm, el)
+      compiler.compileRoot(vm, el, options)
       // should skip literals and one-time bindings
       expect(vm._bindDir.calls.count()).toBe(5)
       // data-some-attr
@@ -226,8 +226,7 @@ if (_.inBrowser) {
       el.setAttribute('a', 'hi')
       el.setAttribute('b', '{{hi}}')
       transclude(el, options)
-      var linker = compile(el, options)
-      linker(vm, el)
+      compiler.compileRoot(vm, el, options)
       expect(vm._bindDir.calls.count()).toBe(0)
       expect(vm.$set).toHaveBeenCalledWith('a', 'hi')
       expect(hasWarned(_, 'Cannot bind dynamic prop on a root')).toBe(true)
@@ -295,7 +294,29 @@ if (_.inBrowser) {
       expect(childSpy).toHaveBeenCalledWith(2)
     })
 
-    it('should remove transcluded directives from parent when unlinking (v-component)', function () {
+    it('should remove parent container directives from parent when unlinking', function () {
+      var vm = new Vue({
+        el: el,
+        template:
+          '<test v-show="ok"></test>',
+        data: {
+          ok: true
+        },
+        components: {
+          test: {
+            template: 'hi'
+          }
+        }
+      })
+      expect(el.firstChild.style.display).toBe('')
+      expect(vm._directives.length).toBe(2)
+      expect(vm._children.length).toBe(1)
+      vm._children[0].$destroy()
+      expect(vm._directives.length).toBe(1)
+      expect(vm._children.length).toBe(0)
+    })
+
+    it('should remove transcluded directives from parent when unlinking (component)', function () {
       var vm = new Vue({
         el: el,
         template:
@@ -317,7 +338,7 @@ if (_.inBrowser) {
       expect(vm._children.length).toBe(0)
     })
 
-    it('should remove transcluded directives from parent when unlinking (v-if + v-component)', function (done) {
+    it('should remove transcluded directives from parent when unlinking (v-if + component)', function (done) {
       var vm = new Vue({
         el: el,
         template:

+ 1 - 1
test/unit/specs/compiler/transclude_spec.js

@@ -1,4 +1,4 @@
-var transclude = require('../../../../src/compiler/transclude')
+var transclude = require('../../../../src/compiler').transclude
 var _ = require('../../../../src/util')
 
 if (_.inBrowser) {

+ 1 - 1
test/unit/specs/directives/cloak_spec.js

@@ -1,5 +1,5 @@
 var _ = require('../../../../src/util')
-var compile = require('../../../../src/compiler/compile')
+var compile = require('../../../../src/compiler').compile
 var Vue = require('../../../../src/vue')
 
 if (_.inBrowser) {