Quellcode durchsuchen

refactor transclusion logic

transcluded contents are now marked with a "transcluded"
attribute so that the compiler knows to compile them in
parent scope. this allows proper re-compile of transcluded
blocks in conditionals (v-if and v-partial).
Evan You vor 11 Jahren
Ursprung
Commit
792c139491

+ 66 - 39
src/compiler/compile.js

@@ -4,27 +4,22 @@ var textParser = require('../parsers/text')
 var dirParser = require('../parsers/directive')
 var templateParser = require('../parsers/template')
 
+module.exports = compile
+
 /**
  * Compile a template and return a reusable composite link
  * function, which recursively contains more link functions
  * inside. This top level compile function should only be
  * called on instance root nodes.
  *
- * When the `asParent` flag is true, this means we are doing
- * a partial compile for a component's parent scope markup
- * (See #502). This could **only** be triggered during
- * compilation of `v-component`, and we need to skip v-with,
- * v-ref & v-component in this situation.
- *
  * @param {Element|DocumentFragment} el
  * @param {Object} options
  * @param {Boolean} partial
- * @param {Boolean} asParent - compiling a component
- *                             container as its parent.
+ * @param {Boolean} transcluded
  * @return {Function}
  */
 
-module.exports = function compile (el, options, partial, asParent) {
+function compile (el, options, partial, transcluded) {
   var isBlock = el.nodeType === 11
   var params = !partial && options.paramAttributes
   // if el is a fragment, this is a block instance
@@ -37,7 +32,7 @@ module.exports = function compile (el, options, partial, asParent) {
     : null
   var nodeLinkFn = isBlock
     ? null
-    : compileNode(el, options, asParent)
+    : compileNode(el, options)
   var childLinkFn =
     !(nodeLinkFn && nodeLinkFn.terminal) &&
     el.tagName !== 'SCRIPT' &&
@@ -57,12 +52,16 @@ module.exports = function compile (el, options, partial, asParent) {
 
   return function link (vm, el) {
     var originalDirCount = vm._directives.length
+    var parentOriginalDirCount =
+      vm.$parent && vm.$parent._directives.length
     if (paramsLinkFn) {
       var paramsEl = isBlock ? el.childNodes[1] : el
       paramsLinkFn(vm, paramsEl)
     }
     // cache childNodes before linking parent, fix #657
     var childNodes = _.toArray(el.childNodes)
+    // if transcluded, link in parent scope
+    if (transcluded) vm = vm.$parent
     if (nodeLinkFn) nodeLinkFn(vm, el)
     if (childLinkFn) childLinkFn(vm, childNodes)
 
@@ -73,9 +72,12 @@ module.exports = function compile (el, options, partial, asParent) {
      * linking.
      */
 
-    if (partial) {
-      var dirs = vm._directives.slice(originalDirCount)
-      return function unlink () {
+    if (partial && !transcluded) {
+      var selfDirs = vm._directives.slice(originalDirCount)
+      var parentDirs = vm.$parent &&
+        vm.$parent._directives.slice(parentOriginalDirCount)
+
+      var teardownDirs = function (vm, dirs) {
         var i = dirs.length
         while (i--) {
           dirs[i]._teardown()
@@ -83,6 +85,13 @@ module.exports = function compile (el, options, partial, asParent) {
         i = vm._directives.indexOf(dirs[0])
         vm._directives.splice(i, dirs.length)
       }
+
+      return function unlink () {
+        teardownDirs(vm, selfDirs)
+        if (parentDirs) {
+          teardownDirs(vm.$parent, parentDirs)
+        }
+      }
     }
   }
 }
@@ -93,14 +102,13 @@ module.exports = function compile (el, options, partial, asParent) {
  *
  * @param {Node} node
  * @param {Object} options
- * @param {Boolean} asParent
  * @return {Function|null}
  */
 
-function compileNode (node, options, asParent) {
+function compileNode (node, options) {
   var type = node.nodeType
   if (type === 1 && node.tagName !== 'SCRIPT') {
-    return compileElement(node, options, asParent)
+    return compileElement(node, options)
   } else if (type === 3 && config.interpolate && node.data.trim()) {
     return compileTextNode(node, options)
   } else {
@@ -113,14 +121,20 @@ function compileNode (node, options, asParent) {
  *
  * @param {Element} el
  * @param {Object} options
- * @param {Boolean} asParent
  * @return {Function|null}
  */
 
-function compileElement (el, options, asParent) {
+function compileElement (el, options) {
+  if (checkTransclusion(el)) {
+    // unwrap textNode
+    if (el.hasAttribute('__vue__wrap')) {
+      el = el.firstChild
+    }
+    return compile(el, options._parent.$options, true, true)
+  }
   var linkFn, tag, component
   // check custom element component, but only on non-root
-  if (!asParent && !el.__vue__) {
+  if (!el.__vue__) {
     tag = el.tagName.toLowerCase()
     component =
       tag.indexOf('-') > 0 &&
@@ -131,12 +145,10 @@ function compileElement (el, options, asParent) {
   }
   if (component || el.hasAttributes()) {
     // check terminal direcitves
-    if (!asParent) {
-      linkFn = checkTerminalDirectives(el, options)
-    }
+    linkFn = checkTerminalDirectives(el, options)
     // if not terminal, build normal link function
     if (!linkFn) {
-      var dirs = collectDirectives(el, options, asParent)
+      var dirs = collectDirectives(el, options)
       linkFn = dirs.length
         ? makeDirectivesLinkFn(dirs)
         : null
@@ -166,16 +178,21 @@ function makeDirectivesLinkFn (directives) {
   return function directivesLinkFn (vm, el) {
     // reverse apply because it's sorted low to high
     var i = directives.length
-    var dir, j, k
+    var dir, j, k, target
     while (i--) {
       dir = directives[i]
+      // a directive can be transcluded if it's written
+      // on a component's container in its parent tempalte.
+      target = dir.transcluded
+        ? vm.$parent
+        : vm
       if (dir._link) {
         // custom link fn
-        dir._link(vm, el)
+        dir._link(target, el)
       } else {
         k = dir.descriptors.length
         for (j = 0; j < k; j++) {
-          vm._bindDir(dir.name, el,
+          target._bindDir(dir.name, el,
                       dir.descriptors[j], dir.def)
         }
       }
@@ -478,38 +495,37 @@ function makeTeriminalLinkFn (el, dirName, value, options) {
  *
  * @param {Element} el
  * @param {Object} options
- * @param {Boolean} asParent
  * @return {Array}
  */
 
-function collectDirectives (el, options, asParent) {
+function collectDirectives (el, options) {
   var attrs = _.toArray(el.attributes)
   var i = attrs.length
   var dirs = []
-  var attr, attrName, dir, dirName, dirDef
+  var attr, attrName, dir, dirName, dirDef, transcluded
   while (i--) {
     attr = attrs[i]
     attrName = attr.name
+    transcluded =
+      options._transcludedAttrs &&
+      options._transcludedAttrs[attrName]
     if (attrName.indexOf(config.prefix) === 0) {
       dirName = attrName.slice(config.prefix.length)
-      if (asParent &&
-          (dirName === 'with' ||
-           dirName === 'component')) {
-        continue
-      }
       dirDef = options.directives[dirName]
       _.assertAsset(dirDef, 'directive', dirName)
       if (dirDef) {
         dirs.push({
           name: dirName,
           descriptors: dirParser.parse(attr.value),
-          def: dirDef
+          def: dirDef,
+          transcluded: transcluded
         })
       }
     } else if (config.interpolate) {
       dir = collectAttrDirective(el, attrName, attr.value,
                                  options)
       if (dir) {
+        dir.transcluded = transcluded
         dirs.push(dir)
       }
     }
@@ -531,10 +547,6 @@ function collectDirectives (el, options, asParent) {
  */
 
 function collectAttrDirective (el, name, value, options) {
-  if (options._skipAttrs &&
-      options._skipAttrs.indexOf(name) > -1) {
-    return
-  }
   var tokens = textParser.parse(value)
   if (tokens) {
     var def = options.directives.attr
@@ -572,4 +584,19 @@ function directiveComparator (a, b) {
   a = a.def.priority || 0
   b = b.def.priority || 0
   return a > b ? 1 : -1
+}
+
+/**
+ * Check whether an element is transcluded
+ *
+ * @param {Element} el
+ * @return {Boolean}
+ */
+
+var transcludedFlagAttr = '__vue__transcluded'
+function checkTransclusion (el) {
+  if (el.nodeType === 1 && el.hasAttribute(transcludedFlagAttr)) {
+    el.removeAttribute(transcludedFlagAttr)
+    return true
+  }
 }

+ 8 - 41
src/directives/if.js

@@ -12,15 +12,11 @@ module.exports = {
       this.end = document.createComment('v-if-end')
       _.replace(el, this.end)
       _.before(this.start, this.end)
-
-      // Note: content transclusion is not available for
-      // <template> blocks
       if (el.tagName === 'TEMPLATE') {
         this.template = templateParser.parse(el, true)
       } else {
         this.template = document.createDocumentFragment()
         this.template.appendChild(templateParser.clone(el))
-        this.checkContent()
       }
       // compile the nested partial
       this.linker = compile(
@@ -37,32 +33,6 @@ module.exports = {
     }
   },
 
-  // check if there are any content nodes from parent.
-  // these nodes are compiled by the parent and should
-  // not be cloned during a re-compilation - otherwise the
-  // parent directives bound to them will no longer work.
-  // (see #736)
-  checkContent: function () {
-    var el = this.el
-    for (var i = 0; i < el.childNodes.length; i++) {
-      var node = el.childNodes[i]
-      // _isContent is a flag set in instance/compile
-      // after the raw content has been compiled by parent
-      if (node._isContent) {
-        ;(this.contentNodes = this.contentNodes || []).push(node)
-        ;(this.contentPositions = this.contentPositions || []).push(i)
-      }
-    }
-    // keep track of any transcluded components contained within
-    // the conditional block. we need to call attach/detach hooks
-    // for them.
-    this.transCpnts =
-      this.vm._transCpnts &&
-      this.vm._transCpnts.filter(function (c) {
-        return el.contains(c.$el)
-      })
-  },
-
   update: function (value) {
     if (this.invalid) return
     if (value) {
@@ -70,15 +40,6 @@ module.exports = {
       // called with different truthy values
       if (!this.unlink) {
         var frag = templateParser.clone(this.template)
-        // persist content nodes from parent.
-        if (this.contentNodes) {
-          var el = frag.childNodes[0]
-          for (var i = 0, l = this.contentNodes.length; i < l; i++) {
-            var node = this.contentNodes[i]
-            var j = this.contentPositions[i]
-            el.replaceChild(node, el.childNodes[j])
-          }
-        }
         this.compile(frag)
       }
     } else {
@@ -90,13 +51,19 @@ module.exports = {
   compile: function (frag) {
     var vm = this.vm
     var originalChildLength = vm._children.length
+    var originalParentChildLength = vm.$parent &&
+      vm.$parent._children.length
+    // the linker is not guaranteed to be present because
+    // this function might get called by v-partial 
     this.unlink = this.linker
       ? this.linker(vm, frag)
       : vm.$compile(frag)
     transition.blockAppend(frag, this.end, vm)
     this.children = vm._children.slice(originalChildLength)
-    if (this.transCpnts) {
-      this.children = this.children.concat(this.transCpnts)
+    if (vm.$parent) {
+      this.children = this.children.concat(
+        vm.$parent._children.slice(originalParentChildLength)
+      )
     }
     if (this.children.length && _.inDoc(vm.$el)) {
       this.children.forEach(function (child) {

+ 1 - 1
src/directives/repeat.js

@@ -120,7 +120,7 @@ module.exports = {
           })
           merged.template = this.inlineTempalte || merged.template
           this.template = transclude(this.template, merged)
-          this._linkFn = compile(this.template, merged, false, true)
+          this._linkFn = compile(this.template, merged)
         }
       } else {
         // to be resolved later

+ 49 - 47
src/instance/compile.js

@@ -1,7 +1,9 @@
 var _ = require('../util')
+var config = require('../config')
 var Directive = require('../directive')
 var compile = require('../compiler/compile')
 var transclude = require('../compiler/transclude')
+var transcludedFlagAttr = '__vue__transcluded'
 
 /**
  * Transclude, compile and link element.
@@ -18,55 +20,42 @@ var transclude = require('../compiler/transclude')
 
 exports._compile = function (el) {
   var options = this.$options
-  var parent = options._parent
   if (options._linkFn) {
     this._initElement(el)
     options._linkFn(this, el)
   } else {
-    var raw = el
     if (options._asComponent) {
-      // separate container element and content
-      var content = options._content = _.extractContent(raw)
-      // create two separate linekrs for container and content
-      var parentOptions = parent.$options
-      
-      // hack: we need to skip the paramAttributes for this
-      // child instance when compiling its parent container
-      // linker. there could be a better way to do this.
-      parentOptions._skipAttrs = options.paramAttributes
-      var containerLinkFn =
-        compile(raw, parentOptions, true, true)
-      parentOptions._skipAttrs = null
-
-      if (content) {
-        var ol = parent._children.length
-        var contentLinkFn =
-          compile(content, parentOptions, true)
-        // call content linker now, before transclusion
-        this._contentUnlinkFn = contentLinkFn(parent, content)
-        // mark all compiled nodes as transcluded, so that
-        // directives that do partial compilation, e.g. v-if
-        // and v-partial can detect them and persist them
-        // through re-compilations.
-        for (var i = 0; i < content.childNodes.length; i++) {
-          content.childNodes[i]._isContent = true
+      // Mark content nodes and attrs so that the compiler
+      // knows they should be compiled in parent scope.
+      options._transcludedAttrs = extractAttrs(el.attributes)
+      var i = el.childNodes.length
+      while (i--) {
+        var node = el.childNodes[i]
+        if (node.nodeType === 1) {
+          node.setAttribute(transcludedFlagAttr, '')
+        } else if (node.nodeType === 3 && node.data.trim()) {
+          // wrap transcluded textNodes in spans, because
+          // raw textNodes can't be persisted through clones
+          // by attaching attributes.
+          var wrapper = document.createElement('span')
+          wrapper.textContent = node.data
+          wrapper.setAttribute('__vue__wrap', '')
+          wrapper.setAttribute(transcludedFlagAttr, '')
+          el.replaceChild(wrapper, node)
         }
-        this._transCpnts = parent._children.slice(ol)
       }
-      // tranclude, this possibly replaces original
-      el = transclude(el, options)
-      this._initElement(el)
-      // now call the container linker on the resolved el
-      this._containerUnlinkFn = containerLinkFn(parent, el)
-    } else {
-      // simply transclude
-      el = transclude(el, options)
-      this._initElement(el)
     }
-    var linkFn = compile(el, options)
-    linkFn(this, el)
+    // transclude and init element
+    // transclude can potentially replace original
+    // so we need to keep reference
+    var original = el
+    el = transclude(el, options)
+    this._initElement(el)
+    // compile and link the rest
+    compile(el, options)(this, el)
+    // finally replace original
     if (options.replace) {
-      _.replace(raw, el)
+      _.replace(original, el)
     }
   }
   return el
@@ -136,13 +125,6 @@ exports._destroy = function (remove, deferCleanup) {
   while (i--) {
     this._children[i].$destroy()
   }
-  // teardown parent linkers
-  if (this._containerUnlinkFn) {
-    this._containerUnlinkFn()
-  }
-  if (this._contentUnlinkFn) {
-    this._contentUnlinkFn()
-  }
   // teardown all directives. this also tearsdown all
   // directive-owned watchers. intentionally check for
   // directives array length on every loop since directives
@@ -197,4 +179,24 @@ exports._cleanup = function () {
   this._callHook('destroyed')
   // turn off all instance listeners.
   this.$off()
+}
+
+/**
+ * Helper to extract a component container's attribute names
+ * into a map, and filtering out `v-with` in the process.
+ * The resulting map will be used in compiler/compile to
+ * determine whether an attribute is transcluded.
+ *
+ * @param {NameNodeMap} attrs
+ */
+
+function extractAttrs (attrs) {
+  var res = {}
+  var vwith = config.prefix + 'with'
+  var i = attrs.length
+  while (i--) {
+    var name = attrs[i].name
+    if (name !== vwith) res[name] = true
+  }
+  return res
 }

+ 0 - 9
test/unit/specs/compiler/compile_spec.js

@@ -204,14 +204,5 @@ if (_.inBrowser) {
       expect(vm._bindDir.calls.count()).toBe(0)
     })
 
-    it('component parent scope compilation should skip v-with & v-component', function () {
-      el.innerHTML = '<div v-component v-with="test"></div>'
-      el = el.firstChild
-      var linker = compile(el, Vue.options, true, true)
-      linker(vm, el)
-      expect(vm._directives.length).toBe(0)
-      expect(el.attributes.length).toBe(2)
-    })
-
   })
 }